package updater

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
	"time"
)

// UpdateInfo describes the result of checking for an available update.
type UpdateInfo struct {
	Available   bool   `json:"available"`
	CurrentVer  string `json:"currentVersion"`
	LatestVer   string `json:"latestVersion"`
	DownloadURL string `json:"downloadUrl"`
	SHA256      string `json:"sha256"`
}

// UpdateService checks a Gitea generic-package registry for new versions
// and can download + launch an installer to apply the update.
type UpdateService struct {
	currentVersion string
	baseURL        string // e.g. https://git.command.vigilcyber.com
	owner          string // e.g. vstockwell
	pkg            string // e.g. wraith
	httpClient     *http.Client
}

// CurrentVersion returns the build-time version string.
func (u *UpdateService) CurrentVersion() string {
	return u.currentVersion
}

// NewUpdateService creates an UpdateService that compares against the
// supplied currentVersion (semver without "v" prefix, e.g. "0.2.0").
func NewUpdateService(currentVersion string) *UpdateService {
	return &UpdateService{
		currentVersion: strings.TrimPrefix(currentVersion, "v"),
		baseURL:        "https://git.command.vigilcyber.com",
		owner:          "vstockwell",
		pkg:            "wraith",
		httpClient: &http.Client{
			Timeout: 30 * time.Second,
		},
	}
}

// giteaPackageVersion is the subset of the Gitea API response we need.
type giteaPackageVersion struct {
	Version string `json:"version"`
}

// giteaRelease is the subset of the Gitea releases API response we need.
type giteaRelease struct {
	TagName string `json:"tag_name"`
}

// versionJSON is the schema of the version.json file in the package.
type versionJSON struct {
	Version     string `json:"version"`
	SHA256      string `json:"sha256"`
	DownloadURL string `json:"downloadUrl"`
}

// CheckForUpdate queries the Gitea package API for the latest version and
// compares it with the current version using semver comparison.
func (u *UpdateService) CheckForUpdate() (*UpdateInfo, error) {
	info := &UpdateInfo{
		CurrentVer: u.currentVersion,
	}

	// Fetch latest package version from the Gitea API.
	// Use Gitea releases API to find the latest release
	apiURL := fmt.Sprintf(
		"%s/api/v1/repos/%s/%s/releases/latest",
		u.baseURL, u.owner, u.pkg,
	)

	slog.Info("checking for updates", "url", apiURL)

	req, err := http.NewRequest("GET", apiURL, nil)
	if err != nil {
		return nil, fmt.Errorf("build request: %w", err)
	}

	resp, err := u.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("fetch latest release: %w", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("read release response: %w", err)
	}

	slog.Info("release API response", "status", resp.StatusCode, "bodyLen", len(body))

	if resp.StatusCode != http.StatusOK {
		// No releases yet — not an error, just nothing to update to
		slog.Info("no releases found", "status", resp.StatusCode)
		return info, nil
	}

	var release giteaRelease
	if err := json.Unmarshal(body, &release); err != nil {
		return nil, fmt.Errorf("decode release: %w", err)
	}

	if release.TagName == "" {
		slog.Info("release has no tag")
		return info, nil
	}

	latestVer := strings.TrimPrefix(release.TagName, "v")
	info.LatestVer = latestVer
	slog.Info("latest release", "tag", release.TagName, "version", latestVer, "current", u.currentVersion)

	cmp := CompareVersions(u.currentVersion, latestVer)
	if cmp >= 0 {
		slog.Info("already up to date", "current", u.currentVersion, "latest", latestVer)
		return info, nil
	}

	// Newer version is available — fetch version.json for SHA256 + download URL.
	tagVersion := release.TagName // e.g. "v0.6.0" or "0.6.0"
	versionInfoURL := fmt.Sprintf(
		"%s/api/packages/%s/generic/%s/%s/version.json",
		u.baseURL, u.owner, u.pkg, tagVersion,
	)

	vInfo, err := u.fetchVersionJSON(versionInfoURL)
	if err != nil {
		// Non-fatal: we still know the version is available, just no SHA256.
		slog.Warn("could not fetch version.json", "error", err)
	}

	info.Available = true

	if vInfo != nil {
		info.SHA256 = vInfo.SHA256
		if vInfo.DownloadURL != "" {
			info.DownloadURL = vInfo.DownloadURL
		}
	}

	if info.DownloadURL == "" {
		info.DownloadURL = fmt.Sprintf(
			"%s/api/packages/%s/generic/%s/%s/wraith-%s-setup.exe",
			u.baseURL, u.owner, u.pkg, tagVersion, tagVersion,
		)
	}

	slog.Info("update available", "current", u.currentVersion, "latest", latestVer)
	return info, nil
}

