package mfsr

import (
	"bufio"
	"bytes"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
)

var DistPath = "https://raw.githubusercontent.com/bittorrent/btfs-distributions/master"

func init() {
	// Possible for the download path to be overwritten from command line
	if dist := os.Getenv("BTFS_DIST_PATH"); dist != "" {
		DistPath = dist
	}
}

const migrations = "fs-repo-migrations"

func migrationsBinName() string {
	switch runtime.GOOS {
	case "windows":
		return migrations + ".exe"
	default:
		return migrations
	}
}

func RunMigration(newv int) error {
	migrateBin := migrationsBinName()

	fmt.Println("  => Looking for suitable fs-repo-migrations binary.")

	// Check BTFS binary directory first
	ex, err := os.Executable()
	if err != nil {
		return err
	}
	exMigrateBin := filepath.Join(filepath.Dir(ex), migrateBin)
	// Check if the migrate bin is valid
	exMigrateBin, err = exec.LookPath(migrateBin)
	if err == nil {
		// check to make sure migrations binary supports our target version
		err = verifyMigrationSupportsVersion(exMigrateBin, newv)
	}
	// Re-check the migrate bin by looking up bins by $PATH
	if err != nil {
		migrateBin, err = exec.LookPath(migrateBin)
		if err == nil {
			// check to make sure migrations binary supports our target version
			err = verifyMigrationSupportsVersion(migrateBin, newv)
		}
	} else {
		migrateBin = exMigrateBin // use found bin
	}

	if err != nil {
		fmt.Println("  => None found, downloading.")

		loc, err := GetMigrations()
		if err != nil {
			fmt.Println("  => Failed to download fs-repo-migrations.")
			return err
		}

		err = verifyMigrationSupportsVersion(loc, newv)
		if err != nil {
			return fmt.Errorf("no fs-repo-migration binary found for version %d: %s", newv, err)
		}

		migrateBin = loc
	}

	cmd := exec.Command(migrateBin, "-to", fmt.Sprint(newv), "-y")
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	fmt.Printf("  => Running: %s -to %d -y\n", migrateBin, newv)

	err = cmd.Run()
	if err != nil {
		fmt.Printf("  => Failed: %s -to %d -y\n", migrateBin, newv)
		return fmt.Errorf("migration failed: %s", err)
	}

	fmt.Printf("  => Success: fs-repo has been migrated to version %d.\n", newv)

	return nil
}

func GetMigrations() (string, error) {
	latest, err := GetLatestVersion(DistPath, migrations)
	if err != nil {
		return "", fmt.Errorf("failed to find latest fs-repo-migrations: %s", err)
	}

	dir, err := ioutil.TempDir("", "go-ipfs-migrate")
	if err != nil {
		return "", fmt.Errorf("failed to create fs-repo-migrations tempdir: %s", err)
	}

	out := filepath.Join(dir, migrationsBinName())

	err = GetBinaryForVersion(migrations, migrations, DistPath, latest, out)
	if err != nil {
		return "", fmt.Errorf("failed to download latest fs-repo-migrations: %s", err)
	}

	err = os.Chmod(out, 0755)
	if err != nil {
		return "", err
	}

	return out, nil
}

func verifyMigrationSupportsVersion(fsrbin string, vn int) error {
	sn, err := migrationsVersion(fsrbin)
	if err != nil {
		return err
	}

	if sn >= vn {
		return nil
	}

	return fmt.Errorf("migrations binary doesn't support version %d: %s", vn, fsrbin)
}

func migrationsVersion(bin string) (int, error) {
	out, err := exec.Command(bin, "-v").CombinedOutput()
	if err != nil {
		return 0, fmt.Errorf("failed to check migrations version: %s", err)
	}

	vs := strings.Trim(string(out), " \n\t")
	vn, err := strconv.Atoi(vs)
	if err != nil {
		return 0, fmt.Errorf("migrations binary version check did not return a number: %s", err)
	}

	return vn, nil
}

