package lkgateway

import (
	"context"
	"fmt"
	"log"
	"os"
	"time"

	"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/release"
)

// BuildVersion — версия bj-server. Переопределяется при сборке:
//
//	go build -ldflags "-X .../lkgateway.BuildVersion=1.2.0"
var BuildVersion = "0.1.0"

// DefaultUpdatePublicKey — публичный ключ артефактории, зашитый в релиз.
// Пустой в исходниках; подставляется при официальной сборке. Если задан в
// настройках (UpdateSettings.PublicKey) — приоритет у настроек.
var DefaultUpdatePublicKey = ""

// installPaths — куда устанавливать артефакты по логическому имени.
// Файлы не из этого списка при авто-обновлении пропускаются (скрипты/SQL
// обновляются отдельно, не на лету).
var installPaths = map[string]string{
	"bj-server":          "/opt/bj/bj-server",
	"crypto-service.jar": "/opt/bj/crypto-service.jar",
}

// Updater — авто-обновление bj-server из артефактории (работает поверх rc).
type Updater struct{ rc *RuntimeConfig }

// NewUpdater создаёт Updater на текущем runtime-конфиге.
func NewUpdater(rc *RuntimeConfig) *Updater { return &Updater{rc: rc} }

// UpdateStatus — сводка для UI/handler.
type UpdateStatus struct {
	Configured     bool
	CurrentVersion string
	Available      string
	HasUpdate      bool
	Channel        string
	Notes          string
	Message        string
}

func (u *Updater) updateClient() (*release.Client, error) {
	cfg := u.rc.Snapshot().Update
	pub := cfg.PublicKey
	if pub == "" {
		pub = DefaultUpdatePublicKey
	}
	if cfg.BaseURL == "" || pub == "" {
		return nil, fmt.Errorf("обновления не настроены (нужны URL артефактории и публичный ключ)")
	}
	channel := cfg.Channel
	if channel == "" {
		channel = "stable"
	}
	return release.NewClient(cfg.BaseURL, channel, pub)
}

// CheckForUpdate скачивает манифест, проверяет подпись, сравнивает версии и
// сохраняет результат в настройки. Возвращает сводку.
func (u *Updater) CheckForUpdate(ctx context.Context) (UpdateStatus, error) {
	st := UpdateStatus{CurrentVersion: BuildVersion, Channel: u.rc.Snapshot().Update.Channel}
	cl, err := u.updateClient()
	if err != nil {
		st.Message = err.Error()
		return st, nil // не настроено — не ошибка
	}
	st.Configured = true

	m, err := cl.FetchManifest(ctx)
	if err != nil {
		st.Message = "проверка не удалась: " + err.Error()
		u.saveCheckResult(st)
		return st, err
	}
	st.Available = m.Version
	st.Notes = m.Notes
	st.HasUpdate = release.IsNewer(m.Version, BuildVersion)
	if st.HasUpdate {
		st.Message = fmt.Sprintf("доступна версия %s (текущая %s)", m.Version, BuildVersion)
	} else {
		st.Message = "установлена актуальная версия " + BuildVersion
	}
	u.saveCheckResult(st)
	return st, nil
}

func (u *Updater) saveCheckResult(st UpdateStatus) {
	cfg := u.rc.Snapshot().Update
	cfg.LastCheck = time.Now().UTC()
	cfg.LastResult = st.Message
	cfg.Available = st.Available
	cfg.Notes = st.Notes
	if err := u.rc.SaveUpdateSettings(cfg); err != nil {
		log.Printf("lk-gateway: сохранение результата проверки обновления: %v", err)
	}
}

// ApplyUpdate скачивает обновлённые артефакты (с проверкой подписи манифеста
// и sha256 каждого файла), атомарно заменяет бинари и завершает процесс с
// ненулевым кодом — systemd (Restart=on-failure) поднимает новую версию.
func (u *Updater) ApplyUpdate(ctx context.Context) error {
	// Гейт лицензией: если лицензирование включено — требуется валидная
	// лицензия с фичей updates. Без лицензирования (открытый режим) — пропускаем.
	if licensingEnabled(u.rc) {
		ls := licenseStatus(u.rc)
		if !ls.Valid {
			return fmt.Errorf("обновления заблокированы — лицензия: %s", ls.Message)
		}
		if !ls.AllowsUpdates {
			return fmt.Errorf("обновления не входят в план %q", ls.Plan)
		}
	}
	cl, err := u.updateClient()
	if err != nil {
		return err
	}
	m, err := cl.FetchManifest(ctx)
	if err != nil {
		return fmt.Errorf("манифест: %w", err)
	}
	if !release.IsNewer(m.Version, BuildVersion) {
		return fmt.Errorf("обновление не требуется (текущая %s, доступна %s)", BuildVersion, m.Version)
	}

	updated := 0
	for _, a := range m.Artifacts {
		dst, ok := installPaths[a.Name]
		if !ok {
			continue // скрипты/SQL не обновляем на лету
		}
		dir := dirOf(dst)
		path, err := cl.DownloadArtifact(ctx, a, dir)
		if err != nil {
			return fmt.Errorf("скачивание %s: %w", a.Name, err)
		}
		// DownloadArtifact кладёт файл под именем a.File; если целевое имя
		// иное — переименуем атомарно.
		if path != dst {
			if err := os.Rename(path, dst); err != nil {
				return fmt.Errorf("установка %s: %w", a.Name, err)
			}
		}
		log.Printf("lk-gateway: обновлён %s → %s (%s)", a.Name, dst, m.Version)
		updated++
	}
	if updated == 0 {
		return fmt.Errorf("в манифесте %s нет обновляемых бинарей", m.Version)
	}

	log.Printf("lk-gateway: обновление до %s применено (%d файлов), перезапуск через systemd…", m.Version, updated)
	// Завершаемся с ненулевым кодом — systemd Restart=on-failure поднимет
	// новый бинарь. Даём пару секунд на флаш логов/ответа.
	go func() {
		time.Sleep(800 * time.Millisecond)
		os.Exit(42)
	}()
	return nil
}

// updateLoop — фоновая авто-проверка обновлений (если включена).
func (u *Updater) updateLoop(ctx context.Context) {
	ticker := time.NewTicker(6 * time.Hour)
	defer ticker.Stop()
	check := func() {
		if !u.rc.Snapshot().Update.AutoCheck {
			return
		}
		cctx, cancel := context.WithTimeout(ctx, 90*time.Second)
		defer cancel()
		if st, err := u.CheckForUpdate(cctx); err == nil && st.HasUpdate {
			log.Printf("lk-gateway: доступно обновление %s (текущая %s)", st.Available, st.CurrentVersion)
		}
	}
	// первая проверка через минуту после старта
	select {
	case <-ctx.Done():
		return
	case <-time.After(time.Minute):
		check()
	}
	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			check()
		}
	}
}

func dirOf(path string) string {
	for i := len(path) - 1; i >= 0; i-- {
		if path[i] == '/' {
			return path[:i]
		}
	}
	return "."
}
