package b2

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"net/url"
	"strconv"
	"strings"
)

// APIurl base address for the B2 API
const APIurl = "https://api.backblaze.com"

// APIsuffix the version of the API
const APIsuffix = "/b2api/v1"

// GoodStatus status code for a successful API call
const GoodStatus = 200

// HeaderInfoPrefix the prefix for file header info
const HeaderInfoPrefix = "X-Bz-Info-"

// ErrGeneric generic error from API
var ErrGeneric = errors.New("Received invalid response from B2 API")

// B2 communicates to B2 API and holds information for the connection
type B2 struct {
	AccountID   string `json:"accountId"`
	APIUrl      string `json:"apiUrl"`
	AuthToken   string `json:"authorizationToken"`
	DownloadURL string `json:"downloadUrl"`
	AppKey      string `json:"-"`
}

// Err B2 error information
type Err struct {
	Code    string `json:"code"`
	Message string `json:"message"`
	Status  int    `json:"status"`
}

func (b *Err) Error() string {
	return fmt.Sprintf("code: '%s' status: '%d' message: '%s'", b.Code, b.Status, b.Message)
}

// readResp take an http response from the B2 API and unmarshal it to the appropriate type
func readResp(resp *http.Response, output interface{}) error {
	data, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return err
	}

	if resp.StatusCode == GoodStatus {
		err = json.Unmarshal(data, output)
		if err != nil {
			return err
		}

		return nil
	}

	// errors are generated anytime there is not a status code of GoodStatus
	errb2 := &Err{}
	err = json.Unmarshal(data, errb2)
	if err != nil {
		return err
	}

	return errb2
}

func (b *B2) readHeaderFileInfo(header http.Header) (*FileInfo, error) {
	var err error
	info := &FileInfo{conn: b}
	info.AccountID = b.AccountID
	info.Type = header.Get("Content-Type")
	info.ID = header.Get("X-Bz-File-Id")
	info.Length, err = strconv.ParseInt(header.Get("Content-Length"), 10, 64)
	if err != nil {
		return nil, err
	}
	info.Name, err = url.QueryUnescape(header.Get("X-Bz-File-Name"))
	if err != nil {
		return nil, err
	}
	info.Sha1 = header.Get("X-Bz-Content-Sha1")

	for headerName, val := range header {
		if !strings.HasPrefix(headerName, HeaderInfoPrefix) {
			continue
		}

		// B2 does not support multiple values per header
		info.Info[headerName[len(HeaderInfoPrefix):]] = val[0]
	}

	return info, nil
}

// NewB2 create a new B2 API handler
func NewB2(accountID string, applicationKey string) (*B2, error) {
	req, err := http.NewRequest("GET", APIurl+APIsuffix+"/b2_authorize_account", nil)
	if err != nil {
		return nil, err
	}

	req.SetBasicAuth(accountID, applicationKey)
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}

	b2 := &B2{}

	err = readResp(resp, b2)
	if err != nil {
		return nil, err
	}

	return b2, nil
}

// CreateBucket creates a new bucket
func (b *B2) CreateBucket(bucketName string, bucketType string) (*Bucket, error) {
	req, err := http.NewRequest("GET", b.APIUrl+APIsuffix+"/b2_create_bucket", nil)
	if err != nil {
		return nil, err
	}

	req.Header.Add("Authorization", b.AuthToken)
	q := req.URL.Query()
	q.Add("accountId", b.AccountID)
	q.Add("bucketName", bucketName)
	q.Add("bucketType", bucketType)
	req.URL.RawQuery = q.Encode()

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}

	bucket := &Bucket{conn: b}
	err = readResp(resp, bucket)
	if err != nil {
		return nil, err
	}

	return bucket, nil
}

// DeleteBucket deletes the bucket specified
func (b *B2) DeleteBucket(bucketID string) (*Bucket, error) {
	data, err := json.Marshal(map[string]string{
		"accountId": b.AccountID,
		"bucketId":  bucketID,
	})
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequest("POST", b.APIUrl+APIsuffix+"/b2_delete_bucket", bytes.NewReader(data))
	if err != nil {
		return nil, err
	}

	req.Header.Add("Authorization", b.AuthToken)

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}

	bucket := &Bucket{conn: b}

	err = readResp(resp, bucket)
	if err != nil {
		return nil, err
	}

	return bucket, nil
}

