package cmd

import (
	"fmt"
	"os"
	"strings"

	tea "charm.land/bubbletea/v2"

	"skraak/tools/calls"
	"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
}

// mustUniqueValue is like mustValue but exits if the flag was already set.
func mustUniqueValue(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 mustValue(args, i, flag)
}

// parseClassifyArgs parses the argument slice and returns classified args.
// Uses mustValue for flag-value extraction (exits on missing value).
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 = mustValue(args, &i, "--folder")
		case "--file":
			a.file = mustValue(args, &i, "--file")
		case "--filter":
			a.filter = mustUniqueValue(args, &i, "--filter", a.filter)
		case "--species":
			a.species = mustUniqueValue(args, &i, "--species", a.species)
		case "--certainty":
			a.certainty = mustIntValue(args, &i, "--certainty", 0, 100)
		case "--sample":
			i += 2
		case "--size":
			a.size = a.requireIntRange(args, i, "--size", 224, 896)
			a.sample = mustIntValue(args, &i, "--sample", 1, 100)
		case "--goto":
			a.gotoFile = mustValue(args, &i, "--goto")
		case "--location":
			a.location = mustUniqueValue(args, &i, "--location", a.location)
		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
}

// validate checks cross-flag constraints after parsing.
func (a classifyArgs) validate() error {
	if a.sample > 0 && a.sample < 100 && a.certainty < 0 {
		return fmt.Errorf("--sample requires --certainty to be set")
	}
	if a.folder == "" && a.file == "" {
		printClassifyUsage()
		return fmt.Errorf("missing required flag: --folder or --file")
	}
	if a.night && a.day {
		printClassifyUsage()
		return fmt.Errorf("--night and --day are mutually exclusive")
	}
	if (a.night || a.day) && a.location == "" {
		printClassifyUsage()
		return fmt.Errorf("--night/--day requires --location")
	}
	return nil
}

// validateBindings checks config bindings and secondary_bindings, returning
// the converted []tools.KeyBinding slice. Returns an error on validation errors.
func validateBindings(cfg *utils.Config, cfgPath string) ([]calls.KeyBinding, error) {
	// Convert config bindings map -> []tools.KeyBinding via existing parseBind.
	bindings := make([]calls.KeyBinding, 0, len(cfg.Classify.Bindings))
	for key, value := range cfg.Classify.Bindings {
		if len(key) != 1 {
			return nil, fmt.Errorf("binding key %q in %s must be a single character", key, cfgPath)
		}
		if purpose, reserved := reservedClassifyKeys[key]; reserved {
			return nil, fmt.Errorf("binding key %q in %s is reserved by the TUI for %s — pick a different key",
				key, cfgPath, purpose)
		}
		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 {
			return nil, fmt.Errorf("secondary_bindings key %q in %s has no matching primary binding",
				primaryKey, cfgPath)
		}
		for k, v := range inner {
			if len(k) != 1 {
				return nil, fmt.Errorf("secondary_bindings[%q] key %q in %s must be a single character",
					primaryKey, k, cfgPath)
			}
			if purpose, reserved := reservedClassifyKeys[k]; reserved {
				return nil, fmt.Errorf("secondary_bindings[%q] key %q in %s is reserved by the TUI for %s — pick a different key",
					primaryKey, k, cfgPath, purpose)
			}
			if v == "" {
				return nil, fmt.Errorf("secondary_bindings[%q][%q] in %s has empty calltype",
					primaryKey, k, cfgPath)
			}
		}
	}

	return bindings, nil
}

// 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) error {
	a := parseClassifyArgs(args)
	if err := a.validate(); err != nil {
		return err
	}

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

	// Validate config contents
	if cfg.Classify.Reviewer == "" {
		return fmt.Errorf("%s is missing \"classify.reviewer\"", cfgPath)
	}
	if len(cfg.Classify.Bindings) == 0 {
		return fmt.Errorf("%s is missing \"classify.bindings\" (need at least one key)", cfgPath)
	}

	bindings, err := validateBindings(&cfg, cfgPath)
	if err != nil {
		return err
	}

	// 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 != "" {
		lat, lng, timezone, err = utils.ParseLocation(a.location)
		if err != nil {
			return fmt.Errorf("parsing location: %w", err)
		}
	}

	// Build config
	config := calls.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 := calls.LoadDataFiles(config)
	if err != nil {
		return fmt.Errorf("loading data files: %w", err)
	}

	// 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")
		return nil
	}

	// Launch TUI (alt screen for clean kitty image rendering)
	p := tea.NewProgram(tui.New(state))
	if _, err := p.Run(); err != nil {
		return fmt.Errorf("TUI error: %w", err)
	}
	return nil
}

// parseBind parses "k=Kiwi" or "d=Kiwi+Duet" format
func parseBind(s string) calls.KeyBinding {
	parts := strings.SplitN(s, "=", 2)
	if len(parts) != 2 {
		panic(fmt.Sprintf("invalid bind format: %s (expected key=value)", s))
	}

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

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

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