#!/usr/bin/env python3
"""MIDI Learn Mode — interactive CLI for assigning MIDI controllers to mixer parameters.

Run this on the RPi to map physical knobs/faders to the mixer's parameters.
The learned mappings are saved to ~/.config/rpi-mixer/mappings/ for the daemon.

Usage:
  midi-learn-cli                          # Interactive learn mode
  midi-learn-cli --list                   # List current mappings
  midi-learn-cli --clear                  # Clear all mappings for default session
  midi-learn-cli --batch                  # Batch learn: assign faders in order
  midi-learn-cli --session mysetup        # Use a named session
  midi-learn-cli --export mysetup.json    # Export mappings to JSON file
"""

from __future__ import annotations

import argparse
import logging
import os
import signal
import sys
import time
from pathlib import Path

# Add project root to path when run as script
_proj_root = Path(__file__).resolve().parent.parent
if str(_proj_root) not in sys.path:
    sys.path.insert(0, str(_proj_root))

from src.midi.types import ParameterType, MIDIMapping
from src.midi.midi_learn import MIDILearn, LearnState
from src.midi.mapping_store import (
    save_mappings,
    load_mappings,
    list_sessions,
    DEFAULT_SESSION_NAME,
)
from src.mixer import ParameterRegistry

logger = logging.getLogger(__name__)

# Global flag for SIGINT handling
_running = True


def _handle_sigint(signum, frame):
    global _running
    print("\nInterrupted.", file=sys.stderr)
    _running = False


def _setup_logging(verbose: bool = False):
    level = logging.DEBUG if verbose else logging.INFO
    logging.basicConfig(level=level, format="%(levelname)s %(message)s")


def _list_mappings(session: str):
    """Print current mappings for a session."""
    mappings = load_mappings(session)
    if not mappings:
        print(f"No mappings found for session '{session}'.")
        return

    print(f"Session '{session}': {len(mappings)} mappings\n")
    for i, m in enumerate(mappings):
        src = f"NRPN{m.nrpn_number}" if m.is_nrpn else f"CC{m.controller}"
        dev = f" [{m.source_device}]" if m.source_device else ""
        ch = f"CH{m.param_channel}" if m.param_channel >= 0 else "Master"
        curve = f" {m.curve}" if m.curve != "linear" else ""
        state = "✓" if m.enabled else "✗"
        print(f"  {i:3d}. [{state}] CH{m.channel:2d} {src:<12s} → {ch:6s} {m.param_type.value:<16s} [{m.midi_min}..{m.midi_max}]{curve}{dev}")


def _interactive_learn(session: str):
    """Run the interactive MIDI learn CLI."""
    global _running
    signal.signal(signal.SIGINT, _handle_sigint)

    registry = ParameterRegistry()
    learn = MIDILearn()
    mappings: list[MIDIMapping] = load_mappings(session)

    print("╔══════════════════════════════════════════════════════════╗")
    print("║       RPi Audio Mixer — MIDI Learn Mode                  ║")
    print("╠══════════════════════════════════════════════════════════╣")
    print("║ Commands:                                                ║")
    print("║   l <param> [ch]  Learn: start listening for a parameter ║")
    print("║   b                Batch learn: assign faders in order   ║")
    print("║   c                Confirm captured mapping              ║")
    print("║   d                Discard captured mapping              ║")
    print("║   list             List current mappings                 ║")
    print("║   save             Save mappings to disk                 ║")
    print("║   del <n>          Delete mapping #n                     ║")
    print("║   status           Show learn status                     ║")
    print("║   help             Show this help                        ║")
    print("║   quit             Exit (auto-saves)                     ║")
    print("╚══════════════════════════════════════════════════════════╝")
    print()
    print(f"Session: {session}  |  Mappings loaded: {len(mappings)}")
    print(f"Parameters available: {len(list(registry.iter_all()))}")
    print()
    print("Available parameters:")
    _print_params(registry)
    print()
    print("Type a command. Tip: plug in your MIDI controller first,")
    print("then type 'l volume 0' and wiggle the fader you want to assign.")
    print()

    while _running:
        try:
            raw = input("midi-learn> ").strip()
            if not raw:
                continue

            parts = raw.split()
            cmd = parts[0].lower()

            if cmd == "quit" or cmd == "exit" or cmd == "q":
                break
            elif cmd == "help" or cmd == "h":
                _print_help()
            elif cmd == "list" or cmd == "ls":
                _list_current(mappings)
            elif cmd == "status" or cmd == "st":
                _print_status(learn, mappings)
            elif cmd == "save" or cmd == "s":
                save_mappings(mappings, session)
                print(f"Saved {len(mappings)} mappings to session '{session}'.")
            elif cmd == "l" or cmd == "learn":
                _cmd_learn(parts, learn, registry)
            elif cmd == "b" or cmd == "batch":
                _cmd_batch(parts, learn, registry)
            elif cmd == "c" or cmd == "confirm":
                _cmd_confirm(learn, mappings)
            elif cmd == "d" or cmd == "discard":
                learn.discard()
                print("Discarded. Ready for new learn.")
            elif cmd == "del" or cmd == "delete":
                _cmd_delete(parts, mappings)
            elif cmd == "clear":
                mappings.clear()
                print("All mappings cleared.")
            else:
                print(f"Unknown command: {cmd} (type 'help' for commands)")

        except EOFError:
            break
        except KeyboardInterrupt:
            print()
            break

    # Auto-save on exit
    if mappings:
        save_mappings(mappings, session)
        print(f"Auto-saved {len(mappings)} mappings to '{session}'.")
    else:
        print("No mappings to save.")


