"""
Type hint applicator for gui_2.py.
Does a single-pass AST-guided line edit to add type annotations.
No dependency on mcp_client — operates directly on file lines.

Run: uv run python scripts/apply_type_hints.py
"""
import ast
import re
import sys
import os
from typing import Any

BASE: str = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
stats: dict[str, Any] = {"auto_none": 0, "manual_sig": 0, "vars": 0, "errors": []}

def abs_path(filename: str) -> str:
 return os.path.join(BASE, filename)

def has_value_return(node: ast.AST) -> bool:
 """Check if function has any 'return <expr>' (not bare return or return None)."""
 for child in ast.walk(node):
  if isinstance(child, ast.Return) and child.value is not None:
   if isinstance(child.value, ast.Constant) and child.value.value is None:
    continue
   return True
 return False

def collect_auto_none(tree: ast.Module) -> list[tuple[str, ast.AST]]:
 """Collect functions that can safely get -> None annotation."""
 results = []

 def scan(scope, prefix=""):
  for node in ast.iter_child_nodes(scope):
   if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
    name = f"{prefix}{node.name}" if prefix else node.name
    if node.returns is None and not has_value_return(node):
     untyped = [a.arg for a in node.args.args if a.arg not in ("self", "cls") and a.annotation is None]
     if not untyped:
      results.append((name, node))
   if isinstance(node, ast.ClassDef):
    scan(node, prefix=f"{node.name}.")
 scan(tree)
 return results

def apply_return_none_single_pass(filepath: str) -> int:
 """Add -> None to all qualifying functions in a single pass."""
 fp = abs_path(filepath)
 with open(fp, 'r', encoding='utf-8') as f:
  code = f.read()
 tree = ast.parse(code)
 candidates = collect_auto_none(tree)
 if not candidates:
  return 0
 lines = code.splitlines(keepends=True)
 # For each candidate, find the line with the colon that ends the signature
 # and insert ' -> None' before it.
 # We need to find the colon on the signature line (not inside default values etc.)
 # Strategy: the signature ends at body[0].lineno - 1 (last sig line)
 # Find the last ':' on that line that's at the right indentation
 edits = []  # (line_idx, col_of_colon)
 for name, node in candidates:
  if not node.body:
   continue
   # The colon is on the last line of the signature
   # For single-line defs: `def foo(self):` -> colon at end
   # For multi-line defs: last line ends with `):` or similar
  body_start = node.body[0].lineno  # 1-indexed
  sig_last_line_idx = body_start - 2  # 0-indexed, the line before body
  # But for single-line signatures, sig_last_line_idx == node.lineno - 1
  if sig_last_line_idx < node.lineno - 1:
   sig_last_line_idx = node.lineno - 1
  line = lines[sig_last_line_idx]
  # Find the last colon on this line (the def colon)
  # Must handle cases like `def foo(self, x: int):` where there are colons in annotations
  # The def colon is always the LAST colon on the line (before any comment)
  stripped = line.rstrip('\n\r')
  # Remove inline comment
  comment_pos = -1
  in_str = False
  str_char = None
  for i, c in enumerate(stripped):
   if in_str:
    if c == str_char:
     in_str = False
    continue
   if c in ('"', "'"):
    in_str = True
    str_char = c
    continue
   if c == '#':
    comment_pos = i
    break
  code_part = stripped[:comment_pos] if comment_pos >= 0 else stripped
  # Find last colon in code_part
  colon_idx = code_part.rfind(':')
  if colon_idx < 0:
   stats["errors"].append(f"no colon found: {filepath}:{name} L{sig_last_line_idx+1}")
   continue
   # Check not already annotated
  if '->' in code_part:
   continue
  edits.append((sig_last_line_idx, colon_idx))
  # Apply edits in reverse order to preserve line indices
 edits.sort(key=lambda x: x[0], reverse=True)
 count = 0
 for line_idx, colon_col in edits:
  line = lines[line_idx]
  new_line = line[:colon_col] + ' -> None' + line[colon_col:]
  lines[line_idx] = new_line
  count += 1
 with open(fp, 'w', encoding='utf-8', newline='') as f:
  f.writelines(lines)
 return count
 # --- Manual signature replacements ---
 # These use regex on the def line to do a targeted replacement.
 # Each entry: (dotted_name, old_params_pattern, new_full_sig_line)
 # We match by finding the exact def line and replacing it.

