"""Echo Core fast commands — no-LLM instant responses.

Dispatch table with handlers for quick operations: git, email, calendar,
notes, search, reminders, and diagnostics.
"""

import json
import logging
import os
import shutil
import subprocess
import threading
from datetime import datetime, date, timedelta
from pathlib import Path
from typing import Callable

log = logging.getLogger(__name__)

PROJECT_ROOT = Path(__file__).resolve().parent.parent
TOOLS_DIR = PROJECT_ROOT / "tools"
MEMORY_DIR = PROJECT_ROOT / "memory"
LOGS_DIR = PROJECT_ROOT / "logs"

# ---------------------------------------------------------------------------
# Thread-local channel context — set by router.py before fast_dispatch calls
# ---------------------------------------------------------------------------

_ctx = threading.local()


def set_channel_context(channel_id: str) -> None:
    """Called by router.py before dispatching a fast command."""
    _ctx.channel_id = channel_id


def _get_ctx_channel() -> str | None:
    return getattr(_ctx, "channel_id", None)


# ---------------------------------------------------------------------------
# Git
# ---------------------------------------------------------------------------

def cmd_commit(args: list[str]) -> str:
    """git add -A + commit with auto or custom message."""
    status = _git("status", "--porcelain")
    if not status.strip():
        return "Nothing to commit."

    files = [l for l in status.strip().split("\n") if l.strip()]
    file_count = len(files)

    if args:
        message = " ".join(args)
    else:
        message = _auto_commit_message(files)

    _git("add", "-A")
    result = _git("commit", "-m", message)

    # Extract short hash
    short_hash = _git("rev-parse", "--short", "HEAD").strip()
    return f"Committed: {short_hash} — {message} ({file_count} files)"


def cmd_push(args: list[str]) -> str:
    """git push with safety checks."""
    # Check for uncommitted changes
    status = _git("status", "--porcelain")
    if status.strip():
        uncommitted = len([l for l in status.strip().split("\n") if l.strip()])
        return f"Warning: {uncommitted} uncommitted changes. Commit first."

    result = _git("push")
    if "Everything up-to-date" in result or "up-to-date" in result.lower():
        return "Already up to date."

    branch = _git("branch", "--show-current").strip()
    return f"Pushed to origin/{branch}."


def cmd_pull(args: list[str]) -> str:
    """git pull --rebase."""
    result = _git("pull", "--rebase")
    if "Already up to date" in result:
        return "Already up to date."
    if "Fast-forward" in result or "rewinding" in result.lower():
        # Count commits pulled
        lines = [l for l in result.split("\n") if l.strip()]
        return f"Pulled updates ({len(lines)} lines of output)."
    return result.strip()[:500]


# ---------------------------------------------------------------------------
# Test & Dev
# ---------------------------------------------------------------------------

def cmd_test(args: list[str]) -> str:
    """Run pytest tests."""
    cmd = ["python3", "-m", "pytest", "tests/", "-q", "--tb=short"]
    if args:
        cmd.extend(["-k", " ".join(args)])
    try:
        proc = subprocess.run(
            cmd, capture_output=True, text=True, timeout=120,
            cwd=str(PROJECT_ROOT),
        )
        output = proc.stdout.strip()
        if proc.stderr and proc.returncode != 0:
            output += "\n" + proc.stderr.strip()
        # Return last few lines (summary)
        lines = output.split("\n")
        summary = "\n".join(lines[-10:]) if len(lines) > 10 else output
        return summary
    except subprocess.TimeoutExpired:
        return "Tests timed out (120s limit)."


# ---------------------------------------------------------------------------
# Email
# ---------------------------------------------------------------------------

def cmd_email(args: list[str]) -> str:
    """Dispatch: /email, /email send ..., /email save, /email digest."""
    if not args:
        return _email_check()
    sub = args[0].lower()
    if sub == "send":
        return _email_send(args[1:])
    if sub == "save":
        return _email_save()
    if sub == "digest":
        return _email_digest()
    if sub == "forward":
        return _email_forward()
    return f"Unknown email sub-command: {sub}. Use: /email, /email send, /email save, /email digest, /email forward"


