package main

import (
	"archive/zip"
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"net/http"
	"os"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
	"time"

	"github.com/hrfee/jfa-go/common"
)

const (
	baseURL   = "https://builds.hrfee.pw"
	namespace = "hrfee"
	repo      = "jfa-go"
)

var buildTime time.Time = func() time.Time {
	i, _ := strconv.ParseInt(buildTimeUnix, 10, 64)
	return time.Unix(i, 0)
}()

type GHRelease struct {
	HTMLURL     string    `json:"html_url"`
	ID          int       `json:"id"`
	TagName     string    `json:"tag_name"`
	Name        string    `json:"name"`
	CreatedAt   time.Time `json:"created_at"`
	PublishedAt time.Time `json:"published_at"`
	Assets      []GHAsset `json:"assets"`
	Body        string    `json:"body"`
}

type GHAsset struct {
	Name               string    `json:"name"`
	State              string    `json:"state"`
	UpdatedAt          time.Time `json:"updated_at"`
	BrowserDownloadURL string    `json:"browser_download_url"`
}

type UnixTime struct {
	time.Time
}

func (t *UnixTime) UnmarshalJSON(b []byte) (err error) {
	unix, err := strconv.ParseInt(strings.TrimPrefix(strings.TrimSuffix(string(b), "\""), "\""), 10, 64)
	if err != nil {
		return
	}
	t.Time = time.Unix(unix, 0)
	return
}

func (t UnixTime) MarshalJSON() ([]byte, error) {
	if t.Time == (time.Time{}) {
		return []byte("\"\""), nil
	}
	return []byte("\"" + strconv.FormatInt(t.Time.Unix(), 10) + "\""), nil
}

var updater string

type BuildType int

const (
	off      BuildType = iota
	internal           // Internal assets through go:embed, no data/.
	external           // External assets in data/, accesses through app.localFS.
	docker             // Only notify of new updates, no self-updating.
)

type ApplyUpdate func() error

type Update struct {
	Version      string      `json:"version"` // vX.X.X or git
	Commit       string      `json:"commit"`
	ReleaseDate  int64       `json:"date"`          // unix time
	Description  string      `json:"description"`   // Commit Name/Release title.
	Changelog    string      `json:"changelog"`     // Changelog, if applicable
	Link         string      `json:"link"`          // Link to commit/release page,
	DownloadLink string      `json:"download_link"` // Optional link to download page.
	CanUpdate    bool        `json:"can_update"`    // Whether or not update can be done automatically.
	update       ApplyUpdate `json:"-"`             // Function to apply update if possible.
}

type Tag struct {
	Ready       bool     `json:"ready"`             // Whether or not build on this tag has completed.
	Version     string   `json:"version,omitempty"` // Version/Commit
	ReleaseDate UnixTime `json:"date"`
}

var goos = map[string]string{
	"darwin":  "macOS",
	"linux":   "Linux",
	"windows": "Windows",
}

var goarch = map[string]string{
	"amd64": "x86_64",
	"arm64": "arm64",
	"arm":   "armv6",
}

//	func newDockerBuild() Update {
//		var tag string
//		if version == "git" {
//			tag = "docker-unstable"
//		} else {
//			tag = "docker-latest"
//		}
//	}
type Updater struct {
	version, commit, tag, url, namespace, name string
	stable                                     bool
	buildType                                  BuildType
	httpClient                                 *http.Client
	timeoutHandler                             common.TimeoutHandler
	binary                                     string
}

