package plg_backend_azure

import (
	"context"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
	"sync"

	. "github.com/mickael-kerjean/filestash/server/common"

	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
)

type AzureBlob struct {
	client *azblob.Client
	ctx    context.Context
}

func init() {
	Backend.Register("azure", &AzureBlob{})
}

func (this *AzureBlob) Init(params map[string]string, app *App) (IBackend, error) {
	cred, err := container.NewSharedKeyCredential(params["account_name"], params["account_key"])
	if err != nil {
		Log.Debug("plg_backend_azure::new_cred_error %s", err.Error())
		return nil, ErrAuthenticationFailed
	}
	serviceURL := fmt.Sprintf("https://%s.blob.core.windows.net/", params["account_name"])
	client, err := azblob.NewClientWithSharedKeyCredential(serviceURL, cred, nil)
	if err != nil {
		Log.Debug("plg_backend_azure::new_client_error %s", err.Error())
		return nil, ErrAuthenticationFailed
	}
	this.ctx = app.Context
	this.client = client
	return this, nil
}

func (this *AzureBlob) LoginForm() Form {
	return Form{
		Elmnts: []FormElement{
			FormElement{
				Name:  "type",
				Type:  "hidden",
				Value: "azure",
			},
			FormElement{
				Name:        "account_name",
				Type:        "text",
				Placeholder: "Account Name",
			},
			FormElement{
				Name:        "account_key",
				Type:        "password",
				Placeholder: "Account Key",
			},
			FormElement{
				Name:        "path",
				Type:        "text",
				Placeholder: "Path",
			},
		},
	}
}

func (this *AzureBlob) Ls(path string) ([]os.FileInfo, error) {
	files := make([]os.FileInfo, 0)
	ap := this.path(path)

	if ap.containerName == "" {
		pager := this.client.NewListContainersPager(nil)
		for pager.More() {
			resp, err := pager.NextPage(this.ctx)
			if err != nil {
				return files, err
			}
			for _, blob := range resp.ListContainersSegmentResponse.ContainerItems {
				files = append(files, File{
					FName: *blob.Name,
					FType: "directory",
					FTime: blob.Properties.LastModified.Unix(),
					FSize: -1,
				})
			}
		}
		return files, nil
	}

	client := this.client.ServiceClient().NewContainerClient(ap.containerName)
	pager := client.NewListBlobsHierarchyPager("/", &container.ListBlobsHierarchyOptions{
		Prefix: &ap.blobName,
	})
	for pager.More() {
		resp, err := pager.NextPage(this.ctx)
		if err != nil {
			return files, err
		}
		for _, blob := range resp.ListBlobsHierarchySegmentResponse.Segment.BlobPrefixes {
			files = append(files, File{
				FName: filepath.Base(*blob.Name),
				FType: "directory",
				FTime: -1,
				FSize: -1,
			})
		}
		for _, blob := range resp.ListBlobsHierarchySegmentResponse.Segment.BlobItems {
			files = append(files, File{
				FName: filepath.Base(*blob.Name),
				FType: "file",
				FTime: blob.Properties.LastModified.Unix(),
				FSize: *blob.Properties.ContentLength,
			})
		}
	}
	return files, nil
}

func (this AzureBlob) Cat(path string) (io.ReadCloser, error) {
	ap := this.path(path)
	return &azureFilecat{
		offset: 0,
		ctx:    this.ctx,
		ap:     ap,
		client: this.client,
		reader: nil,
	}, nil
}

type azureFilecat struct {
	offset int64
	ctx    context.Context
	ap     azurePath
	reader io.ReadCloser
	client *azblob.Client
	mu     sync.Mutex
}

func (this *azureFilecat) Read(p []byte) (n int, err error) {
	this.mu.Lock()
	defer this.mu.Unlock()
	if this.reader == nil {
		resp, err := this.client.DownloadStream(
			this.ctx,
			this.ap.containerName,
			this.ap.blobName,
			&azblob.DownloadStreamOptions{
				Range: azblob.HTTPRange{
					Offset: this.offset,
				},
			},
		)
		if err != nil {
			return 0, err
		}
		this.reader = resp.Body
	}
	return this.reader.Read(p)
}

func (this *azureFilecat) Seek(offset int64, whence int) (int64, error) {
	this.mu.Lock()
	defer this.mu.Unlock()
	if offset < 0 {
		return this.offset, os.ErrInvalid
	}

	switch whence {
	case io.SeekStart:
	case io.SeekCurrent:
		offset += this.offset
	case io.SeekEnd:
		props, err := this.client.ServiceClient().NewContainerClient(this.ap.containerName).NewBlockBlobClient(this.ap.blobName).GetProperties(this.ctx, nil)
		if err != nil {
			return this.offset, err
		}
		offset += *props.ContentLength
	default:
		return this.offset, ErrNotImplemented
	}

	this.offset = offset
	return this.offset, nil
}

func (this *azureFilecat) Close() error {
	this.mu.Lock()
	defer this.mu.Unlock()
	if this.reader == nil {
		return nil
	}
	return this.reader.Close()
}

func (this *AzureBlob) Mkdir(path string) error {
	ap := this.path(path)
	if ap.blobName == "" {
		_, err := this.client.CreateContainer(this.ctx, ap.containerName, nil)
		return err
	}
	_, err := this.client.UploadBuffer(this.ctx, ap.containerName, ap.blobName+".keep", []byte(""), nil)
	return err
}

func (this *AzureBlob) Rm(path string) error {
	ap := this.path(path)
	if ap.blobName == "" {
		_, err := this.client.DeleteContainer(this.ctx, ap.containerName, nil)
		return err
	}
	if strings.HasSuffix(path, "/") == false {
		_, err := this.client.DeleteBlob(this.ctx, ap.containerName, ap.blobName, nil)
		return err
	}
	pager := this.client.NewListBlobsFlatPager(ap.containerName, &container.ListBlobsFlatOptions{
		Include: container.ListBlobsInclude{Snapshots: true, Versions: true},
		Prefix:  &ap.blobName,
	})
	for pager.More() {
		resp, err := pager.NextPage(this.ctx)
		if err != nil {
			return err
		}
		for _, blob := range resp.Segment.BlobItems {
			_, err := this.client.DeleteBlob(context.Background(), ap.containerName, *blob.Name, nil)
			if err != nil {
				return err
			}
		}
	}
	return nil
}

func (this *AzureBlob) Mv(from string, to string) error {
	return ErrNotSupported
}

func (this *AzureBlob) Touch(path string) error {
	return this.Save(path, strings.NewReader(""))
}

func (this *AzureBlob) Save(path string, file io.Reader) error {
	ap := this.path(path)
	_, err := this.client.UploadStream(
		this.ctx, ap.containerName, ap.blobName, file,
		nil,
	)
	return err
}

func (this *AzureBlob) Meta(path string) Metadata {
	if path == "/" {
		return Metadata{
			CanCreateFile: NewBool(false),
			CanRename:     NewBool(false),
			CanMove:       NewBool(false),
			CanUpload:     NewBool(false),
		}
	}
	return Metadata{
		CanRename: NewBool(false),
		CanMove:   NewBool(false),
	}
}

type azurePath struct {
	containerName string
	blobName      string
}

func (this AzureBlob) path(path string) azurePath {
	ap := azurePath{}
	path = strings.TrimLeft(path, "/")
	sp := strings.SplitN(path, "/", 2)
	if len(sp) != 2 {
		return ap
	}
	ap.containerName = sp[0]
	ap.blobName = sp[1]
	return ap
}
