package plg_backend_backblaze

import (
	"bytes"
	"crypto/sha1"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

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

var backblaze_cache AppCache

type Backblaze struct {
	params      map[string]string
	Buckets     map[string]string
	ApiUrl      string `json:"apiUrl"`
	DownloadUrl string `json:"downloadUrl"`
	AccountId   string `json:"accountId"`
	Token       string `json:"authorizationToken"`
	Status      int    `json:"status"`
}

type BackblazeError struct {
	Code    string `json:"code"`
	Message string `json:"message"`
	Status  int    `json:"status"`
}

func init() {
	Backend.Register("backblaze", Backblaze{})
	backblaze_cache = NewAppCache()
}

func (this Backblaze) Init(params map[string]string, app *App) (IBackend, error) {
	this.params = params

	// By default backblaze required quite a few API calls to just find the data that's under a given bucket
	// This would result in a slow application hence we are caching everyting that's in the hot path
	if obj := backblaze_cache.Get(params); obj != nil {
		return obj.(*Backblaze), nil
	}

	// To perform some query, we need to first know things like where we will have to query, get a token, ...
	res, err := this.request("GET", "https://api.backblazeb2.com/b2api/v2/b2_authorize_account", nil, nil)
	if err != nil {
		return nil, err
	}
	body, err := io.ReadAll(res.Body)
	res.Body.Close()
	if err != nil {
		return nil, err
	}
	if err := json.Unmarshal(body, &this); err != nil {
		return nil, err
	}

	// Extract bucket related information as backblaze use bucketId as an identifer
	// BucketId is just some internal ref as people expect to see the bucketName
	res, err = this.request(
		"POST",
		this.ApiUrl+"/b2api/v2/b2_list_buckets",
		strings.NewReader(fmt.Sprintf(
			`{"accountId":"%s"}`,
			this.AccountId,
		)),
		nil,
	)
	if err != nil {
		return nil, err
	}
	body, err = io.ReadAll(res.Body)
	res.Body.Close()
	if err != nil {
		return nil, err
	}
	var buckets struct {
		Buckets []struct {
			BucketId   string `json:"bucketId"`
			BucketName string `json:"bucketName"`
		} `json:"buckets"`
	}
	if err = json.Unmarshal(body, &buckets); err != nil {
		return nil, err
	}
	this.Buckets = make(map[string]string, len(buckets.Buckets))
	for i := range buckets.Buckets {
		this.Buckets[buckets.Buckets[i].BucketName] = buckets.Buckets[i].BucketId
	}
	delete(params, "password")
	backblaze_cache.Set(params, &this)
	return this, nil
}

func (this Backblaze) LoginForm() Form {
	return Form{
		Elmnts: []FormElement{
			FormElement{
				Name:  "type",
				Type:  "hidden",
				Value: "backblaze",
			},
			FormElement{
				Name:        "username",
				Type:        "text",
				Placeholder: "KeyID",
			},
			FormElement{
				Name:        "password",
				Type:        "password",
				Placeholder: "applicationKey",
			},
		},
	}
}

func (this Backblaze) Ls(path string) ([]os.FileInfo, error) {
	if path == "/" {
		files := make([]os.FileInfo, 0, len(this.Buckets))
		for key := range this.Buckets {
			files = append(files, File{
				FName: key,
				FType: "directory",
			})
		}
		return files, nil
	}

	// prepare the query
	p := this.path(path)
	reqJSON, _ := json.Marshal(struct {
		BucketId     string `json:"bucketId"`
		Delimiter    string `json:"delimiter"`
		MaxFileCount int    `json:"maxFileCount"`
		Prefix       string `json:"prefix"`
	}{p.BucketId, "/", 10000, p.Prefix})
	res, err := this.request(
		"POST",
		this.ApiUrl+"/b2api/v2/b2_list_file_names",
		bytes.NewReader(reqJSON),
		nil,
	)
	if err != nil {
		return nil, err
	}
	body, err := io.ReadAll(res.Body)
	res.Body.Close()
	if err != nil {
		return nil, err
	}
	var resBody struct {
		Files []struct {
			FType string `json:"action"`
			Size  int64  `json:"contentLength"`
			Name  string `json:"fileName"`
			Time  int64  `json:"uploadTimestamp"`
		} `json:"files"`
	}
	if err = json.Unmarshal(body, &resBody); err != nil {
		return nil, err
	}
	files := make([]os.FileInfo, len(resBody.Files))
	for i := range resBody.Files {
		files[i] = File{
			FName: strings.TrimSuffix(strings.TrimPrefix(resBody.Files[i].Name, p.Prefix), "/"),
			FType: func() string {
				if resBody.Files[i].FType == "folder" {
					return "directory"
				}
				return "file"
			}(),
			FSize: resBody.Files[i].Size,
			FTime: resBody.Files[i].Time / 1000,
		}
	}
	return files, nil
}

func (this Backblaze) Cat(path string) (io.ReadCloser, error) {
	res, err := this.request(
		"GET",
		this.DownloadUrl+"/file"+path+"?Authorization="+this.Token,
		nil, nil,
	)
	if err != nil {
		return nil, err
	}
	return res.Body, nil
}

func (this Backblaze) Mkdir(path string) error {
	p := this.path(path)

	if p.BucketId == "" {
		bucketName := ""
		if bp := strings.Split(path, "/"); len(bp) > 1 {
			bucketName = bp[1]
		}
		if bucketName == "" {
			return ErrNotValid
		}
		res, err := this.request(
			"POST",
			this.ApiUrl+"/b2api/v2/b2_create_bucket",
			strings.NewReader(fmt.Sprintf(
				`{"accountId": "%s", "bucketName": "%s", "bucketType": "allPrivate"}`,
				this.AccountId,
				bucketName,
			)),
			nil,
		)
		if err != nil {
			return err
		}
		body, err := io.ReadAll(res.Body)
		res.Body.Close()
		if err != nil {
			return err
		}
		var resError BackblazeError
		if err := json.Unmarshal(body, &resError); err != nil {
			return err
		}
		if resError.Message != "" {
			return NewError(resError.Message, resError.Status)
		}
		return nil
	}

	return this.Touch(path + ".bzEmpty")
}

func (this Backblaze) Rm(path string) error {
	p := this.path(path)
	if p.BucketId == "" {
		return ErrNotValid
	}
	if p.Prefix == "" {
		backblaze_cache.Del(this.params) // cache invalidation
		res, err := this.request(
			"POST",
			this.ApiUrl+"/b2api/v2/b2_delete_bucket",
			strings.NewReader(fmt.Sprintf(
				`{"accountId": "%s", "bucketId": "%s"}`,
				this.AccountId,
				p.BucketId,
			)),
			nil,
		)
		if err != nil {
			return err
		}
		body, err := io.ReadAll(res.Body)
		res.Body.Close()
		if err != nil {
			return err
		}
		var resError BackblazeError
		if err := json.Unmarshal(body, &resError); err != nil {
			return err
		}
		if resError.Message != "" {
			return NewError(resError.Message, resError.Status)
		}
		return nil
	}

	// Backblaze doesn't provide a recursive API to delete => requires multiple steps
	// Step 1: find every files in a folder: b2_list_file_names
	res, err := this.request(
		"POST",
		this.ApiUrl+"/b2api/v2/b2_list_file_names",
		strings.NewReader(fmt.Sprintf(
			`{"bucketId": "%s", "maxFileCount": 10000, "delimiter": "/", "prefix": "%s"}`,
			p.BucketId, p.Prefix,
		)),
		nil,
	)
	if err != nil {
		return err
	}
	body, err := io.ReadAll(res.Body)
	res.Body.Close()
	if err != nil {
		return err
	}
	bRes := struct {
		Files []struct {
			FileId   string `json:"fileId"`
			FileName string `json:"fileName"`
		} `json:"files"`
	}{}
	if err = json.Unmarshal(body, &bRes); err != nil {
		return err
	}
	// Step 2: delete files 1 by 1: b2_delete_file_version
	for i := range bRes.Files {
		res, err := this.request(
			"POST",
			this.ApiUrl+"/b2api/v2/b2_delete_file_version",
			strings.NewReader(fmt.Sprintf(
				`{"fileName": "%s", "fileId": "%s"}`,
				bRes.Files[i].FileName, bRes.Files[i].FileId,
			)),
			nil,
		)
		if err != nil {
			return err
		}
		if body, err = io.ReadAll(res.Body); err != nil {
			return err
		}
		res.Body.Close()
		var resError BackblazeError
		if err := json.Unmarshal(body, &resError); err != nil {
			return err
		}
		if resError.Message != "" {
			return NewError(resError.Message, resError.Status)
		}
	}
	return nil
}

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

func (this Backblaze) Touch(path string) error {
	p := this.path(path)

	// Step 1: get the URL we will proceed to the upload
	res, err := this.request(
		"POST",
		this.ApiUrl+"/b2api/v2/b2_get_upload_url",
		strings.NewReader(fmt.Sprintf(`{"bucketId": "%s"}`, p.BucketId)),
		nil,
	)
	if err != nil {
		return err
	}
	body, err := io.ReadAll(res.Body)
	res.Body.Close()
	if err != nil {
		return err
	}
	var resBody struct {
		UploadUrl string `json:"uploadUrl"`
		Token     string `json:"authorizationToken"`
	}
	if err := json.Unmarshal(body, &resBody); err != nil {
		return err
	}

	// Step 2: perform the upload of the empty file
	res, err = this.request(
		"POST",
		resBody.UploadUrl,
		nil,
		func(r *http.Request) {
			r.Header.Set("Authorization", resBody.Token)
			r.Header.Set("X-Bz-File-Name", url.QueryEscape(p.Prefix))
			r.Header.Set("Content-Type", "application/octet-stream")
			r.Header.Set("Content-Length", "0")
			r.Header.Set("X-Bz-Content-Sha1", "da39a3ee5e6b4b0d3255bfef95601890afd80709")
		},
	)
	if err != nil {
		return err
	}
	body, err = io.ReadAll(res.Body)
	res.Body.Close()
	if err != nil {
		return err
	}
	var resError BackblazeError
	if err := json.Unmarshal(body, &resError); err != nil {
		return err
	}
	if resError.Message != "" {
		return NewError(resError.Message, resError.Status)
	}
	return nil
}

func (this Backblaze) Save(path string, file io.Reader) error {
	p := this.path(path)

	// Step 1: get the URL we will proceed to the upload
	res, err := this.request(
		"POST",
		this.ApiUrl+"/b2api/v2/b2_get_upload_url",
		strings.NewReader(fmt.Sprintf(`{"bucketId": "%s"}`, p.BucketId)),
		nil,
	)
	if err != nil {
		return err
	}
	body, err := io.ReadAll(res.Body)
	res.Body.Close()
	if err != nil {
		return err
	}
	var resBody struct {
		UploadUrl string `json:"uploadUrl"`
		Token     string `json:"authorizationToken"`
	}
	if err := json.Unmarshal(body, &resBody); err != nil {
		return err
	}

	// Step 2: get details backblaze requires to perform the upload
	backblazeFileDetail := struct {
		path          string
		ContentLength int64
		Sha1          []byte
	}{}
	backblazeFileDetail.path = filepath.Join(
		GetAbsolutePath(TMP_PATH),
		"data_"+QuickString(20)+".dat",
	)
	f, err := os.OpenFile(backblazeFileDetail.path, os.O_CREATE|os.O_RDWR, os.ModePerm)
	if err != nil {
		return err
	}
	defer f.Close()
	defer os.Remove(backblazeFileDetail.path)
	io.Copy(f, file)
	if obj, ok := file.(io.Closer); ok {
		obj.Close()
	}
	s, err := f.Stat()
	if err != nil {
		return err
	}
	backblazeFileDetail.ContentLength = s.Size()
	f.Seek(0, io.SeekStart)
	h := sha1.New()
	if _, err := io.Copy(h, f); err != nil {
		return err
	}
	backblazeFileDetail.Sha1 = h.Sum(nil)

	// Step 3: perform the upload
	f.Seek(0, io.SeekStart)
	res, err = this.request(
		"POST",
		resBody.UploadUrl,
		f,
		func(r *http.Request) {
			r.ContentLength = backblazeFileDetail.ContentLength
			r.Header.Set("Authorization", resBody.Token)
			r.Header.Set("X-Bz-File-Name", url.QueryEscape(p.Prefix))
			r.Header.Set("Content-Type", "application/octet-stream")
			r.Header.Set("X-Bz-Content-Sha1", fmt.Sprintf("%x", backblazeFileDetail.Sha1))
		},
	)
	if err != nil {
		return err
	}
	body, err = io.ReadAll(res.Body)
	res.Body.Close()
	if err != nil {
		return err
	}
	var resError BackblazeError
	if err := json.Unmarshal(body, &resError); err != nil {
		return err
	}
	if resError.Message != "" {
		return NewError(resError.Message, resError.Status)
	}
	return nil
}

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

func (this Backblaze) request(method string, url string, body io.Reader, fn func(req *http.Request)) (*http.Response, error) {
	req, err := http.NewRequest(method, url, body)
	if err != nil {
		return nil, err
	}

	if this.Token == "" {
		req.Header.Set(
			"Authorization",
			fmt.Sprintf(
				"Basic %s",
				base64.StdEncoding.EncodeToString(
					[]byte(fmt.Sprintf("%s:%s", this.params["username"], this.params["password"])),
				),
			),
		)
	} else {
		req.Header.Set("Authorization", this.Token)
	}
	req.Header.Set("User-Agent", "Filestash "+APP_VERSION+"."+BUILD_DATE)
	req.Header.Set("Accept", "application/json")
	//req.Header.Set("X-Bz-Test-Mode", "force_cap_exceeded")
	if fn != nil {
		fn(req)
	}
	if req.Body != nil {
		defer req.Body.Close()
	}

	res, err := HTTPClient.Do(req)
	if err != nil {
		return res, err
	}

	if res.StatusCode == 401 {
		res.Body.Close()
		return res, ErrAuthenticationFailed
	} else if res.StatusCode == 403 {
		res.Body.Close()
		return res, ErrNotAllowed
	} else if res.StatusCode == 429 {
		res.Body.Close()
		retryAfter, err := strconv.Atoi(res.Header.Get("Retry-After"))
		if err == nil && retryAfter < 10 && retryAfter >= 0 {
			time.Sleep(time.Duration(retryAfter) * time.Second)
			return this.request(method, url, body, fn)
		}
		return res, ErrCongestion
	} else if res.StatusCode == 503 {
		res.Body.Close()
		retryAfter, err := strconv.Atoi(res.Header.Get("Retry-After"))
		if err == nil && retryAfter < 10 && retryAfter >= 0 {
			time.Sleep(time.Duration(retryAfter) * time.Second)
			return this.request(method, url, body, fn)
		}
		return res, ErrCongestion
	}
	return res, nil
}

type BackblazePath struct {
	BucketId string
	Prefix   string
}

func (this Backblaze) path(path string) BackblazePath {
	bp := strings.Split(path, "/")
	bucket := ""
	if len(bp) > 1 {
		bucket = bp[1]
	}
	prefix := ""
	if len(bp) > 2 {
		prefix = strings.Join(bp[2:], "/")
	}

	return BackblazePath{
		this.Buckets[bucket],
		prefix,
	}
}