func newUpdater(buildroneURL, namespace, repo, version, commit, buildType string) *Updater {
	// fmt.Printf(`Updater intializing with "%s", "%s", "%s", "%s", "%s", "%s"\n`, buildroneURL, namespace, repo, version, commit, buildType)
	bType := off
	tag := ""
	switch buildType {
	case "binary":
		if binaryType == "internal" {
			bType = internal
			tag = "internal"
		} else {
			bType = external
			tag = "external"
		}
	case "docker":
		bType = docker
		if version == "git" {
			tag = "docker-unstable"
		} else {
			tag = "docker-latest"
		}
	default:
		bType = off
	}
	if commit == "unknown" {
		bType = off
	}
	if version == "git" && bType != docker {
		tag += "-git"
	}
	binary := "jfa-go"
	if runtime.GOOS == "windows" {
		binary += ".exe"
	}
	return &Updater{
		httpClient:     &http.Client{Timeout: 10 * time.Second},
		timeoutHandler: common.NewTimeoutHandler("updater", buildroneURL, true),
		version:        version,
		commit:         commit,
		buildType:      bType,
		tag:            tag,
		url:            buildroneURL,
		namespace:      namespace,
		name:           repo,
		binary:         binary,
	}
}

type BuildDTO struct {
	ID      int64     // `json:"id"`
	Name    string    // `json:"name"`
	Date    time.Time // `json:"date"`
	Link    string    // `json:"link"`
	Message string
	Branch  string // `json:"branch"`
	Tags    map[string]Tag
}

// SetTransport sets the http.Transport to use for requests. Can be used to set a proxy.
func (ud *Updater) SetTransport(t *http.Transport) {
	ud.httpClient.Transport = t
}

func (ud *Updater) GetTag() (Tag, int, error) {
	if ud.buildType == off {
		return Tag{}, -1, nil
	}
	url := fmt.Sprintf("%s/repo/%s/%s/tag/latest/%s", ud.url, ud.namespace, ud.name, ud.tag)
	// fmt.Printf("Pinging URL \"%s\" for updates\n", url)
	req, _ := http.NewRequest("GET", url, nil)
	resp, err := ud.httpClient.Do(req)
	defer ud.timeoutHandler()
	if err != nil || resp.StatusCode != 200 {
		return Tag{}, resp.StatusCode, err
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return Tag{}, -1, err
	}

	var tag Tag
	err = json.Unmarshal(body, &tag)
	if tag.Version == "" {
		err = errors.New("Tag was empty")
	}
	return tag, resp.StatusCode, err
}

func (t *Tag) IsNew() bool {
	// fmt.Printf("Build Time: %+v, Release Date: %+v", buildTime, t.ReleaseDate)
	// Add 20 minutes to account for build time
	return t.Version[:7] != commit && t.Ready && t.ReleaseDate.After(buildTime.Add(time.Duration(20)*time.Minute))
}

func (ud *Updater) getRelease() (release GHRelease, status int, err error) {
	url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", ud.namespace, ud.name)
	req, _ := http.NewRequest("GET", url, nil)
	resp, err := ud.httpClient.Do(req)
	status = resp.StatusCode
	defer ud.timeoutHandler()
	if err != nil || resp.StatusCode != 200 {
		return
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return
	}
	err = json.Unmarshal(body, &release)
	return
}

func (ud *Updater) GetUpdate(tag Tag) (update Update, status int, err error) {
	switch ud.buildType {
	case internal:
		if ud.tag == "internal-git" {
			update, status, err = ud.getUpdateInternalGit(tag)
		} else if ud.tag == "internal" {
			update, status, err = ud.getUpdateInternal(tag)
		}
	case external, docker:
		if strings.Contains(ud.tag, "git") || ud.tag == "docker-unstable" {
			update, status, err = ud.getCommitGit(tag)
		} else {
			var release GHRelease
			release, status, err = ud.getRelease()
			if err != nil {
				return
			}
			update = Update{
				Changelog:   release.Body,
				Description: release.Name,
				Version:     release.TagName,
				Commit:      tag.Version,
				Link:        release.HTMLURL,
				ReleaseDate: release.PublishedAt.Unix(),
			}
		}
		if ud.buildType == docker {
			update.DownloadLink = fmt.Sprintf("https://hub.docker.com/r/%s/%s/tags", ud.namespace, ud.name)
		}
	}
	return
}

