package main

import (
	"context"
	"embed"
	"flag"
	"fmt"
	"io/fs"
	"log"
	"net/http"
	"os"
	"os/signal"
	"path/filepath"
	"strings"
	"syscall"
	"time"

	"fn-registry/functions/infra"
)

//go:embed all:dist
var frontendDist embed.FS

// Version is the build-time identifier of the kanban app. It is injected
// from app.md's `version:` field via -ldflags "-X main.Version=..." by run.sh
// (and by docker/CI). Defaults to "dev" for hand-built binaries.
var Version = "dev"

func main() {
	// Subcommand `kanban mcp` runs as MCP server over stdio (spawned by claude -p).
	if len(os.Args) > 1 && os.Args[1] == "mcp" {
		if err := runMCPServer(os.Args[2:]); err != nil {
			fmt.Fprintf(os.Stderr, "kanban mcp: %v\n", err)
			os.Exit(1)
		}
		return
	}

	// Subcommand `kanban mint-token` issues an HTTP MCP bearer token for a user.
	if len(os.Args) > 1 && os.Args[1] == "mint-token" {
		if err := runMintToken(os.Args[2:]); err != nil {
			fmt.Fprintf(os.Stderr, "kanban mint-token: %v\n", err)
			os.Exit(1)
		}
		return
	}

	// Subcommand `kanban seed-jira-data` provisions the Jira push module
	// scoped to project DATA + board 33 using pass-stored credentials.
	if len(os.Args) > 1 && os.Args[1] == "seed-jira-data" {
		if err := runSeedJiraData(os.Args[2:]); err != nil {
			fmt.Fprintf(os.Stderr, "kanban seed-jira-data: %v\n", err)
			os.Exit(1)
		}
		return
	}

	// Subcommand `kanban backfill-jira` mirrors every active kanban card that
	// is not yet linked to a Jira issue into Jira, in batches.
	if len(os.Args) > 1 && os.Args[1] == "backfill-jira" {
		if err := runBackfillJira(os.Args[2:]); err != nil {
			fmt.Fprintf(os.Stderr, "kanban backfill-jira: %v\n", err)
			os.Exit(1)
		}
		return
	}

	flags := flag.NewFlagSet("kanban", flag.ExitOnError)
	port := flags.Int("port", 8095, "HTTP port")
	dbPath := flags.String("db", "operations.db", "SQLite database path")
	initialAdmin := flags.String("initial-admin", os.Getenv("KANBAN_INITIAL_ADMIN"), "Bootstrap admin in user:pass form (only if no users yet)")
	flagsPath := flags.String("flags", "dev/feature_flags.json", "Feature flags JSON path (missing file → all disabled)")
	flags.Parse(os.Args[1:])

	featureFlags, err := loadFeatureFlags(*flagsPath)
	if err != nil {
		log.Fatalf("load feature flags: %v", err)
	}
	for name, fl := range featureFlags.Flags {
		log.Printf("feature flag %q enabled=%v", name, fl.Enabled)
	}

	db, err := openDB(*dbPath)
	if err != nil {
		log.Fatalf("open db: %v", err)
	}
	defer db.Close()

	bootstrapAdmin(db, *initialAdmin)
	startSessionCleanup(db)

	internalToken := os.Getenv("KANBAN_INTERNAL_TOKEN")
	if internalToken == "" {
		internalToken = generateInternalToken()
	}

	wd := chatWorkdir(*dbPath)
	logger := newChatLogger(filepath.Join(wd, "chat.log"))
	log.Printf("chat tool log: %s", logger.path)
	hub := NewEventHub()
	dispatcher := NewDispatcher(db, hub)
	dispatcher.Start()
	defer dispatcher.Stop()
	mux := infra.HTTPRouter(apiRoutes(db, wd, logger, internalToken, &featureFlags, hub, dispatcher))

	mux.Handle("/mcp", mcpHTTPHandler(db))

	feHandler := frontendHandler()
	if feHandler != nil {
		mux.Handle("/", feHandler)
		log.Printf("serving frontend from embedded dist/")
	} else {
		log.Printf("no frontend build found, API-only mode")
	}

	authMW := infra.HTTPSessionCookieMiddleware(infra.SessionCookieConfig{
		DB:         db.conn,
		CookieName: cookieName,
		SkipPaths:  []string{"/api/auth/", "/api/tool/", "/api/flags", "/api/version", "/health", "/assets/", "/index.html"},
		UserCtxKey: userCtxKey,
	})

	chain := infra.HTTPMiddlewareChain(
		infra.HTTPLoggerMiddleware(os.Stdout),
		infra.HTTPCORSMiddleware([]string{"*"}, []string{"GET", "POST", "PATCH", "DELETE", "OPTIONS"}),
		apiOnlyAuth(authMW),
	)
	handler := chain(mux)

	addr := fmt.Sprintf(":%d", *port)
	log.Printf("kanban server starting on http://0.0.0.0%s", addr)
	log.Printf("database: %s", *dbPath)

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

	if err := infra.HTTPServe(addr, handler, ctx); err != nil {
		log.Fatalf("server: %v", err)
	}
}

