// Package updater manages standalone UI client artifacts, version selection,
// and persisted update state shared by the loader and the UI process.
package updater

import (
	"errors"
	"fmt"
	"path/filepath"
	"runtime"
	"slices"
	"strings"

	"galaxy/connector"
	gerr "galaxy/error"
	mc "galaxy/model/client"
	"galaxy/storage"
	"galaxy/util"
)

const (
	// ArtifactDir keeps versioned UI executables isolated from user data files.
	ArtifactDir = "ui"
	// ArtifactPrefix is the file name prefix used for all managed UI artifacts.
	ArtifactPrefix = "client-ui"
)

// LaunchTarget describes the executable artifact selected for the next UI run.
type LaunchTarget struct {
	Version string
	Path    string
	Pending bool
}

// Manager coordinates client update state, artifact downloads, and cleanup.
type Manager struct {
	storage   storage.Storage
	connector connector.Connector
	goos      string
	goarch    string
	kind      string
}

// Option customizes Manager construction.
type Option func(*Manager)

// WithPlatform overrides the runtime platform used for version matching.
func WithPlatform(goos, goarch string) Option {
	return func(m *Manager) {
		if goos != "" {
			m.goos = goos
		}
		if goarch != "" {
			m.goarch = goarch
		}
	}
}

// WithArtifactKind overrides the artifact kind accepted by the manager.
func WithArtifactKind(kind string) Option {
	return func(m *Manager) {
		if kind != "" {
			m.kind = kind
		}
	}
}

// NewManager constructs an update manager for standalone executable artifacts.
func NewManager(s storage.Storage, c connector.Connector, opts ...Option) *Manager {
	m := &Manager{
		storage:   s,
		connector: c,
		goos:      runtime.GOOS,
		goarch:    runtime.GOARCH,
		kind:      connector.ArtifactKindExecutable,
	}
	for _, opt := range opts {
		opt(m)
	}
	return m
}

// ArtifactPath returns the deterministic local storage path for the given versioned artifact.
func ArtifactPath(version, goos, goarch, kind string) string {
	name := fmt.Sprintf("%s-%s-%s-%s-%s", ArtifactPrefix, version, goos, goarch, kind)
	if goos == "windows" {
		name += ".exe"
	}
	return filepath.Join(ArtifactDir, name)
}

// LatestCompatibleVersion returns the latest supported version for the selected platform and kind.
func LatestCompatibleVersion(versions []connector.VersionInfo, goos, goarch, kind string) (connector.VersionInfo, bool, error) {
	platformMatches := make([]connector.VersionInfo, 0, len(versions))
	for _, version := range versions {
		if version.OS == goos && version.Arch == goarch {
			platformMatches = append(platformMatches, version)
		}
	}
	if len(platformMatches) == 0 {
		return connector.VersionInfo{}, false, nil
	}

	candidates := make([]connector.VersionInfo, 0, len(platformMatches))
	unsupportedKinds := make(map[string]struct{})
	seenVersion := make(map[string]struct{})
	for _, version := range platformMatches {
		if version.Kind != kind {
			unsupportedKinds[version.Kind] = struct{}{}
			continue
		}
		if _, ok := seenVersion[version.Version]; ok {
			return connector.VersionInfo{}, false, gerr.WrapService(
				fmt.Errorf("ambiguous client artifact version %q for %s/%s", version.Version, goos, goarch),
			)
		}
		seenVersion[version.Version] = struct{}{}
		candidates = append(candidates, version)
	}
	if len(candidates) == 0 {
		kinds := make([]string, 0, len(unsupportedKinds))
		for kind := range unsupportedKinds {
			kinds = append(kinds, kind)
		}
		slices.Sort(kinds)
		return connector.VersionInfo{}, false, gerr.WrapService(
			fmt.Errorf("unsupported client artifact kind(s) for %s/%s: %s", goos, goarch, strings.Join(kinds, ", ")),
		)
	}

	type semVersion struct {
		info connector.VersionInfo
		sem  util.SemVer
	}
	semvers := make([]semVersion, len(candidates))
	for i, candidate := range candidates {
		semver, err := util.ParseSemver(candidate.Version)
		if err != nil {
			return connector.VersionInfo{}, false, gerr.WrapService(
				fmt.Errorf("parse client version %q: %w", candidate.Version, err),
			)
		}
		semvers[i] = semVersion{info: candidate, sem: semver}
	}

	slices.SortFunc(semvers, func(a, b semVersion) int {
		return util.CompareSemver(a.sem, b.sem)
	})
	return semvers[0].info, true, nil
}

// EnsureLaunchTarget returns the versioned executable that should be launched next.
// On the very first run, when no current or pending version exists yet, it downloads
// the latest compatible artifact and marks it as pending.
func (m *Manager) EnsureLaunchTarget() (LaunchTarget, error) {
	state, err := m.ensureState()
	if err != nil {
		return LaunchTarget{}, err
	}

	if state.ClientNextVersion != nil {
		return m.launchTargetForVersion(*state.ClientNextVersion, true)
	}
	if state.ClientCurrentVersion != "" {
		return m.launchTargetForVersion(state.ClientCurrentVersion, false)
	}
	if err := m.CheckAndPrepareLatest(); err != nil {
		return LaunchTarget{}, err
	}

	state, err = m.ensureState()
	if err != nil {
		return LaunchTarget{}, err
	}
	if state.ClientNextVersion == nil {
		return LaunchTarget{}, gerr.WrapService(errors.New("latest client version was not prepared for launch"))
	}

	return m.launchTargetForVersion(*state.ClientNextVersion, true)
}