func (ud *Updater) getUpdateInternal(tag Tag) (update Update, status int, err error) {
	release, status, err := ud.getRelease()
	update = Update{
		Changelog:   release.Body,
		Description: release.Name,
		Version:     release.TagName,
		Commit:      tag.Version,
		Link:        release.HTMLURL,
		ReleaseDate: release.PublishedAt.Unix(),
	}
	if err != nil || status != 200 {
		return
	}
	updateFunc, status, err := ud.downloadInternal(&release.Assets, tag)
	if err == nil && status == 200 {
		update.CanUpdate = true
		update.update = updateFunc
	}
	return
}

func (ud *Updater) getCommitGit(tag Tag) (update Update, status int, err error) {
	url := fmt.Sprintf("%s/repo/%s/%s/build/%s", ud.url, ud.namespace, ud.name, tag.Version)
	req, _ := http.NewRequest("GET", url, nil)
	resp, err := ud.httpClient.Do(req)
	status = resp.StatusCode
	defer ud.timeoutHandler()
	if err != nil || resp.StatusCode != 200 {
		return
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return
	}
	var build BuildDTO
	err = json.Unmarshal(body, &build)
	if err != nil {
		return
	}
	update = Update{
		Description: build.Name,
		Version:     "git",
		Commit:      tag.Version,
		Link:        build.Link,
		ReleaseDate: tag.ReleaseDate.Unix(),
	}
	return
}

func (ud *Updater) getUpdateInternalGit(tag Tag) (update Update, status int, err error) {
	update, status, err = ud.getCommitGit(tag)
	if err != nil || status != 200 {
		return
	}
	updateFunc, status, err := ud.downloadInternalGit()
	if err == nil && status == 200 {
		update.CanUpdate = true
		update.update = updateFunc
	}
	return
}

func getBuildName() string {
	operatingSystem, ok := goos[runtime.GOOS]
	if !ok {
		for _, v := range goos {
			if strings.Contains(v, runtime.GOOS) {
				operatingSystem = v
				break
			}
		}
	}
	if operatingSystem == "" {
		return ""
	}
	arch, ok := goarch[runtime.GOARCH]
	if !ok {
		for _, v := range goarch {
			if strings.Contains(v, runtime.GOARCH) {
				arch = v
				break
			}
		}
	}
	if arch == "" {
		return ""
	}
	tray := ""
	if TRAY {
		tray = "TrayIcon_"
	}
	return tray + operatingSystem + "_" + arch
}

func (ud *Updater) downloadInternal(assets *[]GHAsset, tag Tag) (applyUpdate ApplyUpdate, status int, err error) {
	return ud.pullInternal(ud.getInternalURL(assets, tag))
}

func (ud *Updater) downloadInternalGit() (applyUpdate ApplyUpdate, status int, err error) {
	return ud.pullInternal(ud.getInternalGitURL())
}

func (ud *Updater) getInternalURL(assets *[]GHAsset, tag Tag) string {
	buildName := getBuildName()
	if buildName == "" {
		return ""
	}
	url := ""
	for _, asset := range *assets {
		if strings.Contains(asset.Name, buildName) {
			url = asset.BrowserDownloadURL
			break
		}
	}
	return url
}

func (ud *Updater) getInternalGitURL() string {
	buildName := getBuildName()
	if buildName == "" {
		return ""
	}
	return fmt.Sprintf("%s/repo/%s/%s/latest/file/%s", ud.url, ud.namespace, ud.name, buildName)
}