def apply_manual_sigs(filepath: str, sig_replacements: list[tuple[str, str]]) -> int:
 """Apply manual signature replacements.
 sig_replacements: list of (regex_pattern_for_old_line, replacement_line)
 """
 fp = abs_path(filepath)
 with open(fp, 'r', encoding='utf-8') as f:
  code = f.read()
 count = 0
 for pattern, replacement in sig_replacements:
  new_code = re.sub(pattern, replacement, code, count=1)
  if new_code != code:
   code = new_code
   count += 1
  else:
   stats["errors"].append(f"manual_sig no match: {filepath}: {pattern[:60]}")
 with open(fp, 'w', encoding='utf-8', newline='') as f:
  f.write(code)
 return count

def apply_var_replacements(filepath: str, var_replacements: list[tuple[str, str]]) -> int:
 """Apply variable declaration replacements.
 var_replacements: list of (regex_pattern_for_old_decl, replacement_decl)
 """
 fp = abs_path(filepath)
 with open(fp, 'r', encoding='utf-8') as f:
  code = f.read()
 count = 0
 for pattern, replacement in var_replacements:
  new_code = re.sub(pattern, replacement, code, count=1)
  if new_code != code:
   code = new_code
   count += 1
  else:
   stats["errors"].append(f"var no match: {filepath}: {pattern[:60]}")
 with open(fp, 'w', encoding='utf-8', newline='') as f:
  f.write(code)
 return count

def verify_syntax(filepath: str) -> str:
 fp = abs_path(filepath)
 try:
  with open(fp, 'r', encoding='utf-8') as f:
   code = f.read()
  ast.parse(code)
  return f"Syntax OK: {filepath}"
 except SyntaxError as e:
  return f"SyntaxError in {filepath} at line {e.lineno}: {e.msg}"
  # ============================================================
  # gui_2.py manual signatures (Tier 3 items)
  # ============================================================
GUI2_MANUAL_SIGS: list[tuple[str, str]] = [
 (r'def resolve_pending_action\(self, action_id: str, approved: bool\):',
  r'def resolve_pending_action(self, action_id: str, approved: bool) -> bool:'),
 (r'def _cb_start_track\(self, user_data=None\):',
  r'def _cb_start_track(self, user_data: Any = None) -> None:'),
 (r'def _start_track_logic\(self, track_data\):',
  r'def _start_track_logic(self, track_data: dict[str, Any]) -> None:'),
 (r'def _cb_ticket_retry\(self, ticket_id\):',
  r'def _cb_ticket_retry(self, ticket_id: str) -> None:'),
 (r'def _cb_ticket_skip\(self, ticket_id\):',
  r'def _cb_ticket_skip(self, ticket_id: str) -> None:'),
 (r'def _render_ticket_dag_node\(self, ticket, tickets_by_id, children_map, rendered\):',
  r'def _render_ticket_dag_node(self, ticket: Ticket, tickets_by_id: dict[str, Ticket], children_map: dict[str, list[str]], rendered: set[str]) -> None:'),
]

