package cmd

import (
	"fmt"
	"os"
	"strconv"
	"strings"

	tea "charm.land/bubbletea/v2"

	"skraak/tools"
	"skraak/tui"
	"skraak/utils"
)

// reservedClassifyKeys are single-character keys the classify TUI handles
// itself (see tui/classify.go). User bindings to these keys would be silently
// overridden by the TUI, so we reject them at config-load time.
var reservedClassifyKeys = map[string]string{
	",": "previous segment",
	".": "next segment",
	"0": "confirm label at certainty 100",
	" ": "open comment dialog",
}

func printClassifyUsage() {
	fmt.Fprintf(os.Stderr, "Usage: skraak calls classify [options]\n\n")
	fmt.Fprintf(os.Stderr, "Interactive TUI for reviewing and classifying bird call segments.\n")
	fmt.Fprintf(os.Stderr, "Reads .data files (AviaNZ format) and presents segments for labelling\n")
	fmt.Fprintf(os.Stderr, "with spectrogram display and audio playback.\n\n")
	fmt.Fprintf(os.Stderr, "Options:\n")
	fmt.Fprintf(os.Stderr, "  --folder <path>       Path to folder containing .data files (required, or --file)\n")
	fmt.Fprintf(os.Stderr, "  --file <path>         Path to a single .data file (required, or --folder)\n")
	fmt.Fprintf(os.Stderr, "  --filter <name>       Filter name to scope which segments to review (optional)\n")
	fmt.Fprintf(os.Stderr, "  --species <name>      Scope to species, optionally with calltype (e.g. Kiwi, Kiwi+Duet, Kiwi+_)\n")
	fmt.Fprintf(os.Stderr, "                        Use +_ to match only calls with no calltype\n")
	fmt.Fprintf(os.Stderr, "  --certainty <int>     Scope to certainty value (0-100, optional)\n")
	fmt.Fprintf(os.Stderr, "  --sample <1-100>      Randomly sample N%% of filtered calls (requires --certainty; 100 = no-op)\n")
	fmt.Fprintf(os.Stderr, "  --goto <filename>     Start at this .data file (basename match, optional)\n")
	fmt.Fprintf(os.Stderr, "  --night               Only review solar-night recordings (requires --location)\n")
	fmt.Fprintf(os.Stderr, "  --day                 Only review solar-day recordings (requires --location)\n")
	fmt.Fprintf(os.Stderr, "  --size <int>         Spectrogram image size in pixels (224-896, default: config img_dims or 448)\n")
	fmt.Fprintf(os.Stderr, "  --location <lat,lng[,tz]> GPS coordinates and optional IANA timezone\n")
	fmt.Fprintf(os.Stderr, "                        e.g. --location \"-36.85,174.76\" or --location \"-36.85,174.76,Pacific/Auckland\"\n")
	fmt.Fprintf(os.Stderr, "                        Required with --night or --day. Timezone defaults to UTC.\n")
	fmt.Fprintf(os.Stderr, "                        Not needed for AudioMoth data (UTC from WAV comment).\n")
	fmt.Fprintf(os.Stderr, "\nConfig (required): ~/.skraak/config.json\n")
	fmt.Fprintf(os.Stderr, "  Provides reviewer, keybindings, and display flags (color/sixel/iterm/img_dims).\n")
	fmt.Fprintf(os.Stderr, "  Example:\n")
	fmt.Fprintf(os.Stderr, "    {\n")
	fmt.Fprintf(os.Stderr, "      \"classify\": {\n")
	fmt.Fprintf(os.Stderr, "        \"reviewer\": \"David\",\n")
	fmt.Fprintf(os.Stderr, "        \"color\": true,\n")
	fmt.Fprintf(os.Stderr, "        \"bindings\": {\n")
	fmt.Fprintf(os.Stderr, "          \"k\": \"Kiwi\",\n")
	fmt.Fprintf(os.Stderr, "          \"1\": \"Kiwi+Duet\",\n")
	fmt.Fprintf(os.Stderr, "          \"x\": \"Noise\"\n")
	fmt.Fprintf(os.Stderr, "        }\n")
	fmt.Fprintf(os.Stderr, "      }\n")
	fmt.Fprintf(os.Stderr, "    }\n")
	fmt.Fprintf(os.Stderr, "\nExamples:\n")
	fmt.Fprintf(os.Stderr, "  skraak calls classify --folder /path/to/data\n")
	fmt.Fprintf(os.Stderr, "  skraak calls classify --file /path/to/file.data --filter opensoundscape-kiwi-1.2\n")
	fmt.Fprintf(os.Stderr, "  skraak calls classify --folder /path/to/data --species Kiwi+Duet\n")
}

// classifyArgs holds parsed CLI arguments for the classify subcommand.
type classifyArgs struct {
	folder    string
	file      string
	filter    string
	species   string
	gotoFile  string
	certainty int
	sample    int
	size      int
	night     bool
	day       bool
	location  string
}

