package main

import (
	"context"
	"database/sql"
	"flag"
	"fmt"
	"log/slog"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	_ "github.com/jackc/pgx/v5/stdlib"
	"golang.org/x/crypto/bcrypt"
	"golang.org/x/term"

	"git.soup.land/soup/sxgo/ssr"
	"lookbook/internal/data/admin"
	"lookbook/internal/data/session"
	"lookbook/internal/handlers"
	"lookbook/internal/middleware"
	"lookbook/internal/migrations"
	"lookbook/internal/static"
)

const defaultAddr = ":8080"

func main() {
	migrate := flag.Bool("migrate", false, "run database migrations and exit")
	rollback := flag.Bool("rollback", false, "roll back migrations and exit (one step by default)")
	rollbackTarget := flag.Int64("to", -1, "target version for rollback when using -rollback")
	dbURLFlag := flag.String("db-url", "", "database connection URL")
	flag.Parse()

	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
		Level: slog.LevelInfo,
	}))

	dbURL := *dbURLFlag
	if dbURL == "" {
		dbURL = os.Getenv("DATABASE_URL")
	}
	if dbURL == "" {
		dbURL = migrations.DefaultURL
	}

	switch {
	case *migrate && *rollback:
		logger.Error("choose either -migrate or -rollback, not both")
		os.Exit(1)
	case *migrate:
		if err := migrations.Up(context.Background(), dbURL, logger); err != nil {
			logger.Error("migration failed", slog.Any("err", err))
			os.Exit(1)
		}
		return
	case *rollback:
		if err := migrations.Down(context.Background(), dbURL, *rollbackTarget, logger); err != nil {
			logger.Error("rollback failed", slog.Any("err", err))
			os.Exit(1)
		}
		return
	}

	// Check for subcommand
	args := flag.Args()
	if len(args) < 1 {
		printUsage()
		os.Exit(1)
	}

	mode := args[0]
	switch mode {
	case "web":
		runWebServer(dbURL, logger)
	case "set-password":
		runSetPassword(dbURL, logger)
	default:
		logger.Error("unknown mode", slog.String("mode", mode))
		printUsage()
		os.Exit(1)
	}
}

func printUsage() {
	fmt.Fprintln(os.Stderr, "Usage:")
	fmt.Fprintln(os.Stderr, "  lookbook web           - Run web server")
	fmt.Fprintln(os.Stderr, "  lookbook set-password  - Set/reset admin password")
	fmt.Fprintln(os.Stderr, "")
	fmt.Fprintln(os.Stderr, "Flags:")
	fmt.Fprintln(os.Stderr, "  -migrate               Run database migrations")
	fmt.Fprintln(os.Stderr, "  -rollback              Roll back migrations")
	fmt.Fprintln(os.Stderr, "  -to <version>          Target version for rollback")
	fmt.Fprintln(os.Stderr, "  -db-url <url>          Database connection URL")
}