def _email_check() -> str:
    """Check unread emails."""
    script = TOOLS_DIR / "email_check.py"
    if not script.exists():
        return "email_check.py not found."
    try:
        proc = subprocess.run(
            ["python3", str(script)],
            capture_output=True, text=True, timeout=30,
            cwd=str(PROJECT_ROOT),
        )
        if proc.returncode != 0:
            return f"Email check failed: {proc.stderr.strip()[:200]}"
        data = json.loads(proc.stdout)
        if not data.get("ok"):
            return f"Email error: {data.get('error', 'unknown')}"
        count = data.get("unread_count", 0)
        if count == 0:
            return "Inbox curat."
        emails = data.get("emails", [])
        parts = [f"{count} necitite:\n"]
        for e in emails[:5]:
            sender = e.get("from", "?")
            subject = e.get("subject", "?")
            body = e.get("body_preview", "").strip()
            parts.append(f"**{subject}**\nDe la: {sender}")
            if body:
                parts.append(body[:500])
            parts.append("")
        return "\n".join(parts).strip()
    except json.JSONDecodeError:
        return proc.stdout.strip()[:300] if proc.stdout else "Email check: no output."
    except Exception as e:
        return f"Email check error: {e}"


def _email_send(args: list[str]) -> str:
    """Send email: /email send <to> <subject> :: <body>."""
    raw = " ".join(args)
    if "::" not in raw:
        return "Format: /email send <to> <subject> :: <body>"
    header, body = raw.split("::", 1)
    header_parts = header.strip().split(None, 1)
    if len(header_parts) < 2:
        return "Format: /email send <to> <subject> :: <body>"
    to_addr = header_parts[0]
    subject = header_parts[1]
    body = body.strip()

    script = TOOLS_DIR / "email_send.py"
    if not script.exists():
        return "email_send.py not found."
    try:
        proc = subprocess.run(
            ["python3", str(script), to_addr, subject, body],
            capture_output=True, text=True, timeout=30,
            cwd=str(PROJECT_ROOT),
        )
        if proc.returncode != 0:
            return f"Email send failed: {proc.stderr.strip()[:200]}"
        data = json.loads(proc.stdout)
        if data.get("ok"):
            return f"Email trimis catre {to_addr}."
        return f"Email error: {data.get('error', 'unknown')}"
    except Exception as e:
        return f"Email send error: {e}"


def _email_digest() -> str:
    """Run email digest: save unread emails, generate summaries, send on WhatsApp."""
    script = TOOLS_DIR / "email_digest.py"
    if not script.exists():
        return "email_digest.py not found."

    import sys as _sys

    def _run():
        try:
            result = subprocess.run(
                [_sys.executable, str(script)],
                timeout=120,
                cwd=str(PROJECT_ROOT),
                capture_output=True,
                text=True,
            )
            if result.returncode != 0:
                log.error("Email digest failed: %s", result.stderr.strip()[:300])
            else:
                log.info("Email digest: %s", result.stdout.strip()[:300])
        except Exception as e:
            log.error("Email digest error: %s", e)

    threading.Thread(target=_run, daemon=True).start()
    return "Procesez emailurile... rezumatele vin pe WhatsApp."


def _email_forward() -> str:
    """Forward unread emails directly to WhatsApp without summarizing."""
    script = TOOLS_DIR / "email_forward.py"
    if not script.exists():
        return "email_forward.py not found."

    import sys as _sys

    def _run():
        try:
            result = subprocess.run(
                [_sys.executable, str(script)],
                timeout=60,
                cwd=str(PROJECT_ROOT),
                capture_output=True,
                text=True,
            )
            if result.returncode != 0:
                log.error("Email forward failed: %s", result.stderr.strip()[:300])
            else:
                log.info("Email forward: %s", result.stdout.strip()[:300])
        except Exception as e:
            log.error("Email forward error: %s", e)

    threading.Thread(target=_run, daemon=True).start()
    return "Forwardez emailurile pe WhatsApp..."