// apiOnlyAuth applies auth middleware only to /api/* paths so the SPA shell
// can be served without a session (the SPA itself handles login UI).
func apiOnlyAuth(mw infra.Middleware) infra.Middleware {
	return func(next http.Handler) http.Handler {
		gated := mw(next)
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if strings.HasPrefix(r.URL.Path, "/api/") {
				gated.ServeHTTP(w, r)
				return
			}
			next.ServeHTTP(w, r)
		})
	}
}

func bootstrapAdmin(db *DB, spec string) {
	spec = strings.TrimSpace(spec)
	if spec == "" {
		return
	}
	count, err := db.CountUsers()
	if err != nil {
		log.Printf("bootstrap admin: count users: %v", err)
		return
	}
	if count > 0 {
		return
	}
	parts := strings.SplitN(spec, ":", 2)
	if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
		log.Printf("bootstrap admin: invalid spec, expected user:pass")
		return
	}
	u, err := db.CreateUser(parts[0], parts[1], parts[0])
	if err != nil {
		log.Printf("bootstrap admin: %v", err)
		return
	}
	log.Printf("bootstrap admin: created user %q", u.Username)
}

func startSessionCleanup(db *DB) {
	go func() {
		t := time.NewTicker(1 * time.Hour)
		defer t.Stop()
		for range t.C {
			if n, err := infra.SessionCleanup(db.conn); err != nil {
				log.Printf("session cleanup: %v", err)
			} else if n > 0 {
				log.Printf("session cleanup: purged %d expired", n)
			}
		}
	}()
}

func frontendHandler() http.Handler {
	sub, err := fs.Sub(frontendDist, "dist")
	if err != nil {
		return nil
	}
	entries, _ := fs.ReadDir(sub, ".")
	if len(entries) == 0 {
		return nil
	}
	return cacheHeadersMiddleware(infra.SPAHandler(sub, "index.html"))
}

// cacheHeadersMiddleware ensures the SPA shell is never cached while the
// hashed assets (which are content-addressed by Vite) are cached for a long
// time. Without this, browsers happily reuse an old index.html — pinned to a
// stale /assets/index-<hash>.js URL — and never pick up new releases.
//
// Policy:
//
//	/assets/*  →  public, max-age=1y, immutable  (filename changes per build)
//	everything else  →  no-store, must-revalidate (forces revalidation on every
//	                    navigation so the latest hash is always discovered)
func cacheHeadersMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if strings.HasPrefix(r.URL.Path, "/assets/") {
			w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
		} else {
			w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate")
			w.Header().Set("Pragma", "no-cache")
			w.Header().Set("Expires", "0")
		}
		next.ServeHTTP(w, r)
	})
}
