package rushlink

import (
	"encoding/hex"
	"hash/crc32"
	"io"
	"log"
	"net/http"
	"net/url"
	"os"
	"path"

	"github.com/google/uuid"
	"github.com/pkg/errors"
	bolt "go.etcd.io/bbolt"
)

// Use the Castagnoli checksum because of the acceleration on Intel CPUs
var checksumTable = crc32.MakeTable(crc32.Castagnoli)

// Where to store the uploaded files
var fileStoreDir = ""

// Custom HTTP filesystem handler
type fileUploadFileSystem struct {
	fs http.FileSystem
}

// Open opens file
func (fs fileUploadFileSystem) Open(path string) (http.File, error) {
	log.Println(path)
	file, err := fs.fs.Open(path)
	if err != nil {
		return nil, errors.Wrap(err, "opening file")
	}
	stat, err := file.Stat()
	if err != nil {
		return nil, errors.Wrap(err, "file.Stat()")
	}
	if stat.IsDir() {
		return nil, errors.New("directory index not allowed")
	}
	return file, nil
}

type fileUploadState int

type fileUpload struct {
	State       fileUploadState
	ID          uuid.UUID
	FileName    string
	ContentType string
	Checksum    uint32
}

const (
	dirMode  os.FileMode = 0750
	fileMode os.FileMode = 0640
)

const (
	fileUploadStateUndef   fileUploadState = 0
	fileUploadStatePresent                 = 1
	fileUploadStateDeleted                 = 2
)

func (t fileUploadState) String() string {
	switch t {
	case fileUploadStateUndef:
		return "unknown"
	case fileUploadStatePresent:
		return "present"
	case fileUploadStateDeleted:
		return "deleted"
	default:
		return "invalid"
	}
}

func OpenFileStore(path string) error {
	if path == "" {
		return errors.New("file-store not set")
	}

	// Try to create the file store directory if it does not yet exist
	if err := os.MkdirAll(path, dirMode); err != nil {
		return errors.Wrap(err, "creating file store directory")
	}

	fileStoreDir = path[:]
	return nil
}

func newFileUpload(tx *bolt.Tx, r io.Reader, fileName string, contentType string) (*fileUpload, error) {
	id, err := uuid.NewRandom()
	if err != nil {
		return nil, errors.Wrap(err, "generating UUID")
	}

	filePath := fileStorePath(id, fileName)
	if err := os.Mkdir(path.Dir(filePath), dirMode); err != nil {
		return nil, errors.Wrap(err, "creating file dir")
	}
	file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, fileMode)
	if err != nil {
		return nil, errors.Wrap(err, "opening file")
	}
	defer file.Close()

	hash := crc32.New(checksumTable)
	tee := io.TeeReader(r, hash)
	_, err = io.Copy(file, tee)
	if err != nil {
		return nil, errors.Wrap(err, "writing to file")
	}

	fu := &fileUpload{
		State:       fileUploadStatePresent,
		ID:          id,
		FileName:    fileName,
		ContentType: contentType,
		Checksum:    hash.Sum32(),
	}
	if err := fu.save(tx); err != nil {
		return nil, err
	}
	return fu, nil
}

func getFileUpload(tx *bolt.Tx, id uuid.UUID) (*fileUpload, error) {
	bucket := tx.Bucket([]byte(BUCKET_FILE_UPLOAD))
	if bucket == nil {
		return nil, errors.Errorf("bucket %v does not exist", BUCKET_FILE_UPLOAD)
	}
	storedBytes := bucket.Get(id[:])
	if storedBytes == nil {
		return nil, nil
	}
	fu := &fileUpload{}
	err := Unmarshal(storedBytes, fu)
	return fu, err
}

func (fu *fileUpload) save(tx *bolt.Tx) error {
	bucket := tx.Bucket([]byte(BUCKET_FILE_UPLOAD))
	if bucket == nil {
		return errors.Errorf("bucket %v does not exist", BUCKET_FILE_UPLOAD)
	}

	buf, err := Marshal(fu)
	if err != nil {
		return errors.Wrap(err, "encoding for database failed")
	}
	if err := bucket.Put(fu.ID[:], buf); err != nil {
		return errors.Wrap(err, "database transaction failed")
	}
	return nil
}

func (fu *fileUpload) delete(tx *bolt.Tx) error {
	// Remove the file in the backend
	filePath := fileStorePath(fu.ID, fu.FileName)
	if err := os.Remove(filePath); err != nil {
		return err
	}

	// Update the file in the server
	if err := (&fileUpload{
		ID:    fu.ID,
		State: fileUploadStateDeleted,
	}).save(tx); err != nil {
		return err
	}

	// Cleanup the parent directory
	wrap := "deletion succeeded, but removing the file directory has failed"
	return errors.Wrap(os.Remove(path.Dir(filePath)), wrap)
}

func (fu *fileUpload) url() *url.URL {
	rawurl := "/uploads/" + hex.EncodeToString(fu.ID[:]) + "/" + fu.FileName
	urlParse, err := url.Parse(rawurl)
	if err != nil {
		panic("could not construct /uploads/ url")
	}
	return urlParse
}

func fileStorePath(id uuid.UUID, fileName string) string {
	if fileStoreDir == "" {
		panic("fileStoreDir called while the file store path has not been set")
	}
	return path.Join(fileStoreDir, hex.EncodeToString(id[:]), fileName)
}