// Command tapir is the service entrypoint and CLI. Subcommands are dispatched
// off os.Args[1]; each lives in its own file (list.go, show.go, …). It serves
// the Stage-0 demo:
//
//	tapir auth   mint a YouTube refresh token interactively (one-time setup)
//	tapir run    detect new videos, summarize them, deliver to the store
//	tapir list   list stored summaries, recent first
//	tapir show   show one stored summary in full
//
// Standalone-vs-homelab is a wiring choice (ADR-003): every dependency is
// resolved from config (internal/config) and the SecretStore port, so live
// credentials plug in at runtime without code changes.
package main

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"net/http"
	"os"
	"os/signal"
	"time"

	"gitea.d-ma.be/mathias/tapir/internal/adapters/llm"
	"gitea.d-ma.be/mathias/tapir/internal/adapters/secrets"
	"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/auth"
	"gitea.d-ma.be/mathias/tapir/internal/config"
	"gitea.d-ma.be/mathias/tapir/internal/runner"
	"gitea.d-ma.be/mathias/tapir/internal/usecase"
	"gitea.d-ma.be/mathias/tapir/internal/web"
	"gitea.d-ma.be/mathias/tapir/internal/web/oidc"
)

func main() {
	if len(os.Args) < 2 {
		usage()
		os.Exit(2)
	}

	log := slog.New(slog.NewTextHandler(os.Stderr, nil))
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
	defer stop()

	var err error
	switch os.Args[1] {
	case "list":
		err = runList(ctx, os.Args[2:])
	case "show":
		err = runShow(ctx, os.Args[2:])
	case "auth":
		err = cmdAuth(ctx, log)
	case "run":
		err = cmdRun(ctx, log)
	case "serve":
		err = cmdServe(ctx, log)
	default:
		usage()
		os.Exit(2)
	}

	if err != nil {
		log.Error("command failed", "command", os.Args[1], "err", err)
		os.Exit(1)
	}
}

func usage() {
	fmt.Fprint(os.Stderr, `tapir — summarize new videos from your YouTube subscriptions

usage:
  tapir auth              one-time: authorize YouTube and store a refresh token
  tapir run               detect new videos, summarize, deliver to your store
  tapir serve             run the web UI (read summaries, record watch/skip/save)
  tapir list [-limit N]   list stored summaries, recent first
  tapir show <video-id>   show one summary in full

configuration is via TAPIR_* environment variables (see .env.example).
`)
}

// cmdAuth runs the interactive OAuth flow and persists the refresh token.
func cmdAuth(ctx context.Context, log *slog.Logger) error {
	cfg, err := config.Load()
	if err != nil {
		return err
	}
	if err := cfg.ValidateForAuth(); err != nil {
		return err
	}

	secretStore := secrets.NewFileStore(cfg.SecretsFile)
	authCfg := auth.Config{
		ClientID:     cfg.YTClientID,
		ClientSecret: cfg.YTClientSecret,
		RedirectURL:  "http://" + cfg.OAuthRedirectAddr + "/callback",
		TokenRef:     cfg.YTTokenRef,
	}
	log.Info("starting youtube authorization", "redirect", authCfg.RedirectURL, "token_ref", cfg.YTTokenRef)
	return auth.Run(ctx, authCfg, secretStore, os.Stdout)
}

// cmdRun wires the adapters and engine, then runs the watch→summarize→deliver
// loop. With TAPIR_POLL_INTERVAL unset it performs a single pass.
func cmdRun(ctx context.Context, log *slog.Logger) error {
	cfg, err := config.Load()
	if err != nil {
		return err
	}
	if err := cfg.ValidateForRun(); err != nil {
		return err
	}

	st, err := store.New(ctx, cfg.DBDSN)
	if err != nil {
		return err
	}
	defer st.Close()

	secretStore := secrets.NewFileStore(cfg.SecretsFile)
	src := youtube.New(youtube.Config{
		ClientID:           cfg.YTClientID,
		ClientSecret:       cfg.YTClientSecret,
		TokenSecretRef:     cfg.YTTokenRef,
		PreferredLanguages: []string{"en"},
	}, secretStore)

	// Local Primary only; no BYO fallback for the demo (fallback nil).
	primary := summarizer.Endpoint{
		Client:   llm.New(cfg.GatewayURL, cfg.GatewayKey, cfg.SummarizerModel, cfg.SummarizerTimeout),
		Provider: "local",
		Model:    cfg.SummarizerModel,
	}
	sum := summarizer.New(primary, nil)

	engine := usecase.NewEngine(src, sum, st)
	r := runner.New(src, st, engine, cfg.UserID, log)

	log.Info("starting run", "user", cfg.UserID, "model", cfg.SummarizerModel,
		"gateway", cfg.GatewayURL, "poll_interval", cfg.PollInterval)
	return r.Loop(ctx, cfg.PollInterval)
}

// cmdServe runs the Stage-0 web UI: the summary reader over the existing store
// (ADR-003 — a new transport, not new core). Auth is the StubAuth allow-all seam
// keyed to the configured user; the Conductor swaps in oidc.DexAuth at merge —
// the only line that changes is the `authn` assignment below.
func cmdServe(ctx context.Context, log *slog.Logger) error {
	cfg, err := config.Load()
	if err != nil {
		return err
	}
	if err := cfg.ValidateForServe(); err != nil {
		return err
	}

	st, err := store.New(ctx, cfg.DBDSN)
	if err != nil {
		return err
	}
	defer st.Close()

	// Auth seam (handlers depend on web.Auth only). With Dex configured
	// (TAPIR_OIDC_ISSUER set) serve uses real OIDC login with single-user
	// allowlist authz (ADR-011); otherwise it falls back to the allow-all
	// StubAuth for local dev — never expose StubAuth publicly.
	var authn web.Auth
	if cfg.DexConfigured() {
		authn, err = oidc.New(ctx, oidc.Config{
			Issuer:         cfg.OIDCIssuer,
			ClientID:       cfg.DexClientID,
			ClientSecret:   cfg.DexClientSecret,
			RedirectURL:    cfg.OIDCRedirectURL,
			SessionSecret:  cfg.SessionSecret,
			AllowedSubject: cfg.AllowedSubject,
		})
		if err != nil {
			return fmt.Errorf("dex oidc: %w", err)
		}
		log.Info("web auth: dex oidc", "issuer", cfg.OIDCIssuer)
	} else {
		authn = web.StubAuth{U: web.User{Subject: cfg.UserID}}
		log.Warn("web auth: STUB allow-all (no TAPIR_OIDC_ISSUER) — local dev only, do not expose")
	}

	app := &web.App{Store: st, Auth: authn, UserID: cfg.UserID, Log: log}
	srv := &http.Server{
		Addr:              cfg.HTTPAddr,
		Handler:           app.Router(),
		ReadHeaderTimeout: 10 * time.Second,
	}

	// Graceful shutdown on signal: stop accepting, drain in-flight requests.
	go func() {
		<-ctx.Done()
		shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
		defer cancel()
		_ = srv.Shutdown(shutdownCtx)
	}()

	log.Info("serving web ui", "addr", cfg.HTTPAddr, "user", cfg.UserID)
	if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
		return fmt.Errorf("serve: %w", err)
	}
	return nil
}