func runWebServer(dbURL string, logger *slog.Logger) {
	// Check for pending migrations
	pending, err := migrations.CheckPending(context.Background(), dbURL, logger)
	if err != nil {
		logger.Warn("could not check migration status", slog.Any("err", err))
	} else if pending > 0 {
		logger.Warn("database has pending migrations",
			slog.Int("pending", pending),
			slog.String("hint", "run 'make migrate' to apply"))
	}

	db, err := sql.Open("pgx", dbURL)
	if err != nil {
		logger.Error("failed to open database", slog.Any("err", err))
		os.Exit(1)
	}
	defer db.Close()

	if err := db.Ping(); err != nil {
		logger.Error("failed to ping database", slog.Any("err", err))
		os.Exit(1)
	}

	rc := &handlers.RequestContext{
		DB:        db,
		Logger:    logger,
		TmplCache: ssr.NewTmplCache(handlers.TemplateFuncs),
	}
	router := handlers.NewRouter(rc)

	// Pages
	router.Handle("GET /", handlers.HandleHome)
	router.Handle("GET /item/{id}", handlers.HandleItemPage)

	// Static files
	router.HandleStd("GET /static/{version}/", static.Handler())

	// Media
	router.Handle("GET /media/{id}", handlers.HandleGetMedia)
	router.Handle("GET /proxy/video/{id}", handlers.HandleProxyVideo)

	// Auth API
	router.Handle("POST /api/auth/login", handlers.HandleLogin)
	router.Handle("POST /api/auth/logout", handlers.HandleLogout)
	router.Handle("GET /api/auth/status", handlers.HandleAuthStatus)
	router.Handle("POST /api/auth/password", handlers.HandleChangePassword)

	// Items API
	router.Handle("GET /api/items", handlers.HandleListItems)
	router.Handle("GET /api/items/{id}", handlers.HandleGetItem)
	router.Handle("POST /api/items", handlers.HandleCreateItem)
	router.Handle("PUT /api/items/{id}", handlers.HandleUpdateItem)
	router.Handle("DELETE /api/items/{id}", handlers.HandleDeleteItem)

	// Item creation endpoints
	router.Handle("POST /api/preview", handlers.HandlePreviewLink)
	router.Handle("POST /api/items/from-link", handlers.HandleCreateFromLink)
	router.Handle("POST /api/items/upload", handlers.HandleUpload)
	router.Handle("POST /api/items/quote", handlers.HandleCreateQuote)
	router.Handle("POST /api/items/{id}/refresh", handlers.HandleRefreshMetadata)
	router.Handle("POST /api/items/{id}/media", handlers.HandleReplaceMedia)

	// Tags API
	router.Handle("GET /api/tags", handlers.HandleListTags)
	router.Handle("GET /api/tags/suggest", handlers.HandleSuggestTags)

	addr := defaultAddr
	if envAddr := os.Getenv("ADDR"); envAddr != "" {
		addr = envAddr
	}

	server := &http.Server{
		Addr:              addr,
		Handler:           middleware.Logging(logger)(router),
		ReadHeaderTimeout: 5 * time.Second,
	}

	go func() {
		logger.Info("listening", slog.String("addr", addr))
		if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			logger.Error("http server error", slog.Any("err", err))
			os.Exit(1)
		}
	}()

	waitForShutdown(server, logger)
}

func waitForShutdown(server *http.Server, logger *slog.Logger) {
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, os.Interrupt, syscall.SIGTERM)

	<-quit
	logger.Info("shutting down")

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	if err := server.Shutdown(ctx); err != nil {
		logger.Error("graceful shutdown failed", slog.Any("err", err))
	}
}

func runSetPassword(dbURL string, logger *slog.Logger) {
	fmt.Print("Enter new password: ")
	password, err := readPassword()
	if err != nil {
		logger.Error("failed to read password", slog.Any("err", err))
		os.Exit(1)
	}

	if password == "" {
		logger.Error("password cannot be empty")
		os.Exit(1)
	}

	fmt.Print("Confirm password: ")
	confirm, err := readPassword()
	if err != nil {
		logger.Error("failed to read confirmation", slog.Any("err", err))
		os.Exit(1)
	}

	if password != confirm {
		logger.Error("passwords do not match")
		os.Exit(1)
	}

	db, err := sql.Open("pgx", dbURL)
	if err != nil {
		logger.Error("failed to open database", slog.Any("err", err))
		os.Exit(1)
	}
	defer db.Close()

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		logger.Error("failed to hash password", slog.Any("err", err))
		os.Exit(1)
	}

	if err := admin.QSetPassword(ctx, db, hash); err != nil {
		logger.Error("failed to set password", slog.Any("err", err))
		os.Exit(1)
	}

	// Invalidate all sessions
	if err := session.QDeleteAll(ctx, db); err != nil {
		logger.Error("failed to clear sessions", slog.Any("err", err))
		os.Exit(1)
	}

	logger.Info("password updated, all sessions invalidated")
}

func readPassword() (string, error) {
	fd := int(os.Stdin.Fd())
	if term.IsTerminal(fd) {
		bytes, err := term.ReadPassword(fd)
		fmt.Println() // newline after password input
		return string(bytes), err
	}
	// Fallback for non-terminal (piped input)
	var password string
	_, err := fmt.Scanln(&password)
	return password, err
}