// CheckAndPrepareLatest checks the backend manifest and downloads a newer compatible
// artifact when one exists.
func (m *Manager) CheckAndPrepareLatest() error {
	if m.connector == nil {
		return gerr.WrapService(errors.New("client updater connector is not configured"))
	}

	versions, err := m.connector.CheckVersion()
	if err != nil {
		return err
	}
	latest, ok, err := LatestCompatibleVersion(versions, m.goos, m.goarch, m.kind)
	if err != nil {
		return err
	}
	if !ok {
		return gerr.WrapService(
			fmt.Errorf("server did not provide a compatible %s client for %s/%s", m.kind, m.goos, m.goarch),
		)
	}

	state, err := m.ensureState()
	if err != nil {
		return err
	}

	latestSemver, err := util.ParseSemver(latest.Version)
	if err != nil {
		return gerr.WrapService(fmt.Errorf("parse latest client version %q: %w", latest.Version, err))
	}

	if state.ClientCurrentVersion != "" {
		currentSemver, err := util.ParseSemver(state.ClientCurrentVersion)
		if err != nil {
			return gerr.WrapService(fmt.Errorf("parse current client version %q: %w", state.ClientCurrentVersion, err))
		}
		if util.CompareSemver(currentSemver, latestSemver) >= 0 {
			return nil
		}
	}
	if state.ClientNextVersion != nil {
		nextSemver, err := util.ParseSemver(*state.ClientNextVersion)
		if err != nil {
			return gerr.WrapService(fmt.Errorf("parse pending client version %q: %w", *state.ClientNextVersion, err))
		}
		if util.CompareSemver(nextSemver, latestSemver) >= 0 {
			return nil
		}
	}

	if err := m.downloadArtifact(latest); err != nil {
		return err
	}

	state.ClientNextVersion = &latest.Version
	return m.saveState(state)
}

// MarkLaunchResult records the outcome of a launched artifact and promotes
// pending versions to current only after a successful run.
func (m *Manager) MarkLaunchResult(version string, exitCode int, runErr error) error {
	state, err := m.ensureState()
	if err != nil {
		return err
	}

	if state.ClientNextVersion != nil && *state.ClientNextVersion == version {
		if runErr == nil && exitCode == 0 {
			state.ClientCurrentVersion = version
		}
		state.ClientNextVersion = nil
		if err := m.saveState(state); err != nil {
			return err
		}
		return m.cleanupArtifacts(state)
	}

	if runErr == nil && exitCode == 0 {
		return m.cleanupArtifacts(state)
	}
	return nil
}

func (m *Manager) launchTargetForVersion(version string, pending bool) (LaunchTarget, error) {
	path := ArtifactPath(version, m.goos, m.goarch, m.kind)
	exists, absPath, err := m.storage.FileExists(path)
	if err != nil {
		return LaunchTarget{}, err
	}
	if !exists {
		return LaunchTarget{}, gerr.WrapStorage(
			fmt.Errorf("client artifact for version %q not found at %q", version, path),
		)
	}
	return LaunchTarget{
		Version: version,
		Path:    absPath,
		Pending: pending,
	}, nil
}

func (m *Manager) ensureState() (mc.State, error) {
	if m.storage == nil {
		return mc.State{}, gerr.WrapStorage(errors.New("client updater storage is not configured"))
	}

	exists, err := m.storage.StateExists()
	if err != nil {
		return mc.State{}, err
	}
	if !exists {
		state := mc.State{}
		if err := m.storage.SaveState(state); err != nil {
			return mc.State{}, err
		}
		return state, nil
	}
	return m.storage.LoadState()
}

func (m *Manager) saveState(state mc.State) error {
	return m.storage.SaveState(state)
}

func (m *Manager) downloadArtifact(version connector.VersionInfo) error {
	data, err := m.connector.DownloadVersion(version.URL)
	if err != nil {
		return err
	}
	digest := connector.NewSHA256Digest(data)
	if !digest.Equal(version.Checksum) {
		return gerr.WrapService(fmt.Errorf("downloaded client artifact checksum mismatch for version %s", version.Version))
	}

	path := ArtifactPath(version.Version, version.OS, version.Arch, version.Kind)
	exists, _, err := m.storage.FileExists(path)
	if err != nil {
		return err
	}
	if exists {
		storedData, err := m.storage.ReadFile(path)
		if err != nil {
			return err
		}
		if connector.NewSHA256Digest(storedData).Equal(version.Checksum) {
			return nil
		}
		if err := m.storage.DeleteFile(path); err != nil {
			return err
		}
	}

	return m.storage.WriteFile(path, data)
}

func (m *Manager) cleanupArtifacts(state mc.State) error {
	files, err := m.storage.ListFiles()
	if err != nil {
		return err
	}

	retain := make(map[string]struct{}, 2)
	if state.ClientCurrentVersion != "" {
		retain[ArtifactPath(state.ClientCurrentVersion, m.goos, m.goarch, m.kind)] = struct{}{}
	}
	if state.ClientNextVersion != nil {
		retain[ArtifactPath(*state.ClientNextVersion, m.goos, m.goarch, m.kind)] = struct{}{}
	}

	prefix := filepath.ToSlash(ArtifactDir) + "/"
	for _, file := range files {
		slashed := filepath.ToSlash(file)
		if !strings.HasPrefix(slashed, prefix) {
			continue
		}
		if !strings.HasPrefix(filepath.Base(file), ArtifactPrefix+"-") {
			continue
		}
		if _, ok := retain[file]; ok {
			continue
		}
		if err := m.storage.DeleteFile(file); err != nil {
			return err
		}
	}
	return nil
}
