from __future__ import annotations

from imgui_bundle import imgui

from dataclasses import dataclass, field
from typing      import Optional, Callable, List, Dict, Any



@dataclass
class Command:
 id:           str
 title:        str
 category:     str
 shortcut:     Optional[str] = None
 description:  str = ""
 enabled_when: Optional[str] = None
 action:       Optional[Callable] = None

@dataclass
class ScoredCommand:
 command: Command
 score:   float


class CommandRegistry:
 def __init__(self) -> None:
  self._commands: Dict[str, Command] = {}

 def register(self, command_or_callable: Any) -> Any:
  if isinstance(command_or_callable, Command):
   cmd = command_or_callable
  else:
   cmd = Command(
    id=command_or_callable.__name__,
    title=command_or_callable.__name__.replace("_", " ").title(),
    category="uncategorized",
    action=command_or_callable,
   )
  if cmd.id in self._commands:
   raise ValueError(f"Command {cmd.id} already registered")
  self._commands[cmd.id] = cmd
  return command_or_callable

 def all(self) -> List[Command]:
  return list(self._commands.values())

 def get(self, command_id: str) -> Optional[Command]:
  return self._commands.get(command_id)


def fuzzy_match(query: str, candidates: List[Command], top_n: int = 20) -> List[ScoredCommand]:
 query_lower = query.lower()
 scored: List[ScoredCommand] = []
 for cmd in candidates:
  title_lower = cmd.title.lower()
  if not _is_subsequence(query_lower, title_lower):
   continue
  score = _compute_score(query_lower, title_lower)
  scored.append(ScoredCommand(command=cmd, score=score))
 scored.sort(key=lambda r: r.score, reverse=True)
 return scored[:top_n]


def _is_subsequence(query: str, target: str) -> bool:
 qi = 0
 for ch in target:
  if qi < len(query) and ch == query[qi]:
   qi += 1
 return qi == len(query)


def _compute_score(query: str, target: str) -> float:
 score = 0.0
 if   target.startswith(query):                score += 1.0
 elif _starts_at_word_boundary(query, target): score += 0.5
 if   _is_contiguous(query, target):           score += 0.3
 gaps   = _count_gaps(query, target)
 score -= 0.1 * gaps
 return score


def _starts_at_word_boundary(query: str, target: str) -> bool:
 if not target.startswith(query):
  return False
 return len(query) == 0 or not query[0].isalnum() or len(target) == len(query) or not target[len(query)].isalnum()


def _is_contiguous(query: str, target: str) -> bool:
 return query in target


def _count_gaps(query: str, target: str) -> int:
 qi         = 0
 gaps       = 0
 last_match = -1
 for ti, ch in enumerate(target):
  if qi < len(query) and ch == query[qi]:
   if last_match >= 0 and ti - last_match > 1: gaps += ti - last_match - 1
   last_match = ti
   qi        += 1
 return gaps


def _close_palette(app: Any) -> None:
 """Close the palette and reset all per-open state."""
 app.show_command_palette           = False
 app._command_palette_query         = ""
 app._command_palette_selected      = 0
 app._command_palette_focused       = False
 app._command_palette_input_focused = False


def _execute(app: Any, command: Command) -> None:
 """Run a command and close the palette. Catches exceptions to keep the modal clean."""
 if not command.action:
  return
 try:
  command.action(app)
 except Exception as e:
  print(f"[CommandPalette] Action {command.id} raised: {e}")
 _close_palette(app)


def render_palette_modal(app: Any, commands: List[Command]) -> None:
 """Renders the interactive Command Palette modal. Exposes a text input query bar
 and lists matching commands with fuzzy matching, supporting keyboard navigation (Up/Down/Enter/Esc).

 SSDL: `[I:query_input] -> [B:results_list] => [B:execute_command]`

 ASCII Layout Map:
   +==================== Command Palette ===================+
   | |query_text|                                           |
   | +-----------------------------------------------------+ |
   | | > [category-a] Command Title A  (selected)          | |
   | |   [category-b] Command Title B                      | |
   | +-----------------------------------------------------+ |
   +========================================================+
 """
 if not getattr(app, "show_command_palette", False):
  return

 viewport = imgui.get_main_viewport()
 center   = viewport.get_center()
 imgui.set_next_window_pos((center.x - 300, center.y - 200), imgui.Cond_.always)
 imgui.set_next_window_size((600, 400), imgui.Cond_.always)

 if not hasattr(app, "_command_palette_query"):    app._command_palette_query    = ""
 if not hasattr(app, "_command_palette_selected"): app._command_palette_selected = 0
 if not hasattr(app, "_command_palette_focused"):  app._command_palette_focused  = False

 # Set focus on the window + input field ONCE per open.
 if not app._command_palette_focused:
  imgui.set_next_window_focus()
  app._command_palette_focused = True

 # Escape closes the palette.
 if imgui.is_key_pressed(imgui.Key.escape):
  _close_palette(app)
  return

 expanded, opened = imgui.begin("Command Palette##manual_slop", True, imgui.WindowFlags_.no_collapse)
 if not expanded or not opened:
  app.show_command_palette     = False
  app._command_palette_focused = False
  imgui.end()
  return

 # After the window is drawn, the input gets focus.
 if not getattr(app, '_command_palette_input_focused', False):
  imgui.set_keyboard_focus_here()
  app._command_palette_input_focused = True

 # Process Up/Down/Enter BEFORE input_text so we see the keys before the
 # input field consumes them for cursor movement / text editing.
 results = fuzzy_match(app._command_palette_query, commands, top_n=20)
 if results: app._command_palette_selected = max(0, min(app._command_palette_selected, len(results) - 1))
 else:       app._command_palette_selected = 0

 if imgui.is_key_pressed(imgui.Key.down_arrow):
  if results:
   app._command_palette_selected = min(app._command_palette_selected + 1, len(results) - 1)
 if imgui.is_key_pressed(imgui.Key.up_arrow):
  if results:
   app._command_palette_selected = max(app._command_palette_selected - 1, 0)
 if imgui.is_key_pressed(imgui.Key.enter) or imgui.is_key_pressed(imgui.Key.keypad_enter):
  if results and 0 <= app._command_palette_selected < len(results):
   _execute(app, results[app._command_palette_selected].command)

 imgui.set_next_item_width(-1)
 _, app._command_palette_query = imgui.input_text("##query", app._command_palette_query)

 if imgui.begin_child("##results", (0, -1)):
  for i, scored in enumerate(results):
   is_selected = (i == app._command_palette_selected)
   label      = f"[{scored.command.category}] {scored.command.title}"
   clicked, _ = imgui.selectable(label, is_selected)
   if clicked:
    app._command_palette_selected = i
    _execute(app, scored.command)
  if not results:
   imgui.text_disabled("No matching commands.")
 imgui.end_child()

 imgui.end()