def _email_save() -> str:
    """Save unread emails as KB notes."""
    script = TOOLS_DIR / "email_process.py"
    if not script.exists():
        return "email_process.py not found."
    try:
        proc = subprocess.run(
            ["python3", str(script), "--save"],
            capture_output=True, text=True, timeout=60,
            cwd=str(PROJECT_ROOT),
        )
        output = proc.stdout.strip()
        if not output or "Niciun email" in output:
            return "Nimic de salvat."
        # Count saved lines and index new files
        saved = output.count("Salvat")
        if saved:
            # Index newly saved email files
            kb_emails = MEMORY_DIR / "kb" / "emails"
            if kb_emails.exists():
                for md in sorted(kb_emails.glob("*.md"))[-saved:]:
                    _index_file_async(md)
            return f"Salvate {saved} emailuri in KB."
        return output[:300]
    except Exception as e:
        return f"Email save error: {e}"


# ---------------------------------------------------------------------------
# Calendar
# ---------------------------------------------------------------------------

def cmd_calendar(args: list[str]) -> str:
    """Dispatch: /calendar, /calendar week, /calendar busy."""
    script = TOOLS_DIR / "calendar_check.py"
    if not script.exists():
        return "calendar_check.py not found."

    if not args:
        return _calendar_today(script)
    sub = args[0].lower()
    if sub == "week":
        return _calendar_week(script)
    if sub == "busy":
        return _calendar_busy(script)
    return f"Unknown calendar sub-command: {sub}. Use: /calendar, /calendar week, /calendar busy"


def _calendar_today(script: Path) -> str:
    """Today + tomorrow events."""
    try:
        proc = subprocess.run(
            ["python3", str(script), "today"],
            capture_output=True, text=True, timeout=30,
            cwd=str(PROJECT_ROOT),
        )
        if proc.returncode != 0:
            return f"Calendar error: {proc.stderr.strip()[:200]}"
        data = json.loads(proc.stdout)
        parts = []
        today_events = data.get("today", [])
        tomorrow_events = data.get("tomorrow", [])
        if today_events:
            parts.append("Program azi:")
            for ev in today_events:
                parts.append(f"  {ev.get('time', '?')} — {ev.get('summary', '?')}")
        else:
            parts.append("Azi: nimic programat.")
        if tomorrow_events:
            parts.append("Maine:")
            for ev in tomorrow_events:
                parts.append(f"  {ev.get('time', '?')} — {ev.get('summary', '?')}")
        return "\n".join(parts)
    except Exception as e:
        return f"Calendar error: {e}"


def _calendar_week(script: Path) -> str:
    """Week events."""
    try:
        proc = subprocess.run(
            ["python3", str(script), "week"],
            capture_output=True, text=True, timeout=30,
            cwd=str(PROJECT_ROOT),
        )
        if proc.returncode != 0:
            return f"Calendar error: {proc.stderr.strip()[:200]}"
        data = json.loads(proc.stdout)
        events = data.get("events", [])
        if not events:
            return "Saptamana libera."
        week_range = f"{data.get('week_start', '?')} — {data.get('week_end', '?')}"
        parts = [f"Saptamana {week_range}:"]
        current_date = None
        for ev in events:
            ev_date = ev.get("date", "?")
            if ev_date != current_date:
                current_date = ev_date
                parts.append(f"\n  {ev_date}:")
            parts.append(f"    {ev.get('time', '?')} — {ev.get('summary', '?')}")
        return "\n".join(parts)
    except Exception as e:
        return f"Calendar error: {e}"


def _calendar_busy(script: Path) -> str:
    """Am I busy right now?"""
    try:
        proc = subprocess.run(
            ["python3", str(script), "busy"],
            capture_output=True, text=True, timeout=30,
            cwd=str(PROJECT_ROOT),
        )
        if proc.returncode != 0:
            return f"Calendar error: {proc.stderr.strip()[:200]}"
        data = json.loads(proc.stdout)
        if data.get("busy"):
            event = data.get("event", "?")
            ends = data.get("ends", "?")
            return f"Ocupat: {event} (pana la {ends})"
        return "Liber."
    except Exception as e:
        return f"Calendar error: {e}"


