package main

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

	"github.com/dtoro/oikos/internal/config"
	"github.com/dtoro/oikos/internal/core/app"
	"github.com/dtoro/oikos/internal/core/ports"
	"github.com/dtoro/oikos/internal/adapters/postgres"
	"github.com/dtoro/oikos/internal/adapters/remote"
	"github.com/dtoro/oikos/internal/adapters/ssh"
	"github.com/dtoro/oikos/internal/execworker"
	"github.com/dtoro/oikos/internal/httpapi"
	"github.com/dtoro/oikos/internal/observability"
	"github.com/dtoro/oikos/internal/scheduler"
	"github.com/dtoro/oikos/internal/secrets"
)

var schedulerRunner = scheduler.RunnerForMain()
var execWorkerRunner = execworker.RunnerForMain()

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

	role := os.Args[1]
	cfg := config.FromEnv()

	// Structured logging (slog)
	logger := observability.NewLogger(cfg.Debug)
	slog.SetDefault(logger)

	ctx, cancel := signal.NotifyContext(context.Background(),
		syscall.SIGTERM, syscall.SIGINT)
	defer cancel()

	// Resolve secrets from Infisical, overlaying env-derived config values.
	// If Infisical is not configured, env vars are used as-is (no change).
	sec := secrets.NewManagerFromConfig(
		cfg.InfisicalSiteURL,
		cfg.InfisicalClientID,
		cfg.InfisicalClientSecret,
		cfg.InfisicalProjectID,
		cfg.InfisicalEnv,
		cfg.SecretsDir,
	)
	if sec != nil {
		overlays := secrets.ConfigOverlays(map[string]func(string){
			"mcp_bearer-token":     func(v string) { cfg.MCPBearerToken = v },
			"api_token":            func(v string) { cfg.APIToken = v },
			"oidc_client-secret":   func(v string) { cfg.OIDCClientSecret = v },
		})
		n := secrets.OverlayConfig(ctx, sec, overlays)
		slog.Info("secrets resolved from Infisical", "count", n)

		secrets.VerifyExpectedSecrets(ctx, sec, []string{
			"approval_hmac-secret", "mcp_bearer-token",
			"api_token", "openrouter_api-key", "webhook_hmac-secret",
		})
	}

	slog.Info("starting oikos", "role", role, "config", cfg)

	switch role {
	case "migrate":
		if err := runMigrate(ctx, cfg); err != nil {
			slog.Error("migrate failed", "error", err)
			os.Exit(1)
		}
	case "seed":
		if err := runSeed(ctx, cfg); err != nil {
			slog.Error("seed failed", "error", err)
			os.Exit(1)
		}
	case "export":
		if err := runExport(ctx, cfg); err != nil {
			slog.Error("export failed", "error", err)
			os.Exit(1)
		}
	case "api":
		if err := runAPI(ctx, cfg); err != nil {
			slog.Error("api failed", "error", err)
			os.Exit(1)
		}
	case "scheduler":
		runWithPool(ctx, cfg, "scheduler", schedulerRunner)
	case "execution-worker":
		runWithPool(ctx, cfg, "execution-worker", execWorkerRunner)
	case "all":
		pool, err := db.New(ctx, cfg.DatabaseURL)
		if err != nil {
			slog.Error("connect db", "error", err)
			os.Exit(1)
		}
		defer pool.Close()

		if err := pool.Migrate(ctx); err != nil {
			slog.Error("migrate", "error", err)
			os.Exit(1)
		}

		go schedulerRunner(ctx, pool, cfg)
		go execWorkerRunner(ctx, pool, cfg)

		slog.Info("all: starting api with scheduler + execution-worker in background")
		svc := buildAPIServices(pool)
		if err := httpapi.ListenAndServe(ctx, pool, cfg, svc.entities, svc.entityRepo, svc.readModels, svc.relService, svc.provisioning, svc.seeds); err != nil {
			slog.Error("api failed", "error", err)
			os.Exit(1)
		}
	case "version":
		fmt.Println("oikos dev (Phase 1)")
	case "secret":
		runSecret(ctx, cfg)
	case "help", "--help", "-h":
		usage()
	default:
		fmt.Fprintf(os.Stderr, "unknown role: %s\n", role)
		usage()
		os.Exit(1)
	}
}