// parseClassifyArgs parses the argument slice and returns classified args.
// Exits on parse errors.
func parseClassifyArgs(args []string) classifyArgs {
	a := classifyArgs{certainty: -1, sample: -1, size: 0}

	i := 0
	for i < len(args) {
		arg := args[i]

		switch arg {
		case "--folder":
			a.folder = a.requireValue(args, i, "--folder")
			i += 2
		case "--file":
			a.file = a.requireValue(args, i, "--file")
			i += 2
		case "--filter":
			a.filter = a.requireUniqueValue(args, i, "--filter", a.filter)
			i += 2
		case "--species":
			a.species = a.requireUniqueValue(args, i, "--species", a.species)
			i += 2
		case "--certainty":
			a.certainty = a.requireIntRange(args, i, "--certainty", 0, 100)
			i += 2
		case "--sample":
			a.sample = a.requireIntRange(args, i, "--sample", 1, 100)
			i += 2
		case "--size":
			a.size = a.requireIntRange(args, i, "--size", 224, 896)
			i += 2
		case "--goto":
			a.gotoFile = a.requireValue(args, i, "--goto")
			i += 2
		case "--location":
			a.location = a.requireUniqueValue(args, i, "--location", a.location)
			i += 2
		case "--night":
			a.night = true
			i++
		case "--day":
			a.day = true
			i++
		case "--help", "-h":
			printClassifyUsage()
			os.Exit(0)
		default:
			fmt.Fprintf(os.Stderr, "Error: unknown flag: %s\n\n", arg)
			printClassifyUsage()
			os.Exit(1)
		}
	}

	return a
}

// requireValue returns the next argument or exits if missing.
func (classifyArgs) requireValue(args []string, i int, flag string) string {
	if i+1 >= len(args) {
		fmt.Fprintf(os.Stderr, "Error: %s requires a value\n", flag)
		os.Exit(1)
	}
	return args[i+1]
}

// requireUniqueValue is like requireValue but exits if the flag was already set.
func (a classifyArgs) requireUniqueValue(args []string, i int, flag, current string) string {
	if current != "" {
		fmt.Fprintf(os.Stderr, "Error: %s can only be specified once\n", flag)
		os.Exit(1)
	}
	return a.requireValue(args, i, flag)
}

// requireIntRange parses the next arg as int and validates range [lo,hi].
func (a classifyArgs) requireIntRange(args []string, i int, flag string, lo, hi int) int {
	val := a.requireValue(args, i, flag)
	v, err := strconv.Atoi(val)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error: %s must be an integer\n", flag)
		os.Exit(1)
	}
	if v < lo || v > hi {
		fmt.Fprintf(os.Stderr, "Error: %s must be between %d and %d\n", flag, lo, hi)
		os.Exit(1)
	}
	return v
}

// validate checks cross-flag constraints after parsing.
func (a classifyArgs) validate() {
	if a.sample > 0 && a.sample < 100 && a.certainty < 0 {
		fmt.Fprintf(os.Stderr, "Error: --sample requires --certainty to be set\n")
		os.Exit(1)
	}
	if a.folder == "" && a.file == "" {
		fmt.Fprintf(os.Stderr, "Error: missing required flag: --folder or --file\n\n")
		printClassifyUsage()
		os.Exit(1)
	}
	if a.night && a.day {
		fmt.Fprintf(os.Stderr, "Error: --night and --day are mutually exclusive\n\n")
		printClassifyUsage()
		os.Exit(1)
	}
	if (a.night || a.day) && a.location == "" {
		fmt.Fprintf(os.Stderr, "Error: --night/--day requires --location\n\n")
		printClassifyUsage()
		os.Exit(1)
	}
}

// validateBindings checks config bindings and secondary_bindings, returning
// the converted []tools.KeyBinding slice. Exits on validation errors.
func validateBindings(cfg *utils.Config, cfgPath string) []tools.KeyBinding {
	// Convert config bindings map -> []tools.KeyBinding via existing parseBind.
	bindings := make([]tools.KeyBinding, 0, len(cfg.Classify.Bindings))
	for key, value := range cfg.Classify.Bindings {
		if len(key) != 1 {
			fmt.Fprintf(os.Stderr, "Error: binding key %q in %s must be a single character\n", key, cfgPath)
			os.Exit(1)
		}
		if purpose, reserved := reservedClassifyKeys[key]; reserved {
			fmt.Fprintf(os.Stderr,
				"Error: binding key %q in %s is reserved by the TUI for %s — pick a different key.\n",
				key, cfgPath, purpose)
			os.Exit(1)
		}
		bindings = append(bindings, parseBind(key+"="+value))
	}

	// Validate secondary_bindings: each outer key must exist in bindings,
	// each inner key must be a single non-reserved char, values non-empty.
	for primaryKey, inner := range cfg.Classify.SecondaryBindings {
		if _, ok := cfg.Classify.Bindings[primaryKey]; !ok {
			fmt.Fprintf(os.Stderr,
				"Error: secondary_bindings key %q in %s has no matching primary binding\n",
				primaryKey, cfgPath)
			os.Exit(1)
		}
		for k, v := range inner {
			if len(k) != 1 {
				fmt.Fprintf(os.Stderr,
					"Error: secondary_bindings[%q] key %q in %s must be a single character\n",
					primaryKey, k, cfgPath)
				os.Exit(1)
			}
			if purpose, reserved := reservedClassifyKeys[k]; reserved {
				fmt.Fprintf(os.Stderr,
					"Error: secondary_bindings[%q] key %q in %s is reserved by the TUI for %s — pick a different key.\n",
					primaryKey, k, cfgPath, purpose)
				os.Exit(1)
			}
			if v == "" {
				fmt.Fprintf(os.Stderr,
					"Error: secondary_bindings[%q][%q] in %s has empty calltype\n",
					primaryKey, k, cfgPath)
				os.Exit(1)
			}
		}
	}

	return bindings
}

