package drivers

import (
	"bytes"
	"crypto/md5"
	"crypto/sha1"
	"crypto/sha256"
	"crypto/sha512"
	"encoding/hex"
	"fmt"
	"hash"
	"io"
	"io/ioutil"
	"net/http"
	"net/url"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"strings"

	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
)

type BaseDriver struct {
	Builtin      bool
	URL          string
	DriverHash   string
	DriverName   string
	BinaryPrefix string
}

func (d *BaseDriver) Name() string {
	return d.DriverName
}

func (d *BaseDriver) Hash() string {
	return d.DriverHash
}

func (d *BaseDriver) Checksum() string {
	return d.DriverName
}

func (d *BaseDriver) FriendlyName() string {
	return strings.TrimPrefix(d.DriverName, d.BinaryPrefix)
}

func (d *BaseDriver) Remove() error {
	cacheFilePrefix := d.cacheFile()
	content, err := ioutil.ReadFile(cacheFilePrefix)
	if os.IsNotExist(err) {
		return nil
	}

	if err != nil {
		return err
	}

	dest := path.Join(binDir(), string(content))
	os.Remove(dest)
	os.Remove(cacheFilePrefix + "-" + string(content))
	os.Remove(cacheFilePrefix)

	return nil
}

func (d *BaseDriver) Stage(forceUpdate bool) error {
	if err := d.getError(); err != nil {
		return err
	}

	return d.setError(d.stage(forceUpdate))
}

func (d *BaseDriver) setError(err error) error {
	errFile := d.cacheFile() + ".error"

	if err != nil {
		os.MkdirAll(path.Dir(errFile), 0700)
		ioutil.WriteFile(errFile, []byte(err.Error()), 0600)
	}
	return err
}

func (d *BaseDriver) getError() error {
	errFile := d.cacheFile() + ".error"

	if content, err := ioutil.ReadFile(errFile); err == nil {
		logrus.Errorf("Returning previous error: %s", content)
		d.ClearError()
		return errors.New(string(content))
	}

	return nil
}

func (d *BaseDriver) ClearError() {
	errFile := d.cacheFile() + ".error"
	os.Remove(errFile)
}

func (d *BaseDriver) stage(forceUpdate bool) error {
	if d.Builtin {
		return nil
	}

	cacheFilePrefix := d.cacheFile()

	driverName, err := isInstalled(cacheFilePrefix)
	if !forceUpdate && err != nil || driverName != "" {
		d.DriverName = driverName
		return err
	}

	tempFile, err := ioutil.TempFile("", "machine-driver")
	if err != nil {
		return err
	}
	defer os.Remove(tempFile.Name())
	defer tempFile.Close()

	hasher, err := getHasher(d.DriverHash)
	if err != nil {
		return err
	}

	downloadDest := io.Writer(tempFile)
	if hasher != nil {
		downloadDest = io.MultiWriter(tempFile, hasher)
	}

	if err := d.download(downloadDest); err != nil {
		return err
	}

	if got, ok := compare(hasher, d.DriverHash); !ok {
		return fmt.Errorf("hash does not match, got %s, expected %s", got, d.DriverHash)
	}

	if err := tempFile.Close(); err != nil {
		return err
	}

	driverName, err = d.copyBinary(cacheFilePrefix, tempFile.Name())
	if err != nil {
		return err
	}

	d.DriverName = driverName
	return nil
}

// Exists will return true if the executable binary for the driver can be found
// and the cache file exists (in case of upgrades the binary will match but
// the cache will not yet exist)
func (d *BaseDriver) Exists() bool {
	if d.DriverName == "" {
		return false
	}
	if d.Builtin {
		return true
	}
	_, err := os.Stat(d.binName())
	if err == nil {
		// The executable is there but do it come from the right version?
		_, err = os.Stat(d.srcBinName())
	}
	return err == nil
}

func isElf(input string) bool {
	f, err := os.Open(input)
	if err != nil {
		return false
	}
	defer f.Close()

	elf := make([]byte, 4)
	if _, err := f.Read(elf); err != nil {
		return false
	}
	//support unix binary and mac-os binary mach-o
	return bytes.Compare(elf, []byte{0x7f, 0x45, 0x4c, 0x46}) == 0 || bytes.Compare(elf, []byte{0xcf, 0xfa, 0xed, 0xfe}) == 0
}

