// Keywarden - Centralized SSH Key Management and Deployment
// Copyright (C) 2026 Patrick Asmus (scriptos)
// SPDX-License-Identifier: AGPL-3.0-or-later

package updater

import (
	"encoding/json"
	"net/http"
	"strconv"
	"strings"
	"sync"
	"time"

	"git.techniverse.net/scriptos/keywarden/internal/logging"
)

const (
	// Gitea API endpoint for releases
	releasesAPI = "https://git.techniverse.net/api/v1/repos/scriptos/keywarden/releases?limit=5"
	// Public releases page URL
	ReleasesPageURL = "https://git.techniverse.net/scriptos/keywarden/releases"
	// How often to check for updates
	checkInterval = 6 * time.Hour
	// HTTP timeout for API requests
	httpTimeout = 15 * time.Second
)

// giteaRelease represents the relevant fields from the Gitea releases API
type giteaRelease struct {
	TagName    string `json:"tag_name"`
	HTMLURL    string `json:"html_url"`
	Draft      bool   `json:"draft"`
	Prerelease bool   `json:"prerelease"`
}

// Service checks for new releases in the background
type Service struct {
	currentVersion string

	mu            sync.RWMutex
	latestVersion string
	releaseURL    string
	hasUpdate     bool

	stopCh chan struct{}
}

// NewService creates an update checker. Pass the current application version
// (e.g. "v1.0.0" or "dev"). The checker runs in the background and queries
// the Gitea releases API periodically.
func NewService(currentVersion string) *Service {
	return &Service{
		currentVersion: currentVersion,
		stopCh:         make(chan struct{}),
	}
}

// Start begins periodic update checks in the background.
func (s *Service) Start() {
	// Don't check if running a dev build
	if s.currentVersion == "" || s.currentVersion == "dev" {
		logging.Info("Update checker disabled (development build)")
		return
	}
	logging.Info("Update checker started (current version: %s, checking every %s)", s.currentVersion, checkInterval)

	go s.run()
}

// Stop signals the background goroutine to exit.
func (s *Service) Stop() {
	close(s.stopCh)
}

// HasUpdate returns true if a newer version is available.
func (s *Service) HasUpdate() bool {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.hasUpdate
}

// LatestVersion returns the tag name of the latest release (e.g. "v1.2.0").
func (s *Service) LatestVersion() string {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.latestVersion
}

// ReleaseURL returns the HTML link to the latest release page on Gitea.
func (s *Service) ReleaseURL() string {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.releaseURL
}

// CurrentVersion returns the running application version.
func (s *Service) CurrentVersion() string {
	return s.currentVersion
}

func (s *Service) run() {
	// Initial check shortly after startup
	timer := time.NewTimer(30 * time.Second)
	defer timer.Stop()

	for {
		select {
		case <-s.stopCh:
			return
		case <-timer.C:
			s.check()
			timer.Reset(checkInterval)
		}
	}
}

func (s *Service) check() {
	client := &http.Client{Timeout: httpTimeout}

	resp, err := client.Get(releasesAPI)
	if err != nil {
		logging.Warn("Update check failed: %v", err)
		return
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		logging.Warn("Update check: Gitea API returned status %d", resp.StatusCode)
		return
	}

	var releases []giteaRelease
	if err := json.NewDecoder(resp.Body).Decode(&releases); err != nil {
		logging.Warn("Update check: failed to parse response: %v", err)
		return
	}

	// Find the latest stable release (not draft, not prerelease)
	for _, rel := range releases {
		if rel.Draft || rel.Prerelease || rel.TagName == "" {
			continue
		}

		s.mu.Lock()
		s.latestVersion = rel.TagName
		s.releaseURL = rel.HTMLURL
		s.hasUpdate = isNewer(rel.TagName, s.currentVersion)
		s.mu.Unlock()

		if s.HasUpdate() {
			logging.Info("New version available: %s (current: %s)", rel.TagName, s.currentVersion)
		}
		return
	}
}

// isNewer returns true if latest is a higher version than current.
// Both may optionally have a "v" prefix (e.g. "v1.2.3").
// Pre-release versions (e.g. "v1.0.0-alpha") are considered older than the
// same version without a suffix (e.g. "v1.0.0").
func isNewer(latest, current string) bool {
	latestParts := parseVersion(latest)
	currentParts := parseVersion(current)

	for i := 0; i < len(latestParts) || i < len(currentParts); i++ {
		l, c := 0, 0
		if i < len(latestParts) {
			l = latestParts[i]
		}
		if i < len(currentParts) {
			c = currentParts[i]
		}
		if l > c {
			return true
		}
		if l < c {
			return false
		}
	}

	// Numeric parts are equal – a stable release is newer than a pre-release
	// of the same version (e.g. v0.3.0 > v0.3.0-alpha).
	if hasPreRelease(current) && !hasPreRelease(latest) {
		return true
	}

	return false
}

// parseVersion strips the "v" prefix and pre-release suffixes (e.g. "-alpha")
// then splits "1.2.3" into [1, 2, 3].
// A pre-release version is indicated by returning isPreRelease = true.
func parseVersion(v string) []int {
	v = strings.TrimPrefix(v, "v")
	// Strip pre-release suffix (e.g. "-alpha", "-beta.1", "-rc1")
	if idx := strings.IndexByte(v, '-'); idx >= 0 {
		v = v[:idx]
	}
	parts := strings.Split(v, ".")
	nums := make([]int, 0, len(parts))
	for _, p := range parts {
		n, err := strconv.Atoi(p)
		if err != nil {
			break
		}
		nums = append(nums, n)
	}
	return nums
}

// hasPreRelease returns true when the version string contains a pre-release
// suffix (e.g. "-alpha", "-beta", "-rc1").
func hasPreRelease(v string) bool {
	v = strings.TrimPrefix(v, "v")
	return strings.ContainsRune(v, '-')
}