# ---------------------------------------------------------------------------
# Notes & Journal
# ---------------------------------------------------------------------------

def cmd_note(args: list[str]) -> str:
    """Append a quick note to today's daily file."""
    if not args:
        return "Usage: /note <text>"
    text = " ".join(args)
    today = date.today().isoformat()
    now = datetime.now().strftime("%H:%M")
    filepath = MEMORY_DIR / f"{today}.md"

    if not filepath.exists():
        filepath.write_text(f"# {today}\n\n", encoding="utf-8")

    with open(filepath, "a", encoding="utf-8") as f:
        f.write(f"- [{now}] {text}\n")
    _index_file_async(filepath)
    return "Notat."


def cmd_jurnal(args: list[str]) -> str:
    """Append a journal entry under ## Jurnal section in today's file."""
    if not args:
        return "Usage: /jurnal <text>"
    text = " ".join(args)
    today = date.today().isoformat()
    now = datetime.now().strftime("%H:%M")
    filepath = MEMORY_DIR / f"{today}.md"

    if not filepath.exists():
        filepath.write_text(f"# {today}\n\n## Jurnal\n\n", encoding="utf-8")
        content = filepath.read_text(encoding="utf-8")
    else:
        content = filepath.read_text(encoding="utf-8")
        if "## Jurnal" not in content:
            with open(filepath, "a", encoding="utf-8") as f:
                f.write("\n## Jurnal\n\n")
            content = filepath.read_text(encoding="utf-8")

    # Append under ## Jurnal
    entry = f"**{now}** — {text}\n"
    # Find position after ## Jurnal and any existing entries
    jurnal_idx = content.index("## Jurnal")
    # Find next section or end of file
    rest = content[jurnal_idx + len("## Jurnal"):]
    next_section = rest.find("\n## ")
    if next_section == -1:
        # Append at end
        with open(filepath, "a", encoding="utf-8") as f:
            f.write(f"{entry}\n")
    else:
        # Insert before next section
        insert_pos = jurnal_idx + len("## Jurnal") + next_section
        new_content = content[:insert_pos] + f"\n{entry}" + content[insert_pos:]
        filepath.write_text(new_content, encoding="utf-8")

    _index_file_async(filepath)
    return "Jurnal actualizat."


# ---------------------------------------------------------------------------
# Search & KB
# ---------------------------------------------------------------------------

def cmd_search(args: list[str]) -> str:
    """Semantic search over memory/ files."""
    if not args:
        return "Usage: /search <query>"
    query = " ".join(args)
    try:
        from src.memory_search import search
        results = search(query, top_k=5)
        if not results:
            return "Niciun rezultat."
        parts = []
        for r in results:
            score = f"{r['score']:.2f}"
            snippet = r["chunk"][:120].replace("\n", " ")
            parts.append(f"  [{score}] {r['file']} — {snippet}")
        return "Rezultate:\n" + "\n".join(parts)
    except ConnectionError as e:
        return f"Search error (Ollama): {e}"
    except Exception as e:
        return f"Search error: {e}"


def cmd_kb(args: list[str]) -> str:
    """List recent KB notes, optionally filtered by category."""
    index_file = MEMORY_DIR / "kb" / "index.json"
    if not index_file.exists():
        return "KB index not found. Run reindex."
    try:
        data = json.loads(index_file.read_text(encoding="utf-8"))
        notes = data.get("notes", [])
        if not notes:
            return "KB gol."

        category = args[0].lower() if args else None
        if category:
            notes = [n for n in notes if n.get("category", "").lower() == category]
            if not notes:
                return f"Nicio nota in categoria '{category}'."

        # Sort by date desc, take top 10
        notes.sort(key=lambda n: n.get("date", ""), reverse=True)
        notes = notes[:10]

        parts = []
        for n in notes:
            title = n.get("title", "?")
            d = n.get("date", "?")
            cat = n.get("category", "")
            parts.append(f"  {d} [{cat}] {title}")
        return "Ultimele note:\n" + "\n".join(parts)
    except Exception as e:
        return f"KB error: {e}"


# ---------------------------------------------------------------------------
# Reminders
# ---------------------------------------------------------------------------

