package main

import (
	"context"
	"flag"
	"fmt"
	"log/slog"
	"os"
	"os/exec"
	"regexp"
	"strings"
	"time"

	"git.ma-al.com/goc_marek/gormcol"
	"github.com/joho/godotenv"
	"gorm.io/gorm"
)

// version returns the current version from git tag and last modification date.
func version() string {
	tag := "dev"
	if out, err := exec.Command("git", "describe", "--tags", "--abbrev=0").Output(); err == nil {
		tag = strings.TrimSpace(string(out))
	}

	date := ""
	if out, err := exec.Command("git", "log", "-1", "--format=%ci").Output(); err == nil {
		date = strings.TrimSpace(string(out))
		if len(date) >= 10 {
			date = date[:10]
		}
	}

	if date != "" {
		return fmt.Sprintf("%s (%s)", tag, date)
	}
	return tag
}

// main is the entry point for the gormcol-gen CLI tool.
// It parses flags, loads configuration from .env, connects to the database,
// and generates GORM models with column descriptors.
func main() {

	versionFlag := flag.Bool("version", false, "display version information")
	dsn := flag.String("dsn", "", "database DSN (e.g. user:pass@tcp(host:3306)/dbname)")
	filter := flag.String("filter", "", "regex to match table names (triggers batch mode)")
	all := flag.Bool("all", false, "generate all tables matching filter (shows confirmation)")
	clean := flag.Bool("clean", false, "remove existing model files before generation")
	outDir := flag.String("out", "./app/model/dbmodel", "output directory for generated files")
	pkgName := flag.String("pkg", "dbmodel", "Go package name for generated files")
	flag.Usage = func() {
		fmt.Fprintf(os.Stderr, "gormcol - generate GORM models with column descriptors\n\n")
		fmt.Fprintf(os.Stderr, "Usage:\n")
		fmt.Fprintf(os.Stderr, "  gormcol [options]\n\n")
		fmt.Fprintf(os.Stderr, "Modes:\n")
		fmt.Fprintf(os.Stderr, "  - Interactive (default): select tables with fzf\n")
		fmt.Fprintf(os.Stderr, "  - Batch (--filter or --all): generate matching tables with confirmation\n\n")
		fmt.Fprintf(os.Stderr, "DSN can be provided via --dsn flag or DSN env var (from .env)\n\n")
		fmt.Fprintf(os.Stderr, "Flags:\n")
		flag.PrintDefaults()
	}

	// Load .env file if present in current directory.
	godotenv.Load()

	flag.Parse()

	// Display version on startup.
	fmt.Printf("gormcol %s\n", version())

	// Display version if requested.
	if *versionFlag {
		return
	}

	// Connect to the database.
	dsnValue := *dsn
	if dsnValue == "" {
		dsnValue = os.Getenv("DSN")
	}
	if dsnValue == "" {
		flag.Usage()
		fmt.Fprintln(os.Stderr, "\nerror: --dsn or DSN env is required")
		os.Exit(1)
	}

	// Connect to the database.
	db, err := gormcol.ConnectDSN(dsnValue)
	if err != nil {
		slog.Error("failed to connect to database", "error", err)
		os.Exit(1)
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
	defer cancel()

	cfg := gormcol.GenConfig{
		OutputDir:   *outDir,
		PkgName:     *pkgName,
		TableFilter: *filter,
		Clean:       *clean,
	}

	// Modes:
	// 1. --all: generate all tables matching filter with confirmation
	// 2. --filter: generate all tables matching provided regex with confirmation
	// 3. Default: interactive mode with fzf
	if *all || *filter != "" {
		if *filter == "" {
			// --all without explicit filter, use default
			cfg.TableFilter = "(ps_|b2b_).*"
		}
		yellow := "\033[33m"
		reset := "\033[0m"
		fmt.Printf("%sBatch mode:%s generating tables matching filter '%s'\n", yellow, reset, cfg.TableFilter)
		if !confirmGenerateAll(db, cfg.TableFilter) {
			fmt.Println("Aborted.")
			os.Exit(0)
		}
	} else {
		// Interactive mode
		// Check if fzf is available.
		if _, err := exec.LookPath("fzf"); err != nil {
			fmt.Fprintln(os.Stderr, "error: fzf is required for interactive mode. Install from https://github.com/junegunn/fzf")
			os.Exit(1)
		}
		selected, err := selectTablesInteractive(db)
		if err != nil {
			slog.Error("failed to select tables interactively", "error", err)
			os.Exit(1)
		}
		if len(selected) == 0 {
			fmt.Println("No tables selected, exiting.")
			os.Exit(0)
		}
		cfg.SelectedTables = selected
	}

	// Generate models with column descriptors.
	if err := gormcol.NewWithConfig(db, cfg).GenModels(ctx); err != nil {
		slog.Error("failed to generate models", "error", err)
		os.Exit(1)
	}
}

// selectTablesInteractive displays all database tables in fzf for interactive selection.
// Returns a list of selected table names.
func selectTablesInteractive(db *gorm.DB) ([]string, error) {
	type migratorWithGetTables interface {
		GetTables() ([]string, error)
	}

	tableNames, err := db.Migrator().(migratorWithGetTables).GetTables()
	if err != nil {
		return nil, fmt.Errorf("failed to get tables: %w", err)
	}

	fmt.Printf("Found %d tables, launching fzf for selection...\n", len(tableNames))

	selected, err := runFzf(tableNames)
	if err != nil {
		return nil, fmt.Errorf("fzf selection failed: %w", err)
	}

	return selected, nil
}

// confirmGenerateAll displays a confirmation prompt before generating all tables.
// Returns true if user confirms, false if cancelled or timeout.
func confirmGenerateAll(db *gorm.DB, filter string) bool {
	type migratorWithGetTables interface {
		GetTables() ([]string, error)
	}

	tableNames, err := db.Migrator().(migratorWithGetTables).GetTables()
	if err != nil {
		return false
	}

	if len(tableNames) == 0 {
		fmt.Println("No tables in database.")
		return false
	}

	// Count tables matching the filter.
	re, err := regexp.Compile("^" + filter + "$")
	if err != nil {
		return false
	}

	var matched int
	for _, name := range tableNames {
		if re.MatchString(name) {
			matched++
		}
	}

	if matched == 0 {
		fmt.Printf("No tables match filter '%s'.\n", filter)
		return false
	}

	// Display yellow warning message and wait for user input.
	yellow := "\033[33m"
	reset := "\033[0m"
	msg := fmt.Sprintf("%sWARNING:%s Generate all %d tables? [Enter] confirm / [Esc] cancel", yellow, reset, matched)

	script := fmt.Sprintf(`echo; echo -e '%s'; read -t 10 -n 1 -s key; if [[ -z "$key" ]]; then exit 0; elif [[ "$key" == $'\e' ]]; then exit 1; else exit 0; fi`, msg)
	cmd := exec.Command("bash", "-c", script)
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	err = cmd.Run()
	return err == nil
}

// runFzf launches fzf for interactive table selection.
// Supports multi-select with Tab, fuzzy search, and keyboard navigation.
func runFzf(tableNames []string) ([]string, error) {
	header := "Tab: toggle select | Enter: confirm | Esc: cancel"
	cmd := exec.Command("fzf", "-m", "--height=50%", "--border", "--prompt", "Select tables> ", "--header", header)
	cmd.Stdin = strings.NewReader(strings.Join(tableNames, "\n"))

	out, err := cmd.Output()
	if err != nil {
		if exitErr, ok := err.(*exec.ExitError); ok {
			if exitErr.ExitCode() == 130 {
				// User pressed Esc to cancel.
				return []string{}, nil
			}
		}
		return nil, fmt.Errorf("fzf failed: %w", err)
	}

	// Parse selected lines from fzf output.
	selected := strings.Split(strings.TrimSpace(string(out)), "\n")
	var result []string
	for _, s := range selected {
		s = strings.TrimSpace(s)
		if s != "" {
			result = append(result, s)
		}
	}
	return result, nil
}