def _print_help():
    print("""
Commands:
  learn, l <param> [ch]   Start learning for a parameter
  batch, b [ch]            Batch learn all channel faders in order
  confirm, c               Confirm the last captured mapping
  discard, d               Discard the last captured mapping
  list, ls                 List current mappings
  delete, del <n>          Delete mapping #n
  save, s                  Save mappings to disk
  status, st               Show current learn status
  clear                    Clear all mappings
  quit, q                  Exit (auto-saves)
""")


def _print_params(registry: ParameterRegistry):
    """Print a compact parameter reference."""
    # Group by category
    from src.midi.types import ParameterCategory
    cats: dict[ParameterCategory, list[str]] = {}
    for p in registry.iter_all():
        cats.setdefault(p.category, []).append(p.param_type.value)

    for cat, params in sorted(cats.items()):
        unique = sorted(set(params))
        print(f"  {cat.value}: {', '.join(unique[:12])}{'...' if len(unique) > 12 else ''}")


def _list_current(mappings: list[MIDIMapping]):
    if not mappings:
        print("No mappings defined.")
        return
    for i, m in enumerate(mappings):
        src = f"NRPN{m.nrpn_number}" if m.is_nrpn else f"CC{m.controller}"
        print(f"  {i:3d}. CH{m.channel} {src} → {m.param_type.value} CH{m.param_channel} {'[OFF]' if not m.enabled else ''}")


def _print_status(learn: MIDILearn, mappings: list[MIDIMapping]):
    print(f"Learn state: {learn.status_text}")
    print(f"Mappings: {len(mappings)}")


def _cmd_learn(parts: list[str], learn: MIDILearn, registry: ParameterRegistry):
    """Handle 'learn <param> [channel]' command."""
    if len(parts) < 2:
        print("Usage: learn <param> [channel]")
        print("Example: learn volume 0")
        return

    param_name = parts[1].lower()
    channel = int(parts[2]) if len(parts) > 2 else -1

    # Resolve parameter type
    try:
        pt = ParameterType(param_name)
    except ValueError:
        # Try fuzzy match
        matches = [p for p in ParameterType if param_name in p.value]
        if len(matches) == 1:
            pt = matches[0]
        elif matches:
            print(f"Ambiguous: {', '.join(m.value for m in matches)}")
            return
        else:
            print(f"Unknown parameter: {param_name}")
            print(f"Available: {', '.join(p.value for p in ParameterType)}")
            return

    learn.start_learn(pt, channel)
    if channel >= 0:
        print(f"Listening for MIDI CC on any channel → {pt.value} CH{channel}")
    else:
        print(f"Listening for MIDI CC on any channel → {pt.value} (Master)")
    print("  Wiggle the knob/fader you want to assign, then type 'confirm' or 'c'.")


def _cmd_batch(parts: list[str], learn: MIDILearn, registry: ParameterRegistry):
    """Handle 'batch [channel]' command — learn all channel params in sequence."""
    channel = int(parts[1]) if len(parts) > 1 else 0

    params: list[tuple[ParameterType, int, str]] = [
        (ParameterType.VOLUME, channel, f"CH{channel} Vol"),
        (ParameterType.PAN, channel, f"CH{channel} Pan"),
        (ParameterType.MUTE, channel, f"CH{channel} Mute"),
        (ParameterType.GAIN, channel, f"CH{channel} Gain"),
    ]
    learn.start_batch(params)
    print(f"Batch learn for CH{channel}: wiggle each control in order.")
    print(f"  [1/4] {params[0][2]} — wiggle the fader now...")


def _cmd_confirm(learn: MIDILearn, mappings: list[MIDIMapping]):
    mapping = learn.confirm()
    if mapping:
        mappings.append(mapping)
        print(f"Confirmed: {mapping.label or f'CC{mapping.controller} → {mapping.param_type.value}'}")
    else:
        print("Nothing to confirm. Use 'learn <param>' first.")


def _cmd_delete(parts: list[str], mappings: list[MIDIMapping]):
    if len(parts) < 2:
        print("Usage: delete <n>")
        return
    try:
        idx = int(parts[1])
        if 0 <= idx < len(mappings):
            removed = mappings.pop(idx)
            print(f"Deleted: {removed.label or f'CC{removed.controller}'}")
        else:
            print(f"Invalid index: {idx} (0-{len(mappings)-1})")
    except ValueError:
        print(f"Invalid index: {parts[1]}")


# ── Main ────────────────────────────────────────────────────────────────────

def main():
    parser = argparse.ArgumentParser(
        description="MIDI Learn Mode — assign controllers to mixer parameters",
    )
    parser.add_argument("--list", action="store_true", help="List current mappings")
    parser.add_argument("--clear", action="store_true", help="Clear all mappings")
    parser.add_argument("--batch", action="store_true", help="Batch learn mode (non-interactive)")
    parser.add_argument("--session", default=DEFAULT_SESSION_NAME, help="Mapping session name")
    parser.add_argument("--export", metavar="FILE", help="Export mappings to JSON file")
    parser.add_argument("--verbose", "-v", action="store_true", help="Verbose logging")
    args = parser.parse_args()

    _setup_logging(args.verbose)

    if args.export:
        mappings = load_mappings(args.session)
        save_mappings(mappings, os.path.splitext(args.export)[0])
        print(f"Exported {len(mappings)} mappings to {args.export}")
        return

    if args.list:
        _list_mappings(args.session)
        return

    if args.clear:
        save_mappings([], args.session)
        print(f"Cleared session '{args.session}'.")
        return

    # Interactive mode
    _interactive_learn(args.session)


if __name__ == "__main__":
    main()