// fetchVersionJSON fetches and decodes a version.json file from the given URL.
func (u *UpdateService) fetchVersionJSON(url string) (*versionJSON, error) {
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return nil, err
	}
	if token := os.Getenv("GIT_TOKEN"); token != "" {
		req.Header.Set("Authorization", "token "+token)
	}

	resp, err := u.httpClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("status %d", resp.StatusCode)
	}

	var v versionJSON
	if err := json.NewDecoder(resp.Body).Decode(&v); err != nil {
		return nil, err
	}
	return &v, nil
}

// DownloadUpdate downloads the installer for the given version to a temporary
// file and returns its path. If the UpdateInfo includes a SHA256 hash the
// download is verified against it.
func (u *UpdateService) DownloadUpdate(info *UpdateInfo) (string, error) {
	if info.DownloadURL == "" {
		return "", fmt.Errorf("no download URL")
	}

	slog.Info("downloading update", "url", info.DownloadURL)

	req, err := http.NewRequest("GET", info.DownloadURL, nil)
	if err != nil {
		return "", fmt.Errorf("build request: %w", err)
	}
	if token := os.Getenv("GIT_TOKEN"); token != "" {
		req.Header.Set("Authorization", "token "+token)
	}

	resp, err := u.httpClient.Do(req)
	if err != nil {
		return "", fmt.Errorf("download installer: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("unexpected status %d downloading installer", resp.StatusCode)
	}

	// Write to a temp file.
	tmpDir := os.TempDir()
	filename := fmt.Sprintf("wraith-%s-setup.exe", info.LatestVer)
	destPath := filepath.Join(tmpDir, filename)

	f, err := os.Create(destPath)
	if err != nil {
		return "", fmt.Errorf("create temp file: %w", err)
	}
	defer f.Close()

	hasher := sha256.New()
	writer := io.MultiWriter(f, hasher)

	if _, err := io.Copy(writer, resp.Body); err != nil {
		os.Remove(destPath)
		return "", fmt.Errorf("write installer: %w", err)
	}

	// Verify SHA256 if provided.
	if info.SHA256 != "" {
		got := hex.EncodeToString(hasher.Sum(nil))
		if !strings.EqualFold(got, info.SHA256) {
			os.Remove(destPath)
			return "", fmt.Errorf("SHA256 mismatch: expected %s, got %s", info.SHA256, got)
		}
		slog.Info("SHA256 verified", "hash", got)
	}

	slog.Info("update downloaded", "path", destPath)
	return destPath, nil
}

// ApplyUpdate launches the downloaded installer and signals the caller to
// exit. On Windows it starts the exe; on other platforms it logs a message.
func (u *UpdateService) ApplyUpdate(installerPath string) error {
	slog.Info("applying update", "installer", installerPath)

	if runtime.GOOS == "windows" {
		cmd := exec.Command(installerPath, "/SILENT")
		if err := cmd.Start(); err != nil {
			return fmt.Errorf("launch installer: %w", err)
		}
		// The caller (frontend) should exit the app after this returns.
		return nil
	}

	// Non-Windows: nothing to do automatically — inform the user.
	slog.Info("update downloaded — please run the installer manually", "path", installerPath)
	return nil
}

// CompareVersions compares two semver strings (major.minor.patch).
// Returns -1 if a < b, 0 if equal, 1 if a > b.
// Strings are trimmed of a leading "v" prefix.
func CompareVersions(a, b string) int {
	a = strings.TrimPrefix(a, "v")
	b = strings.TrimPrefix(b, "v")

	aParts := parseSemver(a)
	bParts := parseSemver(b)

	for i := 0; i < 3; i++ {
		if aParts[i] < bParts[i] {
			return -1
		}
		if aParts[i] > bParts[i] {
			return 1
		}
	}
	return 0
}

// parseSemver splits a version string into [major, minor, patch].
// Missing components default to 0.
func parseSemver(v string) [3]int {
	var parts [3]int
	segments := strings.SplitN(v, ".", 3)
	for i, s := range segments {
		if i >= 3 {
			break
		}
		n, _ := strconv.Atoi(s)
		parts[i] = n
	}
	return parts
}