func usage() {
	fmt.Println(`oikos — the homelab OS

Usage: oikos <role> [flags]

Roles:
  migrate    Run database migrations (forward-only, idempotent)
  seed       Ingest seed YAML files into the database
  export     Export DB state back to seed YAMLs (DR / version control)
  api        Run the REST + MCP API server
  scheduler  Run the observe loop
  all        Run all roles in one process (dev mode)
  secret     Secret management (Infisical: get, set, list, verify, audit, migrate, export-sops)
  knowledge  Convert wiki to knowledge seed (one-shot)
  version    Print version info

The operator interface is Nomos (MCP agent) — no CLI needed.
Environment:
  OIKOS_DATABASE_URL    Postgres connection string
  OIKOS_API_LISTEN      API listen address (default :8090)
  OIKOS_ENV             Environment (dev, prod)
  OIKOS_DEBUG           Enable verbose logging (true/1)
  OIKOS_SEEDS_DIR       Path to seeds directory (default: seeds)
  OIKOS_MCP_BEARER_TOKEN  Shared secret for MCP auth`)
}

func runMigrate(ctx context.Context, cfg config.Config) error {
	pool, err := db.New(ctx, cfg.DatabaseURL)
	if err != nil {
		return err
	}
	defer pool.Close()

	slog.Info("running migrations")
	if err := pool.Migrate(ctx); err != nil {
		return err
	}
	slog.Info("migrations complete")
	return nil
}

// runSeed ingests the seed YAMLs via SeedService (Phase 7): the CLI is
// an adapter over the service; ordering, idempotency (content-hash
// no-op), and per-file logging live in the service/repository pair.
func runSeed(ctx context.Context, cfg config.Config) error {
	pool, err := db.New(ctx, cfg.DatabaseURL)
	if err != nil {
		return err
	}
	defer pool.Close()

	// Ensure migrations are applied first
	if err := pool.Migrate(ctx); err != nil {
		return fmt.Errorf("migrations: %w", err)
	}

	seedsDir := cfg.SeedsDir
	if seedsDir == "" {
		seedsDir = "seeds"
	}

	return app.NewSeedService(db.NewSeedRepo(pool)).Ingest(ctx, seedsDir)
}

func runAPI(ctx context.Context, cfg config.Config) error {
	pool, err := db.New(ctx, cfg.DatabaseURL)
	if err != nil {
		return err
	}
	defer pool.Close()

	if err := pool.Migrate(ctx); err != nil {
		return fmt.Errorf("migrations: %w", err)
	}

	svc := buildAPIServices(pool)
	err = httpapi.ListenAndServe(ctx, pool, cfg, svc.entities, svc.entityRepo, svc.readModels, svc.relService, svc.provisioning, svc.seeds)
	if err == http.ErrServerClosed {
		return nil
	}
	return err
}

// apiServices is the composition-root dependency set for the API surface
// (ADR 0016, plan §3.5): repositories, the ssh executor and provisioner,
// the target resolver, and the use-case services built on them.
type apiServices struct {
	entities     *app.EntityService
	entityRepo   *db.EntityRepo
	readModels   ports.ReadModels
	relService   *app.RelationshipService
	provisioning *app.ProvisioningService
	seeds        *app.SeedService
}

func buildAPIServices(pool *db.Pool) apiServices {
	entityRepo := db.NewEntityRepo(pool)
	onto := db.NewOntologyRepo(pool, time.Minute)
	readModels := db.NewEntityReader(pool)
	entities := app.NewEntityService(entityRepo, onto)
	relService := app.NewRelationshipService(db.NewRelRepo(pool), onto)

	executor := ssh.NewExecutor(ssh.FileSignerSource(), 5*time.Minute)
	provisioner := ssh.NewProvisioner(executor)
	resolver := remote.NewResolver(pool)
	provisioning := app.NewProvisioningService(provisioner, resolver, entityRepo, db.NewRelRepo(pool))

	seeds := app.NewSeedService(db.NewSeedRepo(pool))

	return apiServices{
		entities:     entities,
		entityRepo:   entityRepo,
		readModels:   readModels,
		relService:   relService,
		provisioning: provisioning,
		seeds:        seeds,
	}
}