def cmd_remind(args: list[str]) -> str:
    """Create a Google Calendar reminder event.

    /remind HH:MM text        — today at HH:MM
    /remind YYYY-MM-DD HH:MM text — specific date
    """
    if len(args) < 2:
        return "Usage: /remind <HH:MM> <text> or /remind <YYYY-MM-DD> <HH:MM> <text>"

    # Try to detect date vs time in first arg
    first = args[0]
    if "-" in first and len(first) == 10:
        # YYYY-MM-DD format
        if len(args) < 3:
            return "Usage: /remind <YYYY-MM-DD> <HH:MM> <text>"
        day = first
        time_str = args[1]
        text = " ".join(args[2:])
    else:
        # HH:MM — today
        day = date.today().isoformat()
        time_str = first
        text = " ".join(args[1:])

    # Validate time
    try:
        hour, minute = time_str.split(":")
        int(hour)
        int(minute)
    except (ValueError, AttributeError):
        return f"Invalid time format: {time_str}. Use HH:MM."

    start_iso = f"{day}T{time_str}:00"

    try:
        # Import create_event from calendar_check.py
        import sys
        sys.path.insert(0, str(TOOLS_DIR))
        from calendar_check import create_event
        result = create_event(text, start_iso, duration_minutes=30)
        time_display = time_str
        return f"Reminder setat: {time_display} — {text}"
    except Exception as e:
        return f"Remind error: {e}"
    finally:
        # Clean up sys.path
        if str(TOOLS_DIR) in sys.path:
            sys.path.remove(str(TOOLS_DIR))


# ---------------------------------------------------------------------------
# Ops & Diagnostics
# ---------------------------------------------------------------------------

def cmd_logs(args: list[str]) -> str:
    """Show last N lines from echo-core.log."""
    n = 20
    if args:
        try:
            n = int(args[0])
        except ValueError:
            return "Usage: /logs [N] (default 20)"
    log_file = LOGS_DIR / "echo-core.log"
    if not log_file.exists():
        return "No log file found."
    try:
        lines = log_file.read_text(encoding="utf-8").split("\n")
        tail = lines[-n:] if len(lines) > n else lines
        return "\n".join(tail).strip() or "Log empty."
    except Exception as e:
        return f"Log read error: {e}"


def cmd_doctor(args: list[str]) -> str:
    """Quick diagnostic checks."""
    checks = []

    # Claude CLI
    claude_found = shutil.which("claude") is not None
    checks.append(("Claude CLI", claude_found))

    # Keyring secrets count
    try:
        from src.credential_store import get_secret
        import keyring
        keyring.get_password("echo-core", "_registry")
        checks.append(("Keyring", True))
    except Exception:
        checks.append(("Keyring", False))

    # Config valid
    config_file = PROJECT_ROOT / "config.json"
    try:
        json.loads(config_file.read_text(encoding="utf-8"))
        checks.append(("config.json", True))
    except Exception:
        checks.append(("config.json", False))

    # Ollama reachable (read URL from config)
    try:
        import httpx
        ollama_url = "http://10.0.20.161:11434"
        try:
            cfg_data = json.loads(config_file.read_text(encoding="utf-8"))
            ollama_url = cfg_data.get("ollama", {}).get("url", ollama_url).rstrip("/")
        except Exception:
            pass
        resp = httpx.get(f"{ollama_url}/api/version", timeout=5.0)
        checks.append(("Ollama", resp.status_code == 200))
    except Exception:
        checks.append(("Ollama", False))

    # Disk space
    try:
        stat = os.statvfs(str(PROJECT_ROOT))
        free_gb = (stat.f_bavail * stat.f_frsize) / (1024 ** 3)
        checks.append((f"Disk ({free_gb:.1f}GB free)", free_gb >= 1.0))
    except OSError:
        checks.append(("Disk", False))

    # Format output
    lines = []
    for name, ok in checks:
        icon = "OK" if ok else "FAIL"
        lines.append(f"  [{icon}] {name}")
    passed = sum(1 for _, ok in checks if ok)
    total = len(checks)
    header = f"Doctor: {passed}/{total} checks passed"
    return header + "\n" + "\n".join(lines)


