package sync

import (
	"bufio"
	"bytes"
	"encoding/base32"
	"fmt"
	"io"
	"net/url"
	"os"
	"path"
	"strings"

	"srs.epita.fr/fic-server/libfic"

	"golang.org/x/crypto/blake2b"
)

// Importer are abstract methods required to import challenges.
type Importer interface {
	// Kind returns information about the Importer, for human interrest.
	Kind() string
	// Id returns information about the current state (commit id, ...).
	Id() *string
	// init performs the importer initialization.
	Init() error
	// sync tries to pull the latest modification of the underlying storage.
	Sync() error
	// exists checks if the given location exists from the Importer point of view.
	exists(filename string) bool
	// toURL gets the full path/URL to the given file, the Importer will look internaly (used for debuging purpose).
	toURL(filename string) string
	// importFile imports the file at the given URI, inside the global FILES/ directory.
	// Then calls back the next function, with the downloaded location and the original URI.
	// Callback return is forwarded.
	importFile(URI string, next func(string, string) (interface{}, error)) (interface{}, error)
	// getFile write to the given buffer, the file at the given location.
	getFile(filename string, writer *bufio.Writer) error
	// listDir returns a list of the files and subdirectories contained inside the directory at the given location.
	listDir(filename string) ([]string, error)
	// stat returns many information about the given file: such as last modification date, size, ...
	stat(filename string) (os.FileInfo, error)
}

// DirectAccessImporter abstracts importer that support direct file access through a local path
type DirectAccessImporter interface {
	GetLocalPath(p ...string) string
}

// ForgeLinkedImporter abstracts importer that are linked to a forge
type ForgeLinkedImporter interface {
	GetThemeLink(th *fic.Theme) (*url.URL, error)
	GetExerciceLink(e *fic.Exercice) (*url.URL, error)
}

// WritableImporter abstracts importer that we can also write on
type WritableImporter interface {
	// writeFile write the given buffer to the file at the given location.
	writeFile(filename string, reader io.Reader) error
}

// GlobalImporter stores the main importer instance to use for global imports.
var GlobalImporter Importer

// getFileSize returns the size.
func getFileSize(i Importer, URI string) (size int64, err error) {
	if i.exists(URI) {
		if fi, err := i.stat(URI); err != nil {
			return 0, err
		} else {
			return fi.Size(), nil
		}
	}

	dirname := path.Dir(URI)
	if i.exists(dirname) {
		filename := path.Base(URI)
		if files, err := i.listDir(dirname); err != nil {
			return size, err
		} else {
			for _, fname := range []string{filename, filename + "."} {
				found := false
				for _, file := range files {
					if matched, _ := path.Match(fname+"[0-9][0-9]", file); matched {
						found = true
						if fi, err := i.stat(path.Join(dirname, file)); err != nil {
							return size, err
						} else {
							size += fi.Size()
						}
					}
				}

				if found {
					return size, nil
				}
			}
		}
	}

	return size, fmt.Errorf("%q: no such file or directory", URI)
}

// GetFile helps to manage huge file transfert by concatenating splitted (with split(1)) files.
func GetFile(i Importer, URI string, writer *bufio.Writer) error {
	// Import file if it exists
	if i.exists(URI) {
		return i.getFile(URI, writer)
	}

	// Try to find file parts
	dirname := path.Dir(URI)
	if i.exists(dirname) {
		filename := path.Base(URI)
		if files, err := i.listDir(dirname); err != nil {
			return err
		} else {
			for _, fname := range []string{filename, filename + "."} {
				found := false
				for _, file := range files {
					if matched, _ := path.Match(fname+"[0-9][0-9]", file); matched {
						found = true
						if err := i.getFile(path.Join(dirname, file), writer); err != nil {
							return err
						}
					}
				}

				if found {
					return nil
				}
			}
		}
	}

	return fmt.Errorf("%q: no such file or directory", URI)
}

// GetFileContent retrieves the content of the given text file.
func GetFileContent(i Importer, URI string) (string, error) {
	cnt := bytes.Buffer{}

	if err := GetFile(i, URI, bufio.NewWriter(io.Writer(&cnt))); err != nil {
		return "", err
	} else {
		// Ensure we read UTF-8 content.
		buf := make([]rune, 0)
		for b, _, err := cnt.ReadRune(); err == nil; b, _, err = cnt.ReadRune() {
			buf = append(buf, b)
		}

		if len(buf) == 0 {
			return "", fmt.Errorf("File is empty")
		}

		return strings.TrimSpace(string(buf)), nil
	}
}

// getDestinationFilePath generates the destination path, from the URI.
// This function permits to obfusce to player the original URI.
// Theoricaly, changing the import method doesn't change destination URI.
func getDestinationFilePath(URI string) string {
	hash := blake2b.Sum512([]byte(URI))
	return path.Join(fic.FilesDir, strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(hash[:])), path.Base(URI))
}

func importFile(i Importer, URI string, dest string) error {
	if err := os.MkdirAll(path.Dir(dest), 0755); err != nil {
		return err
	}

	// Write file
	if fdto, err := os.Create(dest); err != nil {
		return err
	} else {
		defer fdto.Close()
		writer := bufio.NewWriter(fdto)
		if err := GetFile(i, URI, writer); err != nil {
			os.Remove(dest)
			return err
		}
	}

	return nil
}

// ImportFile imports the file at the given URI, using helpers of the given Importer.
// After import, next is called with relative path where the file has been saved and the original URI.
func ImportFile(i Importer, URI string, next func(string, string) (interface{}, error)) (interface{}, error) {
	dest := getDestinationFilePath(URI)

	// If the present file is still valide, don't erase it
	if _, err := os.Stat(dest); !os.IsNotExist(err) {
		if r, err := next(dest, URI); err == nil {
			return r, err
		}
	}

	if err := importFile(i, URI, dest); err != nil {
		return nil, err
	}

	return next(dest, URI)
}

// WriteFileContent save the given content to the given text file.
func WriteFileContent(i Importer, URI string, content []byte) error {
	if wi, ok := i.(WritableImporter); ok {
		return wi.writeFile(URI, bytes.NewReader(content))
	} else {
		return fmt.Errorf("%t is not capable of writing", i)
	}
}