// GetUploadURL gets an URL to use for uploading files
func (b *B2) GetUploadURL(bucketID string) (*Upload, error) {
	data, err := json.Marshal(map[string]string{
		"bucketId": bucketID,
	})
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequest("POST", b.APIUrl+APIsuffix+"/b2_get_upload_url", bytes.NewReader(data))
	if err != nil {
		return nil, err
	}

	req.Header.Add("Authorization", b.AuthToken)

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}

	upload := &Upload{}
	err = readResp(resp, upload)
	if err != nil {
		return nil, err
	}

	return upload, nil
}

// DownloadFileByID Downloads one file from B2
func (b *B2) DownloadFileByID(fileID string, output io.Writer) (*FileInfo, error) {
	req, err := http.NewRequest("GET", b.DownloadURL+APIsuffix+"/b2_download_file_by_id", nil)
	if err != nil {
		return nil, err
	}

	req.Header.Add("Authorization", b.AuthToken)

	q := req.URL.Query()
	q.Add("fileId", fileID)
	req.URL.RawQuery = q.Encode()

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}

	if resp.StatusCode != GoodStatus {
		return nil, readResp(resp, nil)
	}

	defer resp.Body.Close()

	_, err = io.Copy(output, resp.Body)
	if err != nil {
		return nil, err
	}

	return b.readHeaderFileInfo(resp.Header)
}

// DownloadFileByName downloads one file by providing the name of the bucket and the name of the file
func (b *B2) DownloadFileByName(bucketName string, fileName string, output io.Writer) (*FileInfo, error) {
	urlFileName, err := url.Parse(fileName)
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequest("GET", b.DownloadURL+"/file/"+bucketName+"/"+urlFileName.String(), nil)
	if err != nil {
		return nil, err
	}

	req.Header.Add("Authorization", b.AuthToken)

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}

	if resp.StatusCode != GoodStatus {
		return nil, readResp(resp, nil)
	}

	defer resp.Body.Close()

	_, err = io.Copy(output, resp.Body)
	if err != nil {
		return nil, err
	}

	return b.readHeaderFileInfo(resp.Header)
}

// UpdateBucket update an existing bucket
func (b *B2) UpdateBucket(bucketID string, bucketType string) (*Bucket, error) {
	data, err := json.Marshal(map[string]string{
		"accountId":  b.AccountID,
		"bucketId":   bucketID,
		"bucketType": bucketType,
	})
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequest("POST", b.APIUrl+APIsuffix+"/b2_update_bucket", bytes.NewReader(data))
	if err != nil {
		return nil, err
	}

	req.Header.Add("Authorization", b.AuthToken)

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}

	bucket := &Bucket{conn: b}
	err = readResp(resp, bucket)
	if err != nil {
		return nil, err
	}

	return bucket, nil
}

// DeleteFileVersion deletes one version of a file from B2
func (b *B2) DeleteFileVersion(fileName string, fileID string) (*FileInfo, error) {
	data, err := json.Marshal(map[string]string{
		"fileName": fileName,
		"fileId":   fileID,
	})
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequest("POST", b.APIUrl+APIsuffix+"/b2_delete_file_version", bytes.NewReader(data))
	if err != nil {
		return nil, err
	}

	req.Header.Add("Authorization", b.AuthToken)

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}

	fileInfo := &FileInfo{conn: b}
	err = readResp(resp, fileInfo)
	if err != nil {
		return nil, err
	}

	return fileInfo, nil
}

// ListBuckets lists buckets associated with an account, in alphabetical order by bucket ID
func (b *B2) ListBuckets() ([]Bucket, error) {
	data, err := json.Marshal(map[string]string{
		"accountId": b.AccountID,
	})
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequest("POST", b.APIUrl+APIsuffix+"/b2_list_buckets", bytes.NewReader(data))
	if err != nil {
		return nil, err
	}

	req.Header.Add("Authorization", b.AuthToken)

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}

	buckets := &struct {
		Buckets []Bucket `json:"buckets"`
	}{}
	err = readResp(resp, buckets)
	if err != nil {
		return nil, err
	}

	for i := range buckets.Buckets {
		buckets.Buckets[i].conn = b
	}

	return buckets.Buckets, nil
}

