package main

import (
	"context"
	"fmt"
	"log/slog"
	"time"

	"gitea.d-ma.be/mathias/tapir/internal/adapters/llm"
	"gitea.d-ma.be/mathias/tapir/internal/adapters/store"
	"gitea.d-ma.be/mathias/tapir/internal/adapters/summarizer"
	"gitea.d-ma.be/mathias/tapir/internal/adapters/youtube"
	"gitea.d-ma.be/mathias/tapir/internal/config"
	"gitea.d-ma.be/mathias/tapir/internal/ports"
	"gitea.d-ma.be/mathias/tapir/internal/runner"
	"gitea.d-ma.be/mathias/tapir/internal/usecase"
	"gitea.d-ma.be/mathias/tapir/internal/web"
)

// buildUserRunner constructs a runner.Runner for one user, reusing the same
// engine wiring as buildProcessor but bound to that user's own YouTube refresh
// token (web.YouTubeTokenRef(userID)) — the Stage-1 per-tenant ref, not the
// Stage-0 single ref. It returns an error (not nil) when the global config can't
// support live summarization (gateway, YouTube client creds, secrets file), so
// the scheduler can skip that user gracefully. A user who simply hasn't connected
// YouTube yet builds fine here; their token ref fails to resolve at RunOnce time,
// surfacing as a per-user error the scheduler logs and skips.
func buildUserRunner(cfg config.Config, st *store.Store, secretStore ports.SecretStore, userID string, log *slog.Logger) (*runner.Runner, error) {
	if cfg.GatewayURL == "" || cfg.YTClientID == "" || cfg.YTClientSecret == "" || cfg.SecretsFile == "" {
		return nil, fmt.Errorf("buildUserRunner: incomplete summarization config (gateway, youtube credentials, secrets file)")
	}

	src := youtube.New(youtube.Config{
		ClientID:           cfg.YTClientID,
		ClientSecret:       cfg.YTClientSecret,
		TokenSecretRef:     web.YouTubeTokenRef(userID),
		PreferredLanguages: []string{"en"},
	}, secretStore)

	primary := summarizer.Endpoint{
		Client:   llm.New(cfg.GatewayURL, cfg.GatewayKey, cfg.SummarizerModel, cfg.SummarizerTimeout),
		Provider: "local",
		Model:    cfg.SummarizerModel,
	}
	engine := usecase.NewEngine(src, summarizer.New(primary, nil), st)

	return runner.New(src, st, engine, userID, log,
		runner.WithBackoff(cfg.FetchBackoff),
		runner.WithAutoWindow(cfg.AutoSummarizeWindow)), nil
}

// userLister enumerates every registered user. *store.Store satisfies it via
// ListAllUsers. A small local interface keeps the scheduler testable with a fake.
type userLister interface {
	ListAllUsers(ctx context.Context) ([]store.UserIdentity, error)
}

// runDiscoveryPass runs one discovery pass for every user. runUser performs a
// single user's pass (production: build a runner and RunOnce). Per-user failures
// — including a buildUserRunner error or a RunOnce error — are logged and skipped
// so one bad user, channel, or video never aborts the others (ADR-018 failure
// isolation). Returns the stats summed across users.
func runDiscoveryPass(
	ctx context.Context,
	lister userLister,
	runUser func(context.Context, string) (runner.Stats, error),
	log *slog.Logger,
) runner.Stats {
	users, err := lister.ListAllUsers(ctx)
	if err != nil {
		log.Error("scheduler: list users failed", "err", err)
		return runner.Stats{}
	}

	log.Info("scheduler: starting discovery pass", "users", len(users))
	var total runner.Stats
	for _, u := range users {
		if ctx.Err() != nil {
			break // shutting down: stop enumerating
		}
		stats, err := runUser(ctx, u.UserID)
		total = sumStats(total, stats)
		if err != nil {
			log.Warn("scheduler: user discovery pass had errors", "user", u.UserID, "err", err)
		}
	}
	log.Info("scheduler: pass complete",
		"candidates", total.Candidates, "summarized", total.Summarized,
		"skipped_seen", total.SkippedSeen, "skipped_no_text", total.SkippedNoText,
		"skipped_manual", total.SkippedManual, "skipped_too_old", total.SkippedTooOld,
		"skipped_rate_limited", total.SkippedRateLimited,
		"channel_unavailable", total.ChannelUnavailable, "errors", total.Errors)
	return total
}

// runScheduler runs a discovery pass on startup, then once every interval until
// ctx is cancelled (pod SIGTERM exits the loop cleanly). A non-positive interval
// disables scheduling entirely (no startup pass) so dev/tests never auto-fetch.
// It reuses the existing runner.Runner via runUser — the only new behaviour over
// runner.Loop is iterating all users per tick (ADR-018).
func runScheduler(
	ctx context.Context,
	interval time.Duration,
	lister userLister,
	runUser func(context.Context, string) (runner.Stats, error),
	log *slog.Logger,
) {
	if interval <= 0 {
		return // disabled
	}

	runDiscoveryPass(ctx, lister, runUser, log)

	ticker := time.NewTicker(interval)
	defer ticker.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			runDiscoveryPass(ctx, lister, runUser, log)
		}
	}
}

// sumStats adds two passes' stats field-wise, so runDiscoveryPass can report a
// per-tick aggregate across all users.
func sumStats(a, b runner.Stats) runner.Stats {
	return runner.Stats{
		Candidates:         a.Candidates + b.Candidates,
		Summarized:         a.Summarized + b.Summarized,
		SkippedSeen:        a.SkippedSeen + b.SkippedSeen,
		SkippedNoText:      a.SkippedNoText + b.SkippedNoText,
		SkippedManual:      a.SkippedManual + b.SkippedManual,
		SkippedTooOld:      a.SkippedTooOld + b.SkippedTooOld,
		SkippedRateLimited: a.SkippedRateLimited + b.SkippedRateLimited,
		ChannelUnavailable: a.ChannelUnavailable + b.ChannelUnavailable,
		Errors:             a.Errors + b.Errors,
	}
}