func runWithPool(ctx context.Context, cfg config.Config, name string, fn func(context.Context, *db.Pool, config.Config)) {
	pool, err := db.New(ctx, cfg.DatabaseURL)
	if err != nil {
		slog.Error("connect db", "role", name, "error", err)
		os.Exit(1)
	}
	defer pool.Close()

	if err := pool.Migrate(ctx); err != nil {
		slog.Error("migrate", "role", name, "error", err)
		os.Exit(1)
	}

	fn(ctx, pool, cfg)
}

func runSecret(ctx context.Context, cfg config.Config) {
	if len(os.Args) < 3 {
		fmt.Fprintln(os.Stderr, "usage: oikos secret <get|set|list|verify|audit|migrate|export-sops>")
		os.Exit(1)
	}

	sub := os.Args[2]

	// get/set/list go through SecretsService (Phase 7); the CLI is an
	// adapter over the service. verify/audit/migrate/export-sops remain
	// backend-specific diagnostics.
	switch sub {
	case "get":
		if len(os.Args) < 4 {
			fmt.Fprintln(os.Stderr, "usage: oikos secret get <key>")
			os.Exit(1)
		}
		key := os.Args[3]
		svc := app.NewSecretsService(newInfisicalBackendOrFail(cfg))
		val, err := svc.Get(ctx, key)
		if err != nil {
			slog.Error("secret get", "key", key, "error", err)
			os.Exit(1)
		}
		fmt.Println(val)

	case "set":
		if len(os.Args) < 5 {
			fmt.Fprintln(os.Stderr, "usage: oikos secret set <key> <value>")
			os.Exit(1)
		}
		key := os.Args[3]
		value := os.Args[4]
		svc := app.NewSecretsService(newInfisicalBackendOrFail(cfg))
		if err := svc.Set(ctx, key, value); err != nil {
			slog.Error("secret set", "key", key, "error", err)
			os.Exit(1)
		}
		fmt.Printf("stored: %s\n", key)

	case "list":
		svc := app.NewSecretsService(newInfisicalBackendOrFail(cfg))
		keys, err := svc.List(ctx)
		if err != nil {
			slog.Error("secret list", "error", err)
			os.Exit(1)
		}
		for _, k := range keys {
			fmt.Println(k)
		}

	case "verify":
		runSecretVerify(ctx, cfg)

	case "audit":
		runSecretAudit(ctx, cfg)

	case "migrate", "export-sops":
		runSecretLegacy(ctx, cfg, sub)

	default:
		fmt.Fprintf(os.Stderr, "unknown secret command: %s\n", sub)
		os.Exit(1)
	}
}

// expectedSecrets is the set of keys that should exist in Infisical
// for a fully-migrated deployment.
var expectedSecrets = []string{
	"mcp_bearer-token",
	"api_token",
	"openrouter_api-key",
	"webhook_hmac-secret",
}

// runSecretVerify checks that all expected secrets are present in Infisical.
func runSecretVerify(ctx context.Context, cfg config.Config) {
	backend := newInfisicalBackendOrFail(cfg)
	keys, err := backend.List(ctx)
	if err != nil {
		slog.Error("verify: list", "error", err)
		os.Exit(1)
	}
	keySet := make(map[string]struct{}, len(keys))
	for _, k := range keys {
		keySet[k] = struct{}{}
	}

	missing := 0
	for _, exp := range expectedSecrets {
		if _, ok := keySet[exp]; !ok {
			fmt.Printf("MISSING: %s\n", exp)
			missing++
		} else {
			fmt.Printf("OK:      %s\n", exp)
		}
	}

	fmt.Printf("\n%d/%d present, %d missing\n", len(expectedSecrets)-missing, len(expectedSecrets), missing)
	if missing > 0 {
		os.Exit(1)
	}
}

