package main

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"log"
	"net/http"
	"net/url"
	"os"
	"os/signal"
	"path/filepath"
	"strings"
	"syscall"
	"time"

	"github.com/jackc/pgx/v5/pgxpool"

	"osaet/backend/internal/admin"
)

func main() {
	if err := run(); err != nil {
		fmt.Fprintln(os.Stderr, "error:", err)
		os.Exit(1)
	}
}

func run() error {
	command := "serve"
	if len(os.Args) > 1 {
		command = os.Args[1]
	}

	cfg := admin.LoadConfig()
	if err := cfg.Validate(); err != nil {
		return err
	}
	logCloser, err := admin.ConfigureLogging(cfg)
	if err != nil {
		return err
	}
	if logCloser != nil {
		defer logCloser.Close()
	}

	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()

	db, err := admin.OpenDatabase(ctx, cfg.DatabaseURL)
	if err != nil {
		return err
	}
	defer db.Close()

	switch command {
	case "serve":
		return serve(ctx, cfg, db)
	case "migrate":
		return admin.RunMigrations(ctx, db, cfg.MigrationsDir)
	case "create-user":
		return createUser(ctx, db)
	default:
		return fmt.Errorf("unknown command %q", command)
	}
}

func createUser(ctx context.Context, db *pgxpool.Pool) error {
	if len(os.Args) < 3 {
		return errors.New("usage: osaet-admin create-user <username>")
	}

	password := os.Getenv("OSAET_ADMIN_PASSWORD")
	if password == "" {
		return errors.New("OSAET_ADMIN_PASSWORD is required")
	}

	user, err := admin.NewStore(db).CreateOrUpdateUser(ctx, os.Args[2], password)
	if err != nil {
		return err
	}

	fmt.Fprintf(os.Stdout, "admin user %q is ready\n", user.Username)
	return nil
}

func serve(ctx context.Context, cfg admin.Config, db *pgxpool.Pool) error {
	logServeStartup(ctx, cfg, db)

	server := &http.Server{
		Addr:              cfg.Addr,
		Handler:           admin.NewServerWithContext(ctx, db, cfg).Router(),
		ReadHeaderTimeout: 5 * time.Second,
	}

	errCh := make(chan error, 1)
	go func() {
		log.Printf("http server listening addr=%s", cfg.Addr)
		errCh <- server.ListenAndServe()
	}()

	select {
	case <-ctx.Done():
		log.Printf("shutdown signal received")
		shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
		defer cancel()
		if err := server.Shutdown(shutdownCtx); err != nil {
			return err
		}
		log.Printf("http server stopped")
		return nil
	case err := <-errCh:
		if errors.Is(err, http.ErrServerClosed) {
			log.Printf("http server closed")
			return nil
		}
		return err
	}
}

func logServeStartup(ctx context.Context, cfg admin.Config, db *pgxpool.Pool) {
	log.Printf("osaet-admin starting command=serve")
	log.Printf("config addr=%s repo_root=%s", cfg.Addr, cleanPath(cfg.RepoRoot))
	log.Printf("paths posts=%s site=%s static=%s admin=%s migrations=%s",
		cleanPath(cfg.PostsDir),
		cleanPath(cfg.SiteDir),
		cleanPath(cfg.StaticDir),
		cleanPath(cfg.AdminDir),
		cleanPath(cfg.MigrationsDir),
	)
	log.Printf("logging file=%s max_bytes=%d max_backups=%d", cleanPath(cfg.LogFile), cfg.LogMaxBytes, cfg.LogMaxBackups)
	log.Printf("database configured=%t %s", strings.TrimSpace(cfg.DatabaseURL) != "", databaseSummary(cfg.DatabaseURL))
	log.Printf("r2 configured=%t endpoint=%s bucket=%s prefix=%s public_base_url=%s max_upload_bytes=%d",
		strings.TrimSpace(cfg.R2.Endpoint) != "" &&
			strings.TrimSpace(cfg.R2.Bucket) != "" &&
			strings.TrimSpace(cfg.R2.AccessKeyID) != "" &&
			strings.TrimSpace(cfg.R2.SecretAccessKey) != "",
		r2EndpointHost(cfg.R2.Endpoint),
		r2BucketName(cfg.R2.Bucket),
		cleanR2Prefix(cfg.R2.Bucket, cfg.R2.Prefix),
		cfg.R2.PublicBaseURL,
		cfg.R2.MaxUploadBytes,
	)
	logPathState("static output", cfg.StaticDir)
	logPathState("admin output", cfg.AdminDir)
	logPathState("posts snapshot", cfg.PostsDir)
	logMigrationState(ctx, db, cfg.MigrationsDir)
	logDatabaseInfo(ctx, db)
	log.Printf("routes public=/ admin=/admin api=/api/admin health=/healthz ready=/readyz")
}

