package updates

import (
	"context"
	"fmt"
	"log/slog"
	"os"
	"sync"
	"time"
)

type Controller struct {
	app        AppDescriptor
	provider   ReleaseProvider
	downloader Downloader
	store      SnapshotStore
	platform   PlatformInstaller
	logger     *slog.Logger

	mu          sync.Mutex
	snapshot    Snapshot
	busy        bool
	listeners   map[int]chan Snapshot
	nextListener int
}

func NewController(cfg Config) (*Controller, error) {
	if cfg.Provider == nil || cfg.Downloader == nil || cfg.Store == nil || cfg.Platform == nil {
		return nil, ErrInvalidConfig
	}
	if _, err := NormalizeVersion(cfg.App.CurrentVersion); err != nil {
		return nil, err
	}
	if cfg.App.WorkingDirectory == "" {
		workingDirectory, err := os.Getwd()
		if err == nil {
			cfg.App.WorkingDirectory = workingDirectory
		}
	}

	logger := cfg.Logger
	if logger == nil {
		logger = slog.Default()
	}

	snapshot, err := cfg.Store.Load(context.Background())
	if err != nil {
		return nil, err
	}
	snapshot.CurrentVersion = cfg.App.CurrentVersion
	snapshot.Channel = cfg.App.Channel
	if snapshot.State == "" {
		snapshot.State = StateIdle
	}

	controller := &Controller{
		app:        cfg.App,
		provider:   cfg.Provider,
		downloader: cfg.Downloader,
		store:      cfg.Store,
		platform:   cfg.Platform,
		logger:     logger,
		snapshot:   snapshot,
		listeners:  make(map[int]chan Snapshot),
	}

	if snapshot.Staged != nil {
		comparison, err := CompareVersions(snapshot.Staged.Release.Version, cfg.App.CurrentVersion)
		if err == nil && comparison == 0 {
			snapshot.State = StateUpToDate
			snapshot.Staged = nil
			snapshot.Candidate = nil
			snapshot.LastError = nil
			controller.snapshot = snapshot
			if err := controller.store.Save(context.Background(), snapshot); err != nil {
				return nil, err
			}
		}
	}

	return controller, nil
}

func (controller *Controller) Snapshot() Snapshot {
	controller.mu.Lock()
	defer controller.mu.Unlock()
	return controller.snapshot
}

func (controller *Controller) Subscribe(buffer int) (<-chan Snapshot, func()) {
	if buffer < 1 {
		buffer = 1
	}

	controller.mu.Lock()
	defer controller.mu.Unlock()

	controller.nextListener++
	id := controller.nextListener
	channel := make(chan Snapshot, buffer)
	channel <- controller.snapshot
	controller.listeners[id] = channel

	return channel, func() {
		controller.mu.Lock()
		defer controller.mu.Unlock()
		if _, ok := controller.listeners[id]; ok {
			delete(controller.listeners, id)
		}
	}
}

func (controller *Controller) Check(ctx context.Context, _ CheckRequest) (Snapshot, error) {
	if !controller.beginOperation() {
		return Snapshot{}, ErrBusy
	}
	defer controller.endOperation()

	now := time.Now().UTC()
	controller.transition(StateChecking, nil, nil)

	release, err := controller.provider.Resolve(ctx, ResolveRequest{App: controller.app})
	if err != nil {
		return controller.fail("resolve_failed", err)
	}

	if release == nil {
		snapshot := controller.currentSnapshot()
		snapshot.State = StateUpToDate
		snapshot.LastCheckedAt = &now
		snapshot.Candidate = nil
		snapshot.LastError = nil
		if err := controller.save(snapshot); err != nil {
			return Snapshot{}, err
		}
		return snapshot, nil
	}

	comparison, err := CompareVersions(release.Version, controller.app.CurrentVersion)
	if err != nil {
		return controller.fail("invalid_release_version", err)
	}
	if comparison <= 0 {
		snapshot := controller.currentSnapshot()
		snapshot.State = StateUpToDate
		snapshot.LastCheckedAt = &now
		snapshot.Candidate = nil
		snapshot.LastError = nil
		if err := controller.save(snapshot); err != nil {
			return Snapshot{}, err
		}
		return snapshot, nil
	}

	snapshot := controller.currentSnapshot()
	snapshot.State = StateUpdateAvailable
	snapshot.LastCheckedAt = &now
	snapshot.Candidate = release
	snapshot.Staged = nil
	snapshot.LastError = nil
	if err := controller.save(snapshot); err != nil {
		return Snapshot{}, err
	}

	return snapshot, nil
}