func (ud *Updater) pullInternal(url string) (applyUpdate ApplyUpdate, status int, err error) {
	if url == "" {
		return
	}
	req, _ := http.NewRequest("GET", url, nil)
	resp, err := ud.httpClient.Do(req)
	status = resp.StatusCode
	if err != nil || resp.StatusCode != 200 {
		return
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		status = -1
		return
	}
	zp, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
	if err != nil {
		status = -1
		return
	}
	for _, zf := range zp.File {
		if zf.Name != ud.binary {
			continue
		}
		var file string
		file, err = os.Executable()
		if err != nil {
			return
		}
		var path string
		path, err = filepath.EvalSymlinks(file)
		if err != nil {
			return
		}
		var info fs.FileInfo
		info, err = os.Stat(path)
		if err != nil {
			return
		}
		mode := info.Mode()
		var unzippedFile io.ReadCloser
		unzippedFile, err = zf.Open()
		if err != nil {
			return
		}
		defer unzippedFile.Close()
		var f *os.File
		f, err = os.OpenFile(path+"_", os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode)
		if err != nil {
			return
		}
		defer f.Close()
		_, err = io.Copy(f, unzippedFile)
		if err != nil {
			return
		}
		applyUpdate = func() error {
			oldName := path + "-" + version + "-" + commit
			err := os.Rename(path, oldName)
			if err != nil {
				return err
			}
			err = os.Rename(path+"_", path)
			if err != nil {
				return err
			}
			return os.Remove(oldName)
		}
		return
	}
	// gz, err := gzip.NewReader(resp.Body)
	// if err != nil {
	// 	status = -1
	// 	return
	// }
	// defer gz.Close()
	// tarReader := tar.NewReader(gz)
	// var header *tar.Header
	// for {
	// 	header, err = tarReader.Next()
	// 	if err == io.EOF {
	// 		break
	// 	}
	// 	if err != nil {
	// 		status = -1
	// 		return
	// 	}
	// 	switch header.Typeflag {
	// 	case tar.TypeReg:
	// 		// Search only for file named ud.binary
	// 		if header.Name == ud.binary {
	// 			var file string
	// 			file, err = os.Executable()
	// 			if err != nil {
	// 				return
	// 			}
	// 			var path string
	// 			path, err = filepath.EvalSymlinks(file)
	// 			if err != nil {
	// 				return
	// 			}
	// 			var info fs.FileInfo
	// 			info, err = os.Stat(path)
	// 			if err != nil {
	// 				return
	// 			}
	// 			mode := info.Mode()
	// 			var f *os.File
	// 			f, err = os.OpenFile(path+"_", os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode)
	// 			if err != nil {
	// 				return
	// 			}
	// 			defer f.Close()
	// 			_, err = io.Copy(f, tarReader)
	// 			if err != nil {
	// 				return
	// 			}
	// 			applyUpdate = func() error {
	// 				oldName := path + "-" + version + "-" + commit
	// 				err := os.Rename(path, oldName)
	// 				if err != nil {
	// 					return err
	// 				}
	// 				err = os.Rename(path+"_", path)
	// 				if err != nil {
	// 					return err
	// 				}
	// 				return os.Remove(oldName)
	// 			}
	// 			return
	// 		}
	// 	}
	// }
	err = errors.New("Couldn't find file: " + ud.binary)
	return
}

// func newInternalBuild() Update {
// 	tag := "internal"

//	func update(path string) err {
//		if
//		fp, err := os.Executable()
//		if err != nil {
//			return err
//		}
//		fullPath, err := filepath.EvalSymlinks(fp)
//		if err != nil {
//			return err
//		}
//		newBinary,
//	}
func (app *appContext) checkForUpdates() {
	for {
		go func() {
			tag, status, err := app.updater.GetTag()
			if status != 200 || err != nil {
				if err != nil && strings.Contains(err.Error(), "strconv.ParseInt") {
					app.err.Println("No new updates available.")
				} else if status != -1 { // -1 means updates disabled, we don't need to log it.
					app.err.Printf("Failed to get latest tag (%d): %v", status, err)
				}
				return
			}
			if tag != app.tag && tag.IsNew() {
				app.info.Println("Update found")
				update, status, err := app.updater.GetUpdate(tag)
				if status != 200 || err != nil {
					app.err.Printf("Failed to get update (%d): %v", status, err)
					return
				}
				app.tag = tag
				app.update = update
				app.newUpdate = true
			}
		}()
		time.Sleep(30 * time.Minute)
	}
}