def cmd_heartbeat(args: list[str]) -> str:
    """Force a heartbeat run and return the result."""
    try:
        from src.heartbeat import run_heartbeat
        from src.config import Config
        config = Config()
        result = run_heartbeat(config.data)
        return f"Heartbeat: {result}"
    except Exception as e:
        return f"Heartbeat error: {e}"


def cmd_help(args: list[str]) -> str:
    """List all available fast commands."""
    return """Comenzi disponibile:

Git:
  /commit [msg]     — Commit all changes (auto-message or custom)
  /push             — Push to remote (with safety checks)
  /pull             — Pull with rebase

Dev:
  /test [pattern]   — Run tests (optional -k filter)

Email:
  /email            — Check unread emails
  /email send <to> <subject> :: <body>  — Send email
  /email save       — Save unread emails to KB
  /email digest     — Rezumate emailuri necitite → WhatsApp
  /email forward    — Forward emailuri necitite direct → WhatsApp (fără rezumat)

Calendar:
  /calendar         — Today + tomorrow events
  /calendar week    — This week's schedule
  /calendar busy    — Am I in a meeting now?

Notes:
  /note <text>      — Quick note in daily file
  /jurnal <text>    — Journal entry in daily file

Search:
  /search <query>   — Semantic search over memory/
  /kb [category]    — Recent KB notes

Reminders:
  /remind <HH:MM> <text>             — Reminder today
  /remind <YYYY-MM-DD> <HH:MM> <text> — Reminder on date

Audio:
  /audio <text>                  — TTS pe text
  /audio <url>                   — Extrage articol → audio
  /audio <url> rezumat           — Rezumat Claude → audio (flag oriunde)
  /audio                         — Ultimul răspuns Echo → audio
  /audio M2 [text|url] [rezumat] — Voce specificată (M1-M5, F1-F5)
  /audio ajutor                  — Ajutor detaliat

Ops:
  /logs [N]         — Last N log lines (default 20)
  /doctor           — System diagnostics
  /heartbeat        — Force heartbeat check
  /help             — This message

Session:
  /clear            — Clear Claude session
  /status           — Session info
  /model [name]     — Show/change model"""


# ---------------------------------------------------------------------------
# Audio / TTS
# ---------------------------------------------------------------------------

_VOICES = {"M1", "M2", "M3", "M4", "M5", "F1", "F2", "F3", "F4", "F5"}
_AUDIO_PREFIX = "__AUDIO__:"
_MAX_TTS_CHARS = 3000