func (controller *Controller) Download(ctx context.Context) (Snapshot, error) {
	if !controller.beginOperation() {
		return Snapshot{}, ErrBusy
	}
	defer controller.endOperation()

	snapshot := controller.currentSnapshot()
	if snapshot.Candidate == nil {
		return Snapshot{}, ErrNoUpdate
	}

	controller.transition(StateDownloading, snapshot.Candidate, nil)

	downloadedFile, err := controller.downloader.Download(ctx, snapshot.Candidate.Artifact)
	if err != nil {
		return controller.fail("download_failed", err)
	}
	if downloadedFile.SHA256 != "" && snapshot.Candidate.Artifact.SHA256 != "" && downloadedFile.SHA256 != snapshot.Candidate.Artifact.SHA256 {
		return controller.fail("checksum_mismatch", ErrChecksumMismatch)
	}

	controller.transition(StateVerifying, snapshot.Candidate, nil)

	root, err := controller.platform.DetectInstallRoot(controller.app)
	if err != nil {
		return controller.fail("detect_root_failed", err)
	}

	stagedArtifact, err := controller.platform.Stage(ctx, root, downloadedFile, *snapshot.Candidate)
	if err != nil {
		return controller.fail("stage_failed", err)
	}

	nextSnapshot := controller.currentSnapshot()
	nextSnapshot.State = StateReadyToApply
	nextSnapshot.Staged = &stagedArtifact
	nextSnapshot.Candidate = &stagedArtifact.Release
	nextSnapshot.LastError = nil
	if err := controller.save(nextSnapshot); err != nil {
		return Snapshot{}, err
	}

	return nextSnapshot, nil
}

func (controller *Controller) ApplyAndRestart(ctx context.Context) error {
	if !controller.beginOperation() {
		return ErrBusy
	}
	defer controller.endOperation()

	snapshot := controller.currentSnapshot()
	if snapshot.Staged == nil {
		return ErrNoStagedUpdate
	}

	controller.transition(StateApplying, snapshot.Candidate, snapshot.Staged)

	if err := controller.platform.SpawnApplyAndRestart(ctx, ApplyRequest{
		App:    controller.app,
		Root:   snapshot.Staged.Root,
		Staged: *snapshot.Staged,
	}); err != nil {
		_, failErr := controller.fail("apply_failed", err)
		return failErr
	}

	nextSnapshot := controller.currentSnapshot()
	nextSnapshot.State = StateRestarting
	nextSnapshot.LastError = nil
	return controller.save(nextSnapshot)
}

func (controller *Controller) beginOperation() bool {
	controller.mu.Lock()
	defer controller.mu.Unlock()
	if controller.busy {
		return false
	}
	controller.busy = true
	return true
}

func (controller *Controller) endOperation() {
	controller.mu.Lock()
	defer controller.mu.Unlock()
	controller.busy = false
}

func (controller *Controller) currentSnapshot() Snapshot {
	controller.mu.Lock()
	defer controller.mu.Unlock()
	return controller.snapshot
}

func (controller *Controller) transition(state State, candidate *Release, staged *StagedArtifact) {
	snapshot := controller.currentSnapshot()
	snapshot.State = state
	snapshot.Candidate = candidate
	snapshot.Staged = staged
	snapshot.LastError = nil
	_ = controller.save(snapshot)
}

func (controller *Controller) save(snapshot Snapshot) error {
	snapshot.CurrentVersion = controller.app.CurrentVersion
	snapshot.Channel = controller.app.Channel
	if err := controller.store.Save(context.Background(), snapshot); err != nil {
		return err
	}

	controller.mu.Lock()
	controller.snapshot = snapshot
	listeners := make([]chan Snapshot, 0, len(controller.listeners))
	for _, listener := range controller.listeners {
		listeners = append(listeners, listener)
	}
	controller.mu.Unlock()

	for _, listener := range listeners {
		select {
		case listener <- snapshot:
		default:
			select {
			case <-listener:
			default:
			}
			listener <- snapshot
		}
	}

	return nil
}

func (controller *Controller) fail(code string, err error) (Snapshot, error) {
	controller.logger.Error("update flow failed", "code", code, "error", err)

	snapshot := controller.currentSnapshot()
	snapshot.State = StateFailed
	snapshot.LastError = &ErrorInfo{
		Code:    code,
		Message: err.Error(),
	}
	if saveErr := controller.save(snapshot); saveErr != nil {
		return Snapshot{}, fmt.Errorf("%w: %w", err, saveErr)
	}
	return snapshot, err
}