func GetVersions(ipfspath, dist string) ([]string, error) {
	rc, err := httpFetch(ipfspath + "/" + dist + "/versions")
	if err != nil {
		return nil, err
	}
	defer rc.Close()

	var out []string
	scan := bufio.NewScanner(rc)
	for scan.Scan() {
		out = append(out, scan.Text())
	}

	return out, nil
}

func GetLatestVersion(ipfspath, dist string) (string, error) {
	vs, err := GetVersions(ipfspath, dist)
	if err != nil {
		return "", err
	}
	var latest string
	for i := len(vs) - 1; i >= 0; i-- {
		if !strings.Contains(vs[i], "-dev") {
			latest = vs[i]
			break
		}
	}
	if latest == "" {
		return "", fmt.Errorf("couldn't find a non dev version in the list")
	}
	return vs[len(vs)-1], nil
}

func httpGet(url string) (*http.Response, error) {
	req, err := http.NewRequest(http.MethodGet, url, nil)
	if err != nil {
		return nil, fmt.Errorf("http.NewRequest error: %s", err)
	}

	req.Header.Set("User-Agent", "go-ipfs")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("http.DefaultClient.Do error: %s", err)
	}

	return resp, nil
}

func httpFetch(url string) (io.ReadCloser, error) {
	resp, err := httpGet(url)
	if err != nil {
		return nil, err
	}

	if resp.StatusCode >= 400 {
		mes, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			return nil, fmt.Errorf("error reading error body: %s", err)
		}

		return nil, fmt.Errorf("GET %s error: %s: %s", url, resp.Status, string(mes))
	}

	return resp.Body, nil
}

func GetBinaryForVersion(distname, binnom, root, vers, out string) error {
	dir, err := ioutil.TempDir("", "go-ipfs-auto-migrate")
	if err != nil {
		return err
	}

	var archive string
	switch runtime.GOOS {
	case "windows":
		archive = "zip"
		binnom += ".exe"
	default:
		archive = "tar.gz"
	}
	osv, err := osWithVariant()
	if err != nil {
		return err
	}
	finame := fmt.Sprintf("%s_%s_%s-%s.%s", distname, vers, osv, runtime.GOARCH, archive)
	distpath := fmt.Sprintf("%s/%s/%s/%s", root, distname, vers, finame)

	data, err := httpFetch(distpath)
	if err != nil {
		return err
	}

	arcpath := filepath.Join(dir, finame)
	fi, err := os.Create(arcpath)
	if err != nil {
		return err
	}

	_, err = io.Copy(fi, data)
	if err != nil {
		return err
	}
	fi.Close()

	return unpackArchive(distname, binnom, arcpath, out, archive)
}

// osWithVariant returns the OS name with optional variant.
// Currently returns either runtime.GOOS, or "linux-musl".
func osWithVariant() (string, error) {
	if runtime.GOOS != "linux" {
		return runtime.GOOS, nil
	}

	// ldd outputs the system's kind of libc.
	// - on standard ubuntu: ldd (Ubuntu GLIBC 2.23-0ubuntu5) 2.23
	// - on alpine: musl libc (x86_64)
	//
	// we use the combined stdout+stderr,
	// because ldd --version prints differently on different OSes.
	// - on standard ubuntu: stdout
	// - on alpine: stderr (it probably doesn't know the --version flag)
	//
	// we suppress non-zero exit codes (see last point about alpine).
	out, err := exec.Command("sh", "-c", "ldd --version || true").CombinedOutput()
	if err != nil {
		return "", err
	}

	// now just see if we can find "musl" somewhere in the output
	scan := bufio.NewScanner(bytes.NewBuffer(out))
	for scan.Scan() {
		if strings.Contains(scan.Text(), "musl") {
			return "linux-musl", nil
		}
	}

	return "linux", nil
}