# ============================================================
# gui_2.py variable type annotations
# ============================================================
GUI2_VAR_REPLACEMENTS: list[tuple[str, str]] = [
 (r'^CONFIG_PATH = ', 'CONFIG_PATH: Path = '),
 (r'^PROVIDERS = ', 'PROVIDERS: list[str] = '),
 (r'^COMMS_CLAMP_CHARS = ', 'COMMS_CLAMP_CHARS: int = '),
 (r'^C_OUT = ', 'C_OUT: tuple[float, ...] = '),
 (r'^C_IN = ', 'C_IN: tuple[float, ...] = '),
 (r'^C_REQ = ', 'C_REQ: tuple[float, ...] = '),
 (r'^C_RES = ', 'C_RES: tuple[float, ...] = '),
 (r'^C_TC = ', 'C_TC: tuple[float, ...] = '),
 (r'^C_TR = ', 'C_TR: tuple[float, ...] = '),
 (r'^C_TRS = ', 'C_TRS: tuple[float, ...] = '),
 (r'^C_LBL = ', 'C_LBL: tuple[float, ...] = '),
 (r'^C_VAL = ', 'C_VAL: tuple[float, ...] = '),
 (r'^C_KEY = ', 'C_KEY: tuple[float, ...] = '),
 (r'^C_NUM = ', 'C_NUM: tuple[float, ...] = '),
 (r'^C_SUB = ', 'C_SUB: tuple[float, ...] = '),
 (r'^DIR_COLORS = ', 'DIR_COLORS: dict[str, tuple[float, ...]] = '),
 (r'^KIND_COLORS = ', 'KIND_COLORS: dict[str, tuple[float, ...]] = '),
 (r'^HEAVY_KEYS = ', 'HEAVY_KEYS: set[str] = '),
 (r'^DISC_ROLES = ', 'DISC_ROLES: list[str] = '),
 (r'^AGENT_TOOL_NAMES = ', 'AGENT_TOOL_NAMES: list[str] = '),
]

if __name__ == "__main__":
 print("=== Phase A: Auto-apply -> None (single-pass AST) ===")
 n = apply_return_none_single_pass("gui_2.py")
 stats["auto_none"] += n
 print(f"  gui_2.py: {n} applied")
 # Verify syntax after Phase A
 r = verify_syntax("gui_2.py")
 if "Error" in r:
  print(f"  ABORT: {r}")
  sys.exit(1)
 print("  Syntax OK after Phase A")
 print("\n=== Phase B: Manual signatures (regex) ===")
 n = apply_manual_sigs("gui_2.py", GUI2_MANUAL_SIGS)
 stats["manual_sig"] += n
 print(f"  gui_2.py: {n} applied")
 # Verify syntax after Phase B
 r = verify_syntax("gui_2.py")
 if "Error" in r:
  print(f"  ABORT: {r}")
  sys.exit(1)
 print("  Syntax OK after Phase B")
 print("\n=== Phase C: Variable annotations (regex) ===")
 # Use re.MULTILINE so ^ matches line starts

 def apply_var_replacements_m(filepath, replacements):
  fp = abs_path(filepath)
  with open(fp, 'r', encoding='utf-8') as f:
   code = f.read()
  count = 0
  for pattern, replacement in replacements:
   new_code = re.sub(pattern, replacement, code, count=1, flags=re.MULTILINE)
   if new_code != code:
    code = new_code
    count += 1
   else:
    stats["errors"].append(f"var no match: {filepath}: {pattern[:60]}")
  with open(fp, 'w', encoding='utf-8', newline='') as f:
   f.write(code)
  return count
 n = apply_var_replacements_m("gui_2.py", GUI2_VAR_REPLACEMENTS)
 stats["vars"] += n
 print(f"  gui_2.py: {n} applied")
 print("\n=== Final Syntax Verification ===")
 r = verify_syntax("gui_2.py")
 print(f"  gui_2.py: {r}")
 all_ok = "Error" not in r
 print("\n=== Summary ===")
 print(f"  Auto -> None: {stats['auto_none']}")
 print(f"  Manual sigs:  {stats['manual_sig']}")
 print(f"  Variables:    {stats['vars']}")
 print(f"  Errors:       {len(stats['errors'])}")
 if stats['errors']:
  print("\n=== Errors ===")
  for e in stats['errors']:
   print(f"  {e}")
 if all_ok:
  print("\nAll files pass syntax check.")
 else:
  print("\nSYNTAX ERRORS DETECTED — review and fix before committing.")
