package main

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

	"github.com/dtoro/oikos/internal/config"
	"github.com/dtoro/oikos/internal/db"
	"github.com/dtoro/oikos/internal/httpapi"
	"github.com/dtoro/oikos/internal/knowledge"
	"github.com/dtoro/oikos/internal/notifier"
	"github.com/dtoro/oikos/internal/observability"
	"github.com/dtoro/oikos/internal/scheduler"
	"github.com/dtoro/oikos/internal/secrets"
	"github.com/dtoro/oikos/web"
	"github.com/jackc/pgx/v5"
)

// uiHandler serves the control-room SPA from assets embedded at build time
// (web/embed.go), with SPA fallback to index.html. Requests arrive as /ui/*;
// the /ui prefix is stripped to index into the embedded dist/ tree. Files are
// written via http.ServeContent (not http.FileServer) to avoid its
// index.html -> "./" canonical redirect, which loops for /ui/.
func uiHandler() http.Handler {
	dist, err := web.DistFS()
	if err != nil {
		slog.Warn("ui: embedded assets unavailable", "error", err)
		return http.NotFoundHandler()
	}
	serve := func(w http.ResponseWriter, r *http.Request, name string) bool {
		f, err := dist.Open(name)
		if err != nil {
			return false
		}
		defer f.Close()
		data, err := io.ReadAll(f)
		if err != nil {
			return false
		}
		http.ServeContent(w, r, name, time.Time{}, bytes.NewReader(data))
		return true
	}
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		name := strings.TrimPrefix(strings.TrimPrefix(r.URL.Path, "/ui"), "/")
		if name == "" {
			name = "index.html"
		}
		if serve(w, r, name) {
			return
		}
		// SPA fallback: serve index.html for unknown client-side routes.
		if serve(w, r, "index.html") {
			return
		}
		http.NotFound(w, r)
	})
}

var schedulerRunner = scheduler.RunnerForMain()
var notifierRunner = notifier.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)

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

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

	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 "notifier":
		runWithPool(ctx, cfg, "notifier", notifierRunner)
	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 notifierRunner(ctx, pool, cfg)

		slog.Info("all: starting api with scheduler + notifier in background")
		if err := httpapi.ListenAndServe(ctx, pool, cfg, uiHandler()); 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
  notifier   Run the notification service (Matrix alerts)
  all        Run all roles in one process (dev mode)
  secret     Secret management (Infisical)
  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
}

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"
	}

	// Ingest ontology seed
	ontoContent, err := os.ReadFile(seedsDir + "/ontology.yaml")
	if err != nil {
		return fmt.Errorf("read ontology seed: %w", err)
	}
	err = pool.SeedIngest(ctx, "ontology.yaml", ontoContent,
		func(ctx context.Context, tx pgx.Tx, data map[string]any) error {
			r, err := db.IngestOntologySeed(ctx, tx, data)
			if err != nil {
				return err
			}
			slog.Info("ontology ingested",
				"lifecycles", r.Lifecycles,
				"entity_types", r.EntityTypes,
				"relationship_types", r.RelationshipTypes)
			return nil
		})
	if err != nil {
		return err
	}

	// Ingest inventory seed
	invContent, err := os.ReadFile(seedsDir + "/inventory.yaml")
	if err != nil {
		return fmt.Errorf("read inventory seed: %w", err)
	}
	err = pool.SeedIngest(ctx, "inventory.yaml", invContent,
		func(ctx context.Context, tx pgx.Tx, data map[string]any) error {
			r, err := db.IngestInventorySeed(ctx, tx, data)
			if err != nil {
				return err
			}
			slog.Info("inventory ingested",
				"entities", r.Entities,
				"relationships", r.Relationships)
			return nil
		})
	if err != nil {
		return err
	}

	// Ingest policy seed
	polContent, err := os.ReadFile(seedsDir + "/policy.yaml")
	if err != nil {
		return fmt.Errorf("read policy seed: %w", err)
	}
	err = pool.SeedIngest(ctx, "policy.yaml", polContent,
		func(ctx context.Context, tx pgx.Tx, data map[string]any) error {
			r, err := db.IngestPolicySeed(ctx, tx, data)
			if err != nil {
				return err
			}
			slog.Info("policy ingested",
				"risk_classes", r.RiskClasses,
				"approval_rules", r.ApprovalRules,
				"autonomy_settings", r.AutonomySettings)
			return nil
		})
	if err != nil {
		return err
	}

	// Ingest knowledge seed (documents, investigations, runbooks)
	knContent, err := os.ReadFile(seedsDir + "/knowledge.yaml")
	if err != nil {
		if os.IsNotExist(err) {
			slog.Info("knowledge seed not found, skipping")
		} else {
			return fmt.Errorf("read knowledge seed: %w", err)
		}
	} else {
		err = pool.SeedIngest(ctx, "knowledge.yaml", knContent,
			func(ctx context.Context, tx pgx.Tx, data map[string]any) error {
				r, err := knowledge.Ingest(ctx, tx, data)
				if err != nil {
					return err
				}
				slog.Info("knowledge ingested",
					"documents", r.Documents,
					"investigations", r.Investigations,
					"runbooks", r.Runbooks)
				return nil
			})
		if err != nil {
			return err
		}
	}

	slog.Info("seed ingest complete")
	return nil
}

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)
	}

	err = httpapi.ListenAndServe(ctx, pool, cfg, uiHandler())
	if err == http.ErrServerClosed {
		return nil
	}
	return err
}

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 <list|migrate|export-sops>")
		os.Exit(1)
	}

	sub := os.Args[2]
	secretsDir := cfg.SecretsDir
	if secretsDir == "" {
		secretsDir = "archive/secrets-sops-backup"
	}
	sopsBackend := secrets.NewSOPSBackend(secretsDir)

	switch sub {
	case "list":
		keys, err := sopsBackend.List(ctx)
		if err != nil {
			slog.Error("secret list", "error", err)
			os.Exit(1)
		}
		for _, k := range keys {
			fmt.Println(k)
		}

	case "migrate":
		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"
		}
		if infCfg.SiteURL == "" {
			fmt.Fprintln(os.Stderr, "error: OIKOS_INFISICAL_SITE_URL not set")
			os.Exit(1)
		}

		infBackend := secrets.NewInfisicalBackend(infCfg)
		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")

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

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 := db.ExportToYAML(ctx, pool)
	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
}