// RunCallsClassify handles the "calls classify" subcommand
// classifyImageSize returns the effective image size: CLI flag overrides config.
// Zero from both means use the default (448, handled downstream).
func classifyImageSize(cliSize, configSize int) int {
	if cliSize > 0 {
		return cliSize
	}
	return configSize
}

// RunCallsClassify handles the "calls classify" subcommand
func RunCallsClassify(args []string) {
	a := parseClassifyArgs(args)
	a.validate()

	// Load reviewer, bindings, and display flags from ~/.skraak/config.json.
	cfg, cfgPath, err := utils.LoadConfig()
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
		fmt.Fprintf(os.Stderr, "Create %s with a \"classify\" section; run `skraak calls classify --help` for an example.\n", cfgPath)
		os.Exit(1)
	}

	// Validate config contents
	if cfg.Classify.Reviewer == "" {
		fmt.Fprintf(os.Stderr, "Error: %s is missing \"classify.reviewer\"\n", cfgPath)
		os.Exit(1)
	}
	if len(cfg.Classify.Bindings) == 0 {
		fmt.Fprintf(os.Stderr, "Error: %s is missing \"classify.bindings\" (need at least one key)\n", cfgPath)
		os.Exit(1)
	}

	bindings := validateBindings(&cfg, cfgPath)

	// Parse species+calltype
	speciesName, callType := utils.ParseSpeciesCallType(a.species)

	// Parse location into lat/lng/timezone
	var lat, lng float64
	var timezone string
	if a.location != "" {
		var err error
		lat, lng, timezone, err = utils.ParseLocation(a.location)
		if err != nil {
			fmt.Fprintf(os.Stderr, "Error: %v\n", err)
			os.Exit(1)
		}
	}

	// Build config
	config := tools.ClassifyConfig{
		Folder:            a.folder,
		File:              a.file,
		Filter:            a.filter,
		Species:           speciesName,
		CallType:          callType,
		Certainty:         a.certainty,
		Sample:            a.sample,
		Goto:              a.gotoFile,
		Reviewer:          cfg.Classify.Reviewer,
		Color:             cfg.Classify.Color,
		ImageSize:         classifyImageSize(a.size, cfg.Classify.ImgDims),
		Sixel:             cfg.Classify.Sixel,
		ITerm:             cfg.Classify.ITerm,
		Bindings:          bindings,
		SecondaryBindings: cfg.Classify.SecondaryBindings,
		Night:             a.night,
		Day:               a.day,
		Lat:               lat,
		Lng:               lng,
		Timezone:          timezone,
	}

	// Load data files
	state, err := tools.LoadDataFiles(config)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
		os.Exit(1)
	}

	// Show filtered counts (files with no matching segments are already pruned)
	if state.TimeFilteredCount > 0 {
		label := "daytime"
		if config.Day {
			label = "nighttime"
		}
		fmt.Fprintf(os.Stderr, "Skipped %d %s files\n", state.TimeFilteredCount, label)
	}
	fmt.Fprintf(os.Stderr, "Loaded %d files with %d matching segments\n",
		len(state.DataFiles), state.TotalSegments())

	if state.TotalSegments() == 0 {
		fmt.Fprintf(os.Stderr, "No segments to review.\n")
		os.Exit(0)
	}

	// Launch TUI (alt screen for clean kitty image rendering)
	p := tea.NewProgram(tui.New(state))
	if _, err := p.Run(); err != nil {
		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
		os.Exit(1)
	}
}

// parseBind parses "k=Kiwi" or "d=Kiwi+Duet" format
func parseBind(s string) tools.KeyBinding {
	parts := strings.SplitN(s, "=", 2)
	if len(parts) != 2 {
		fmt.Fprintf(os.Stderr, "Error: invalid bind format: %s (expected key=value)\n", s)
		os.Exit(1)
	}

	key := parts[0]
	value := parts[1]

	// Check for Species+CallType format
	if strings.Contains(value, "+") {
		valueParts := strings.SplitN(value, "+", 2)
		return tools.KeyBinding{
			Key:      key,
			Species:  valueParts[0],
			CallType: valueParts[1],
		}
	}

	// Species only
	return tools.KeyBinding{
		Key:     key,
		Species: value,
	}
}