// ListFileNames Lists the names of all files in a bucket, starting at a given name
func (b *B2) ListFileNames(bucketID string, startFileName string, maxFileCount int) ([]FileName, string, error) {
	data, err := json.Marshal(struct {
		BucketID      string `json:"bucketId"`
		StartFileName string `json:"startFileName,omitempty"`
		MaxFileCount  int    `json:"maxFileCount,omitempty"`
	}{
		BucketID:      bucketID,
		StartFileName: startFileName,
		MaxFileCount:  maxFileCount,
	})
	if err != nil {
		return nil, "", err
	}

	req, err := http.NewRequest("POST", b.APIUrl+APIsuffix+"/b2_list_file_names", bytes.NewReader(data))
	if err != nil {
		return nil, "", err
	}

	req.Header.Add("Authorization", b.AuthToken)
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, "", err
	}

	list := &struct {
		Files        []FileName `json:"files"`
		NextFileName string     `json:"nextFileName"`
	}{}
	err = readResp(resp, list)
	if err != nil {
		return nil, "", err
	}

	for i := range list.Files {
		list.Files[i].conn = b
	}

	return list.Files, list.NextFileName, nil
}

// ListFileVersions lists all of the versions of all of the files contained in one bucket, in alphabetical order by file name, and by reverse of date/time uploaded for versions of files with the same name
func (b *B2) ListFileVersions(bucketID string, startFileName string, startFileID string, maxFileCount int) ([]FileName, string, string, error) {
	data, err := json.Marshal(struct {
		BucketID      string `json:"bucketId"`
		StartFileName string `json:"startFileName,omitempty"`
		StartFileID   string `json:"startFileId,omitempty"`
		MaxFileCount  int    `json:"maxFileCount,omitempty"`
	}{
		BucketID:      bucketID,
		StartFileName: startFileName,
		StartFileID:   startFileID,
		MaxFileCount:  maxFileCount,
	})
	if err != nil {
		return nil, "", "", err
	}

	req, err := http.NewRequest("POST", b.APIUrl+APIsuffix+"/b2_list_file_versions", bytes.NewReader(data))
	if err != nil {
		return nil, "", "", err
	}

	req.Header.Add("Authorization", b.AuthToken)
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, "", "", err
	}

	list := &struct {
		Files        []FileName `json:"files"`
		NextFileID   string     `json:"nextFileId"`
		NextFileName string     `json:"nextFileName"`
	}{}
	err = readResp(resp, list)
	if err != nil {
		return nil, "", "", err
	}

	for i := range list.Files {
		list.Files[i].conn = b
	}

	return list.Files, list.NextFileID, list.NextFileName, nil
}

// GetFileInfo Gets information about one file stored in B2
func (b *B2) GetFileInfo(fileID string) (*FileInfo, error) {
	data, err := json.Marshal(map[string]string{
		"fileId": fileID,
	})
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequest("POST", b.APIUrl+APIsuffix+"/b2_get_file_info", bytes.NewReader(data))
	if err != nil {
		return nil, err
	}

	req.Header.Add("Authorization", b.AuthToken)
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}

	info := &FileInfo{conn: b}
	err = readResp(resp, info)
	if err != nil {
		return nil, err
	}

	return info, nil
}

// HideFile hides a file so that downloading by name will not find the file, but previous versions of the file are still stored. See File Versions about what it means to hide a file
func (b *B2) HideFile(bucketID string, fileName string) (*FileName, error) {
	data, err := json.Marshal(map[string]string{
		"bucketId": bucketID,
		"fileName": fileName,
	})
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequest("POST", b.APIUrl+APIsuffix+"/b2_hide_file", bytes.NewReader(data))
	if err != nil {
		return nil, err
	}

	req.Header.Add("Authorization", b.AuthToken)
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}

	info := &FileName{conn: b}
	err = readResp(resp, info)
	if err != nil {
		return nil, err
	}

	return info, nil
}