func (d *BaseDriver) copyBinary(cacheFile, input string) (string, error) {
	temp, err := ioutil.TempDir("", "machine-driver-extract")
	if err != nil {
		return "", err
	}
	defer os.RemoveAll(temp)

	file := ""
	driverName := ""

	if isElf(input) {
		file = input
		u, err := url.Parse(d.URL)
		if err != nil {
			return "", err
		}

		if !strings.HasPrefix(path.Base(u.Path), d.BinaryPrefix) {
			return "", fmt.Errorf("invalid URL %s, path should be of the format %s*", d.URL, d.BinaryPrefix)
		}

		s := strings.TrimPrefix(path.Base(u.Path), d.BinaryPrefix)
		name := strings.FieldsFunc(s, func(r rune) bool {
			return r == '-' || r == '_' || r == '.'
		})[0]

		if name == "" {
			return "", fmt.Errorf("invalid URL %s, NAME is empty, path should be of the format %sNAME", d.URL, d.BinaryPrefix)
		}
		driverName = d.BinaryPrefix + name
	} else {
		if err := exec.Command("tar", "xvf", input, "-C", temp).Run(); err != nil {
			if err := exec.Command("unzip", "-o", input, "-d", temp).Run(); err != nil {
				return "", fmt.Errorf("failed to extract")
			}
		}
	}

	filepath.Walk(temp, filepath.WalkFunc(func(p string, info os.FileInfo, err error) error {
		if info.IsDir() {
			return nil
		}

		if strings.HasPrefix(path.Base(p), d.BinaryPrefix) {
			file = p
		}

		return nil
	}))

	if file == "" {
		return "", fmt.Errorf("failed to find driver in archive. There must be a file of form %s*", d.BinaryPrefix)
	}

	if driverName == "" {
		driverName = path.Base(file)
	}
	f, err := os.Open(file)
	if err != nil {
		return "", err
	}
	defer f.Close()

	if err := os.MkdirAll(path.Dir(cacheFile), 0755); err != nil {
		return "", err
	}

	driverName = strings.ToLower(driverName)
	dest, err := os.Create(cacheFile + "-" + driverName)
	if err != nil {
		return "", err
	}
	defer dest.Close()

	if _, err := io.Copy(dest, f); err != nil {
		return "", err
	}

	logrus.Infof("Found driver %s", driverName)
	return driverName, ioutil.WriteFile(cacheFile, []byte(driverName), 0644)
}

// binName is the full path to the binary executable. This does not take in
// account the version of the binary
func (d *BaseDriver) binName() string {
	return path.Join(binDir(), d.DriverName)
}

// srcBinName is the full path of the cached/hashed binary executable. This takes
// in account the version of the binary
func (d *BaseDriver) srcBinName() string {
	return d.cacheFile() + "-" + d.DriverName
}

func binDir() string {
	if dl := os.Getenv("CATTLE_DEV_MODE"); dl != "" {
		return "./management-state/bin"
	}

	return "/opt/drivers/management-state/bin"
}

func compare(hash hash.Hash, value string) (string, bool) {
	if hash == nil {
		return "", true
	}

	got := hex.EncodeToString(hash.Sum([]byte{}))
	expected := strings.TrimSpace(strings.ToLower(value))

	return got, got == expected
}

func getHasher(hash string) (hash.Hash, error) {
	switch len(hash) {
	case 0:
		return nil, nil
	case 32:
		return md5.New(), nil
	case 40:
		return sha1.New(), nil
	case 64:
		return sha256.New(), nil
	case 128:
		return sha512.New(), nil
	}

	return nil, fmt.Errorf("invalid hash format: %s", hash)
}

func (d *BaseDriver) download(dest io.Writer) error {
	logrus.Infof("Download %s", d.URL)
	resp, err := http.Get(d.URL)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	_, err = io.Copy(dest, resp.Body)
	return err
}

func (d *BaseDriver) cacheFile() string {
	key := sha256Bytes([]byte(d.URL + d.DriverHash))

	base := os.Getenv("CATTLE_HOME")
	if base == "" {
		base = "./management-state"
	}

	return path.Join(base, "machine-drivers", key)
}

func isInstalled(file string) (string, error) {
	content, err := ioutil.ReadFile(file)
	if os.IsNotExist(err) {
		return "", nil
	}
	return strings.ToLower(strings.TrimSpace(string(content))), err
}

func sha256Bytes(content []byte) string {
	hash := sha256.New()
	io.Copy(hash, bytes.NewBuffer(content))
	return hex.EncodeToString(hash.Sum([]byte{}))
}