func r2EndpointHost(endpoint string) string {
	if strings.TrimSpace(endpoint) == "" {
		return ""
	}
	parsed, err := url.Parse(endpoint)
	if err != nil {
		return "invalid"
	}
	return parsed.Host
}

func r2BucketName(bucket string) string {
	bucket = strings.Trim(strings.TrimSpace(bucket), "/")
	parts := strings.SplitN(bucket, "/", 2)
	return parts[0]
}

func cleanR2Prefix(bucket string, prefix string) string {
	bucket = strings.Trim(strings.TrimSpace(bucket), "/")
	prefix = strings.Trim(strings.TrimSpace(prefix), "/")
	parts := strings.SplitN(bucket, "/", 2)
	if len(parts) == 2 && prefix != "" {
		return parts[1] + "/" + prefix
	}
	if len(parts) == 2 {
		return parts[1]
	}
	return prefix
}

func logDatabaseInfo(ctx context.Context, db *pgxpool.Pool) {
	if db == nil {
		log.Printf("database pool unavailable")
		return
	}

	var database string
	var user string
	var serverAddr sql.NullString
	var serverPort sql.NullInt32
	var serverVersion string
	var timezone string
	err := db.QueryRow(ctx, `
SELECT current_database(),
       current_user,
       inet_server_addr()::text,
       inet_server_port(),
       current_setting('server_version'),
       current_setting('TimeZone')`).Scan(
		&database,
		&user,
		&serverAddr,
		&serverPort,
		&serverVersion,
		&timezone,
	)
	if err != nil {
		log.Printf("database info failed: %v", err)
		return
	}

	addr := "local"
	if serverAddr.Valid {
		addr = serverAddr.String
	}
	port := int32(0)
	if serverPort.Valid {
		port = serverPort.Int32
	}
	stats := db.Stat()
	log.Printf("database connected db=%s user=%s server=%s port=%d postgres=%s timezone=%s", database, user, addr, port, serverVersion, timezone)
	log.Printf("database pool total=%d idle=%d acquired=%d max=%d", stats.TotalConns(), stats.IdleConns(), stats.AcquiredConns(), stats.MaxConns())
}

func logMigrationState(ctx context.Context, db *pgxpool.Pool, migrationsDir string) {
	migrations, err := admin.LoadMigrationFiles(migrationsDir)
	if err != nil {
		log.Printf("migrations dir=%s status=unreadable error=%v", cleanPath(migrationsDir), err)
		return
	}

	var tableExists bool
	err = db.QueryRow(ctx, `SELECT to_regclass('public.admin_schema_migrations') IS NOT NULL`).Scan(&tableExists)
	if err != nil {
		log.Printf("migrations files=%d applied=unknown error=%v", len(migrations), err)
		return
	}
	if !tableExists {
		log.Printf("migrations files=%d applied=0 table=missing", len(migrations))
		return
	}

	var applied int
	if err := db.QueryRow(ctx, `SELECT count(*) FROM admin_schema_migrations`).Scan(&applied); err != nil {
		log.Printf("migrations files=%d applied=unknown error=%v", len(migrations), err)
		return
	}
	log.Printf("migrations files=%d applied=%d pending=%d", len(migrations), applied, maxInt(len(migrations)-applied, 0))
}

func logPathState(label string, path string) {
	info, err := os.Stat(path)
	if err != nil {
		log.Printf("path %s=%s status=missing error=%v", label, cleanPath(path), err)
		return
	}
	if info.IsDir() {
		log.Printf("path %s=%s status=dir", label, cleanPath(path))
		return
	}
	log.Printf("path %s=%s status=file size=%d", label, cleanPath(path), info.Size())
}

func databaseSummary(databaseURL string) string {
	if strings.TrimSpace(databaseURL) == "" {
		return "dsn=empty"
	}
	parsed, err := url.Parse(databaseURL)
	if err != nil {
		return "dsn=unparseable"
	}
	user := ""
	if parsed.User != nil {
		user = parsed.User.Username()
	}
	dbName := strings.TrimPrefix(parsed.Path, "/")
	sslMode := parsed.Query().Get("sslmode")
	parts := []string{
		"scheme=" + parsed.Scheme,
		"host=" + parsed.Host,
		"db=" + dbName,
		"user=" + user,
	}
	if sslMode != "" {
		parts = append(parts, "sslmode="+sslMode)
	}
	return strings.Join(parts, " ")
}

func cleanPath(path string) string {
	if path == "" {
		return ""
	}
	cleaned, err := filepath.Abs(path)
	if err != nil {
		return path
	}
	return cleaned
}

func maxInt(a int, b int) int {
	if a > b {
		return a
	}
	return b
}