// runSecretAudit resolves all expected secrets from Infisical and prints
// a diff against current env-derived values. Values are truncated for safety.
func runSecretAudit(ctx context.Context, cfg config.Config) {
	backend := newInfisicalBackendOrFail(cfg)

	envValues := map[string]string{
		"mcp_bearer-token":     cfg.MCPBearerToken,
		"api_token":            cfg.APIToken,
		"oidc_client-secret":   cfg.OIDCClientSecret,
	}

	fmt.Println("key                           infisical  env        status")
	fmt.Println(strings.Repeat("-", 72))

	for _, key := range expectedSecrets {
		infVal, infErr := backend.Get(ctx, key)
		envVal := envValues[key]

		if infErr != nil {
			fmt.Printf("%-29s ERROR      %-10s NOT-IN-INFISICAL\n", key, trunc(envVal, 8))
			continue
		}
		if envVal == "" {
			fmt.Printf("%-29s %-10s (empty)   INFISICAL-ONLY\n", key, trunc(infVal, 8))
			continue
		}
		if infVal == envVal {
			fmt.Printf("%-29s %-10s %-10s MATCH\n", key, trunc(infVal, 8), trunc(envVal, 8))
		} else {
			fmt.Printf("%-29s %-10s %-10s DRIFT\n", key, trunc(infVal, 8), trunc(envVal, 8))
		}
	}
}

func trunc(s string, n int) string {
	if len(s) <= n {
		return s
	}
	if n > 1 {
		return s[:n-1] + "…"
	}
	return s[:n]
}

// newInfisicalBackendOrFail creates an Infisical backend from config or exits.
func newInfisicalBackendOrFail(cfg config.Config) *secrets.InfisicalBackend {
	if cfg.InfisicalSiteURL == "" {
		fmt.Fprintln(os.Stderr, "error: OIKOS_INFISICAL_SITE_URL not set")
		os.Exit(1)
	}
	infCfg := secrets.InfisicalConfig{
		SiteURL:      cfg.InfisicalSiteURL,
		ClientID:     cfg.InfisicalClientID,
		ClientSecret: cfg.InfisicalClientSecret,
		ProjectID:    cfg.InfisicalProjectID,
		SecretPath:   "/",
		Env:          cfg.InfisicalEnv,
	}
	if infCfg.Env == "" {
		infCfg.Env = "dev"
	}
	return secrets.NewInfisicalBackend(infCfg)
}

// runSecretLegacy handles SOPS-only commands (migrate, export-sops).
func runSecretLegacy(ctx context.Context, cfg config.Config, sub string) {
	secretsDir := cfg.SecretsDir
	if secretsDir == "" {
		secretsDir = "archive/secrets-sops-backup"
	}
	sopsBackend := secrets.NewSOPSBackend(secretsDir)

	switch sub {
	case "migrate":
		infBackend := newInfisicalBackendOrFail(cfg)
		keys, err := sopsBackend.List(ctx)
		if err != nil {
			slog.Error("migrate: read sops", "error", err)
			os.Exit(1)
		}

		migrated, failed := 0, 0
		for _, key := range keys {
			val, err := sopsBackend.Get(ctx, key)
			if err != nil {
				slog.Warn("migrate: skip", "key", key, "error", err)
				failed++
				continue
			}
			sKey := strings.ReplaceAll(key, "/", "_")
			if err := infBackend.Set(ctx, sKey, val); err != nil {
				slog.Warn("migrate: push failed", "key", sKey, "error", err)
				failed++
				continue
			}
			migrated++
			fmt.Printf("migrated: %s → %s\n", key, sKey)
		}
		fmt.Printf("migrated %d, failed %d\n", migrated, failed)

	case "export-sops":
		keys, err := sopsBackend.List(ctx)
		if err != nil {
			slog.Error("export-sops: read", "error", err)
			os.Exit(1)
		}
		fmt.Printf("# SOPS DR fallback — %d keys — export date: now\n", len(keys))
		fmt.Printf("# Store in a secure offline location.\n\n")
		for _, k := range keys {
			fmt.Printf("%s: <sops-encrypted>\n", k)
		}
		fmt.Printf("\n# To restore: sops -d secrets/*.yaml\n")
	}
}

// runExport regenerates the seed YAMLs from the DB via SeedService and
// writes them into the seeds dir (DR / version control).
func runExport(ctx context.Context, cfg config.Config) error {
	pool, err := db.New(ctx, cfg.DatabaseURL)
	if err != nil {
		return err
	}
	defer pool.Close()

	exports, err := app.NewSeedService(db.NewSeedRepo(pool)).Export(ctx)
	if err != nil {
		return err
	}

	for name, content := range exports {
		path := cfg.SeedsDir + "/" + name
		if err := os.WriteFile(path, content, 0644); err != nil {
			return fmt.Errorf("write %s: %w", path, err)
		}
		slog.Info("exported", "file", path, "bytes", len(content))
	}
	return nil
}