def cmd_audio(args: list[str]) -> str:
    """TTS via Supertonic. Returnează __AUDIO__:/cale sau text de eroare.

    Sintaxa:
      /audio                      → ultimul răspuns Echo → audio
      /audio <text>               → text explicit → audio
      /audio <url>                → extrage text principal din URL → audio
      /audio rezumat <url>        → Claude rezumă URL → audio
      /audio M2 [text|url|gol]    → voce specificată (M1-M5, F1-F5)
      /audio ajutor               → ajutor
    """
    voice = "M2"
    remaining = list(args)

    # Detectare voce ca prim token
    if remaining and remaining[0].upper() in _VOICES:
        voice = remaining[0].upper()
        remaining = remaining[1:]

    # Detectare flag "rezumat" oriunde în args (indiferent de ordine)
    do_summarize = False
    if any(t.lower() == "rezumat" for t in remaining):
        do_summarize = True
        remaining = [t for t in remaining if t.lower() != "rezumat"]

    channel_id = _get_ctx_channel()

    # Determinare text sursă
    text: str | None = None

    if not remaining:
        from src.last_response_store import get_last
        text = get_last(channel_id or "")
        if not text:
            return "Nu există un răspuns anterior. Folosește: /audio <text>"

    elif len(remaining) == 1 and remaining[0].lower() == "ajutor":
        return (
            "🎙️ /audio — Text-to-Speech local (Supertonic)\n\n"
            "  /audio <text>                    — TTS pe text dat\n"
            "  /audio <url>                     — extrage articol → audio\n"
            "  /audio <url> rezumat             — rezumat Claude → audio\n"
            "  /audio                           — ultimul răspuns Echo → audio\n"
            "  /audio M2 [text|url] [rezumat]   — voce specifică (M1-M5, F1-F5)\n\n"
            "Flag rezumat: poate fi pus oriunde în comandă\n"
            "  /audio rezumat <url>  ≡  /audio <url> rezumat  ≡  /audio M2 <url> rezumat\n\n"
            "Voci: M1 M2 M3 M4 M5 (masculin) · F1 F2 F3 F4 F5 (feminin)"
        )

    elif len(remaining) == 1 and remaining[0].startswith("http"):
        url = remaining[0]
        if do_summarize:
            extracted = _extract_url_text(url)
            if not extracted:
                return f"Nu am putut extrage text din URL: {url}"
            text = _claude_summarize(extracted)
            if not text:
                return "Rezumatul a eșuat. Încearcă /audio <url> pentru extragere directă."
        else:
            text = _extract_url_text(url)
            if not text:
                return f"Nu am putut extrage text din URL: {url}"

    else:
        text = " ".join(remaining)

    # Trunchiere text lung
    if len(text) > _MAX_TTS_CHARS:
        text = text[:_MAX_TTS_CHARS] + "..."

    # Normalizare RO pentru TTS (numere, monedă, unități, abrevieri) —
    # aceeași expandare ca voice mode live, fără trunchierea la 200 cuvinte
    from src.voice.normalize import expand_for_tts
    text = expand_for_tts(text)

    # Apel TTS (import direct din tools/tts.py)
    result = _tts_synthesize(text, voice)
    if result.get("ok"):
        return f"{_AUDIO_PREFIX}{result['path']}"
    return f"TTS error: {result.get('error', 'necunoscut')}"


def _tts_synthesize(text: str, voice: str) -> dict:
    """Import tools/tts.py și apelează synthesize()."""
    import sys as _sys
    _tools_dir = str(TOOLS_DIR)
    if _tools_dir not in _sys.path:
        _sys.path.insert(0, _tools_dir)
    try:
        import importlib
        import tts as _tts_mod
        # Re-import pentru a prinde modificări la hot-reload
        importlib.reload(_tts_mod)
        return _tts_mod.synthesize(text, voice=voice, lang="ro")
    except ImportError as e:
        return {"ok": False, "error": f"tools/tts.py nu poate fi importat: {e}"}
    except Exception as e:
        return {"ok": False, "error": str(e)}


def split_text_chunks(text: str, max_chars: int = 1500) -> list[str]:
    """Împarte text în chunks pe paragrafe fără a depăși max_chars."""
    import re as _re
    paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()]
    if not paragraphs:
        paragraphs = [p.strip() for p in text.split("\n") if p.strip()]

    chunks: list[str] = []
    current_parts: list[str] = []
    current_len = 0

    for para in paragraphs:
        if len(para) > max_chars:
            if current_parts:
                chunks.append("\n\n".join(current_parts))
                current_parts = []
                current_len = 0
            sentences = _re.split(r'(?<=[.!?])\s+', para)
            for sent in sentences:
                if current_len + len(sent) + 1 > max_chars and current_parts:
                    chunks.append(" ".join(current_parts))
                    current_parts = [sent]
                    current_len = len(sent)
                else:
                    current_parts.append(sent)
                    current_len += len(sent) + 1
        elif current_len + len(para) + 2 > max_chars and current_parts:
            chunks.append("\n\n".join(current_parts))
            current_parts = [para]
            current_len = len(para)
        else:
            current_parts.append(para)
            current_len += len(para) + 2

    if current_parts:
        chunks.append("\n\n".join(current_parts))

    return chunks if chunks else [text[:max_chars]]


def extract_url_text(url: str) -> str | None:
    """Extrage textul principal dintr-un URL (publică)."""
    return _extract_url_text(url)


def _extract_url_text(url: str) -> str | None:
    """Extrage textul principal dintr-un URL cu trafilatura."""
    try:
        import trafilatura
        downloaded = trafilatura.fetch_url(url)
        if not downloaded:
            return None
        return trafilatura.extract(downloaded)
    except ImportError:
        log.warning("trafilatura nu e instalat; folosesc fallback httpx")
        return _extract_url_text_fallback(url)
    except Exception as e:
        log.warning("Extragere URL eșuată pentru %s: %s", url, e)
        return None


def _extract_url_text_fallback(url: str) -> str | None:
    """Fallback simplu: fetch + strip HTML."""
    try:
        import re
        import httpx as _httpx
        resp = _httpx.get(url, timeout=15, follow_redirects=True)
        resp.raise_for_status()
        text = re.sub(r"<[^>]+>", " ", resp.text)
        text = re.sub(r"\s+", " ", text).strip()
        return text[:5000] if text else None
    except Exception:
        return None


def _claude_summarize(text: str) -> str | None:
    """Rezumă text cu claude -p (one-shot, fără sesiune)."""
    prompt = (
        "Rezumă în maxim 200 de cuvinte, în română, "
        f"păstrând informațiile cheie:\n\n{text[:5000]}"
    )
    try:
        result = subprocess.run(
            ["claude", "-p", prompt],
            capture_output=True, text=True, timeout=90,
            cwd=str(PROJECT_ROOT),
        )
        if result.returncode == 0 and result.stdout.strip():
            return result.stdout.strip()
        return None
    except Exception as e:
        log.warning("Claude summarize eșuat: %s", e)
        return None


# ---------------------------------------------------------------------------
# Dispatch
# ---------------------------------------------------------------------------

COMMANDS: dict[str, Callable] = {
    "commit": cmd_commit,
    "push": cmd_push,
    "pull": cmd_pull,
    "test": cmd_test,
    "email": cmd_email,
    "calendar": cmd_calendar,
    "note": cmd_note,
    "jurnal": cmd_jurnal,
    "search": cmd_search,
    "kb": cmd_kb,
    "remind": cmd_remind,
    "logs": cmd_logs,
    "doctor": cmd_doctor,
    "heartbeat": cmd_heartbeat,
    "help": cmd_help,
    "audio": cmd_audio,
}


def dispatch(name: str, args: list[str]) -> str | None:
    """Dispatch a fast command by name. Returns response or None if unknown."""
    handler = COMMANDS.get(name)
    if handler is None:
        return None
    try:
        return handler(args)
    except Exception as e:
        log.exception("Fast command /%s failed", name)
        return f"Error in /{name}: {e}"


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _index_file_async(filepath: Path) -> None:
    """Index a file for semantic search in a background thread."""
    def _do_index():
        try:
            from src.memory_search import index_file
            index_file(filepath)
        except Exception as e:
            log.debug("Background index of %s failed: %s", filepath.name, e)
    threading.Thread(target=_do_index, daemon=True).start()


def _git(*args: str) -> str:
    """Run a git command and return stdout."""
    proc = subprocess.run(
        ["git", *args],
        capture_output=True, text=True, timeout=30,
        cwd=str(PROJECT_ROOT),
    )
    if proc.returncode != 0 and proc.stderr:
        return proc.stderr.strip()
    return proc.stdout.strip()


def _auto_commit_message(files: list[str]) -> str:
    """Generate a commit message from changed files list (porcelain output)."""
    areas = set()
    added = modified = deleted = 0
    for line in files:
        if not line.strip():
            continue
        status_code = line[:2].strip()
        filename = line[3:]
        if "/" in filename:
            areas.add(filename.split("/")[0])
        else:
            areas.add("root")
        if "A" in status_code or "?" in status_code:
            added += 1
        elif "D" in status_code:
            deleted += 1
        else:
            modified += 1

    parts = []
    if added:
        parts.append(f"+{added}")
    if modified:
        parts.append(f"~{modified}")
    if deleted:
        parts.append(f"-{deleted}")
    change_summary = " ".join(parts) if parts else "changes"
    area_list = ", ".join(sorted(areas)[:3])
    if len(areas) > 3:
        area_list += f" +{len(areas) - 3} more"
    return f"Update {area_list} ({change_summary})"
