"""Echo Core message router — routes messages to Claude or commands."""

import json
import logging
import os
import re
import signal
from datetime import datetime, timezone
from pathlib import Path
from typing import Callable

from src.config import Config
from src.fast_commands import dispatch as fast_dispatch, set_channel_context
from src.last_response_store import set_last as _set_last_response
from src.claude_session import (
    send_message,
    clear_session,
    get_active_session,
    list_sessions,
    set_session_model,
    VALID_MODELS,
)
from src.jsonlock import read_locked, write_locked
from src.planning_orchestrator import PlanningOrchestrator
from src.planning_session import (
    clear_planning_state,
    get_planning_state,
    is_in_planning,
)

log = logging.getLogger(__name__)

APPROVED_TASKS_FILE = Path(__file__).parent.parent / "approved-tasks.json"

# Anti-jailbreak: strip user-controlled leading [voice] / [speaker:...]
# tokens so they cannot impersonate the system-injected prefix on voice turns.
_LEADING_VOICE_TOKEN_RE = re.compile(
    r'^\s*(?:\[voice\]|\[speaker:[^\]]*\])\s*', re.IGNORECASE
)


def _strip_leading_voice_tokens(text: str) -> str:
    while True:
        stripped = _LEADING_VOICE_TOKEN_RE.sub('', text, count=1)
        if stripped == text:
            return text
        text = stripped

# Module-level config instance (lazy singleton)
_config: Config | None = None


def _get_config() -> Config:
    """Return the module-level config, creating it on first access."""
    global _config
    if _config is None:
        _config = Config()
    return _config


def route_message(
    channel_id: str,
    user_id: str,
    text: str,
    model: str | None = None,
    on_text: Callable[[str], None] | None = None,
    adapter_name: str | None = None,
) -> tuple[str, bool]:
    """Route an incoming message. Returns (response_text, is_command).

    If text starts with / it's a command (handled here for text-based commands).
    Otherwise it goes to Claude via send_message (auto start/resume).

    *on_text* — optional callback invoked with each intermediate text block
    from Claude, enabling real-time streaming to the adapter.

    *adapter_name* — "discord" / "telegram" / "whatsapp" / None. Used for
    adapter-specific response shaping (e.g., redirect line on WhatsApp).
    """
    text = text.strip()
    text = _strip_leading_voice_tokens(text)

    # ---- Planning state-aware routing -----------------------------------
    # If the channel is in an active planning session, the user's message is
    # part of that conversation — route it to the orchestrator (NOT Claude
    # main session, NOT slash commands except explicit /cancel and /advance).
    in_planning = is_in_planning(adapter_name or "echo", channel_id)
    if in_planning:
        low = text.lower().strip()
        if low in ("/cancel", "/anuleaza", "/anulează", "anulează planning", "anuleaza planning"):
            # Capture slug BEFORE clearing state so we can revert approved-tasks status.
            adapter_key = adapter_name or "echo"
            state_snapshot = get_planning_state(adapter_key, channel_id)
            cleared = PlanningOrchestrator.cancel(adapter_key, channel_id)
            if state_snapshot and state_snapshot.get("slug"):
                _revert_status_for_slug(state_snapshot["slug"], to="pending")
            if cleared:
                return "Planning anulat. Status revenit la pending.", True
            return "Nu era nicio sesiune activă.", True
        if low in ("/advance", "/continua", "/continuă", "continuă faza", "continua faza"):
            session, response, completed = PlanningOrchestrator.advance(
                adapter_name or "echo", channel_id, on_text=on_text,
            )
            return response, True
        if low in ("/finalize", "/dau drumul", "dau drumul"):
            return _approve_from_planning(channel_id, adapter_name or "echo"), True
        if text.startswith("/"):
            # Allow other commands to fall through (e.g. /status, /clear),
            # but skip Ralph dispatch and Claude routing below.
            pass
        else:
            # Plain message → planning conversation.
            try:
                session, response, phase_ready = PlanningOrchestrator.respond(
                    adapter_name or "echo", channel_id, text, on_text=on_text,
                )
                if session is None:
                    # State raced — drop planning marker, fall through.
                    log.warning(
                        "planning state vanished mid-respond for channel=%s", channel_id
                    )
                else:
                    if phase_ready:
                        response = (
                            response
                            + "\n\n— Apasă **Continuă faza** ca să trec la următoarea, "
                            "sau **Anulează** dacă te-ai răzgândit."
                        )
                    return response, False
            except Exception as e:
                log.error("Planning respond failed for %s: %s", channel_id, e)
                return f"Planning blocat: {e}", False

    # Ralph commands — short form (/p /a /l /k) and legacy aliases (!propose !approve !status !stop)
    ralph_response = _try_ralph_dispatch(text, adapter_name=adapter_name)
    if ralph_response is not None:
        return ralph_response, True

    # Text-based commands (not slash commands — these work in any adapter)
    if text.lower() == "/clear":
        default_model = _get_config().get("bot.default_model", "sonnet")
        cleared_text = clear_session(channel_id)
        if cleared_text:
            return f"Session cleared. Model reset to {default_model}.", True
        return "No active session.", True

    if text.lower() == "/status":
        return _status(channel_id), True

    if text.lower().startswith("/model"):
        return _model_command(channel_id, text), True

    if text.startswith("/"):
        parts = text[1:].split()
        cmd_name = parts[0].lower()
        cmd_args = parts[1:]
        set_channel_context(channel_id)
        result = fast_dispatch(cmd_name, cmd_args)
        if result is not None:
            return result, True
        return f"Unknown command: /{cmd_name}", True

    # Regular message → Claude
    if not model:
        # Check session model first, then channel default, then global default
        session = get_active_session(channel_id)
        if session and session.get("model"):
            model = session["model"]
        else:
            channel_cfg = _get_channel_config(channel_id)
            model = (channel_cfg or {}).get("default_model") or _get_config().get("bot.default_model", "sonnet")

    # Voice turns get a system-controlled [voice] [speaker:NAME] prefix so
    # VOICE_MODE.md rules self-activate per-turn. Session key is the plain
    # channel_id — voice + text share one Claude session on the same channel.
    claude_text = text
    voice_mode = adapter_name == "discord-voice"
    if voice_mode:
        user_name = _get_config().get("voice.user_name", "user") or "user"
        claude_text = f"[voice] [speaker:{user_name}] {text}"
    session_key = channel_id

    try:
        response = send_message(
            session_key, claude_text, model=model, on_text=on_text,
            voice_mode=voice_mode,
        )
        _set_last_response(channel_id, response)
        return response, False
    except Exception as e:
        log.error("Claude error for channel %s: %s", channel_id, e)
        return f"Error: {e}", False


def _status(channel_id: str) -> str:
    """Build status message for a channel."""
    session = get_active_session(channel_id)
    if not session:
        return "No active session."

    model = session.get("model", "unknown")
    sid = session.get("session_id", "unknown")[:12]
    count = session.get("message_count", 0)

    return f"Model: {model} | Session: {sid}... | Messages: {count}"


def _model_command(channel_id: str, text: str) -> str:
    """Handle /model [choice] text command."""
    parts = text.strip().split()
    if len(parts) == 1:
        # /model — show current
        session = get_active_session(channel_id)
        if session:
            current = session.get("model", "unknown")
        else:
            channel_cfg = _get_channel_config(channel_id)
            current = (channel_cfg or {}).get("default_model") or _get_config().get("bot.default_model", "sonnet")
        available = ", ".join(sorted(VALID_MODELS))
        return f"Current model: {current}\nAvailable: {available}"

    choice = parts[1].lower()
    if choice not in VALID_MODELS:
        return f"Invalid model '{choice}'. Choose from: {', '.join(sorted(VALID_MODELS))}"

    session = get_active_session(channel_id)
    if session:
        set_session_model(channel_id, choice)
    else:
        # Pre-set for next message
        from src.claude_session import _load_sessions, _save_sessions
        from datetime import datetime, timezone
        sessions = _load_sessions()
        sessions[channel_id] = {
            "session_id": "",
            "model": choice,
            "created_at": datetime.now(timezone.utc).isoformat(),
            "last_message_at": datetime.now(timezone.utc).isoformat(),
            "message_count": 0,
        }
        _save_sessions(sessions)
    return f"Model changed to {choice}."


def _load_approved_tasks() -> dict:
    """Load approved-tasks.json under a shared flock; empty structure if missing."""
    try:
        data = read_locked(str(APPROVED_TASKS_FILE))
    except FileNotFoundError:
        return {"projects": [], "last_updated": None}
    if not data:
        return {"projects": [], "last_updated": None}
    return data


def _save_approved_tasks(data: dict) -> None:
    """Persist approved-tasks.json under an exclusive flock + atomic replace."""
    data["last_updated"] = datetime.now(timezone.utc).isoformat()
    write_locked(str(APPROVED_TASKS_FILE), lambda _existing: data)


RALPH_CMDS = {
    "propose": ("/p", "!propose"),
    "approve": ("/a", "!approve"),
    "list":    ("/l", "!status"),
    "stop":    ("/k", "!stop"),
}


_WHATSAPP_REDIRECT = (
    "\n\n💡 Pentru meniu interactiv folosește Discord sau Telegram."
)


def _maybe_whatsapp_redirect(text: str, adapter_name: str | None) -> str:
    """Append a redirect hint for WhatsApp users so they discover the rich UX."""
    if adapter_name == "whatsapp":
        return text + _WHATSAPP_REDIRECT
    return text


def _translate_whatsapp_text(text: str) -> str | None:
    """Translate WhatsApp text-keyword commands to slash equivalents.

    Acoperă **doar** keyword-urile robuste (single-token + opțional slug):
      - `aprob`            → `/a`            (listează pending)
      - `aprob <slug>`     → `/a <slug>`     (aprobă proiect)
      - `stop <slug>`      → `/k <slug>`     (oprește Ralph)
      - `stare`            → `/l`            (status global)
      - `stare <slug>`     → `/l <slug>`     (status filtrat)

    NU acoperă `propose` — descrierea liberă e prea fragilă pentru parsing
    text-only (utilizatorii ar trimite descrieri multi-line care s-ar
    interpreta greșit). Pentru propose, redirecționăm spre Discord/Telegram.

    Returnează slash command translatat sau None dacă text-ul nu match.
    Case-insensitive pe keyword (slug-ul rămâne ca în input).

    Apelat DOAR pe adapter `whatsapp` în router (nu vrem ca un user pe
    Discord să zică „stop" și să se întâmple ceva).
    """
    if not text or not text.strip():
        return None

    parts = text.strip().split(None, 1)
    keyword = parts[0].lower()
    rest = parts[1].strip() if len(parts) > 1 else ""

    if keyword == "aprob":
        return f"/a {rest}".rstrip()
    if keyword == "stop" and rest:
        # `stop` fără slug ar putea fi colocvial („stop, am uitat ceva") — nu translatăm.
        return f"/k {rest}"
    if keyword == "stare":
        return f"/l {rest}".rstrip()
    return None


def _try_ralph_dispatch(text: str, adapter_name: str | None = None) -> str | None:
    """Parse and dispatch Ralph commands. Returns response string or None if no match."""
    # WhatsApp keyword preprocessing — doar pe whatsapp, înainte de dispatch.
    if adapter_name == "whatsapp":
        translated = _translate_whatsapp_text(text)
        if translated is not None:
            text = translated

    low = text.lower()
    first = low.split(None, 1)[0] if low else ""

    if first in ("/p", "!propose"):
        parts = text.split(None, 2)
        if len(parts) < 3:
            return _maybe_whatsapp_redirect(
                "Folosire: /p <slug> <descriere>\nEx: /p roa2web Homepage redesign cu hero section",
                adapter_name,
            )
        return _ralph_propose(parts[1].strip(), parts[2].strip())

    if first in ("/a", "!approve"):
        parts = text.split(None, 1)
        slugs = []
        if len(parts) > 1:
            slugs = [s.strip() for s in parts[1].replace(",", " ").split() if s.strip()]
        return _ralph_approve(slugs)

    if first in ("/l", "!status"):
        parts = text.split(None, 1)
        filter_slug = parts[1].strip().lower() if len(parts) > 1 else None
        return _maybe_whatsapp_redirect(_ralph_status(filter_slug), adapter_name)

    if first in ("/k", "!stop"):
        parts = text.split(None, 1)
        if len(parts) < 2:
            return "Folosire: /k <slug>"
        return _ralph_stop(parts[1].strip())

    return None


def _parse_propose_flags(text: str) -> tuple[dict, str]:
    """Strip leading --repo/--branch/--base-branch flags from text.

    Returns (flags_dict, remaining_text). Flags are accepted in any order before
    the description. Unknown tokens are left in remaining_text.
    """
    tokens = text.split()
    flags: dict[str, str] = {}
    consumed = 0
    while consumed < len(tokens):
        tok = tokens[consumed]
        if tok in ("--repo", "--branch", "--base-branch") and consumed + 1 < len(tokens):
            key = tok.lstrip("-").replace("-", "_")
            flags[key] = tokens[consumed + 1]
            consumed += 2
        else:
            break
    return flags, " ".join(tokens[consumed:]).strip()


def _ralph_propose(slug: str, description: str) -> str:
    """Adaugă un proiect cu status pending în approved-tasks.json.

    Description may be prefixed with optional flags:
        --repo <name>          Gitea repo to clone (default: slug)
        --branch <name>        Feature branch to create (default: none → main)
        --base-branch <name>   Branch to fork from (default: main)

    Example: /p roa2web-bonuri --repo roa2web --branch feature/bonuri "<descriere>"
    """
    flags, description = _parse_propose_flags(description)
    if not description:
        return "Descriere lipsă după flag-uri. Folosire: /p <slug> [--repo X --branch Y] <descriere>"

    data = _load_approved_tasks()

    for p in data["projects"]:
        if p["name"].lower() == slug.lower():
            return f"Proiectul '{slug}' există deja cu status: {p.get('status', 'unknown')}."

    data["projects"].append({
        "name": slug,
        "description": description,
        "status": "pending",
        "planning_session_id": None,
        "final_plan_path": None,
        "repo": flags.get("repo"),
        "branch": flags.get("branch"),
        "base_branch": flags.get("base_branch"),
        "proposed_at": datetime.now(timezone.utc).isoformat(),
        "approved_at": None,
        "started_at": None,
        "pid": None,
    })
    _save_approved_tasks(data)

    extras = []
    if flags.get("repo"): extras.append(f"repo={flags['repo']}")
    if flags.get("branch"): extras.append(f"branch={flags['branch']}")
    if flags.get("base_branch"): extras.append(f"base={flags['base_branch']}")
    extras_str = f"\n   └ {' · '.join(extras)}" if extras else ""
    return f"📋 Adăugat: {slug}{extras_str}\n   └ {description}\n\nAprobă cu: /a {slug}"


def _ralph_approve(slugs: list[str]) -> str:
    """Aprobă unul sau mai multe proiecte. Listă goală = listează pending."""
    data = _load_approved_tasks()

    if not slugs:
        pending = [p for p in data["projects"] if p.get("status") == "pending"]
        if not pending:
            return "Niciun proiect pending. Adaugă cu /p <slug> <descriere>."
        lines = ["📋 Proiecte pending (aprobă cu /a <slug>):"]
        for p in pending:
            lines.append(f"  • {p['name']}")
            lines.append(f"    └ {p['description'][:80]}")
        return "\n".join(lines)

    approved_info: list[tuple[str, str]] = []
    not_found: list[str] = []

    for slug in slugs:
        found = False
        for p in data["projects"]:
            if p["name"].lower() == slug.lower():
                p["status"] = "approved"
                p["approved_at"] = datetime.now(timezone.utc).isoformat()
                approved_info.append((p["name"], p.get("description", "")))
                found = True
                break
        if not found:
            not_found.append(slug)

    if not_found:
        return f"Nu am găsit: {', '.join(not_found)}. Verifică /l pentru lista completă."

    _save_approved_tasks(data)
    lines = ["✅ Aprobat pentru tonight:"]
    for name, desc in approved_info:
        lines.append(f"  • {name}")
        lines.append(f"    └ {desc[:80]}")
    lines.append("\nNight-execute rulează la 23:00 și implementează stories autonom.")
    return "\n".join(lines)


def _ralph_status(filter_slug: str | None = None) -> str:
    """Status Ralph pentru proiecte. Optional filter pe slug."""
    data = _load_approved_tasks()
    projects = data.get("projects", [])

    if filter_slug:
        projects = [p for p in projects if filter_slug in p["name"].lower()]

    if not projects:
        return "Niciun proiect. Adaugă cu /p <slug> <descriere>."

    status_labels = {
        "approved": "⏳ aștept 23:00",
        "pending":  "📋 pending",
        "complete": "✅ complet",
        "failed":   "❌ eșuat",
        "stopped":  "⏹ oprit",
    }

    lines = ["📊 Proiecte Ralph:"]
    for p in projects:
        status = p.get("status", "unknown")
        name = p["name"]
        desc = p.get("description", "")
        pid = p.get("pid")
        started = p.get("started_at", "")[:16].replace("T", " ") if p.get("started_at") else "-"

        if pid and status == "running":
            try:
                os.kill(pid, 0)
                indicator = f"🟢 PID {pid}"
            except (ProcessLookupError, PermissionError):
                indicator = "🔴 PID mort"
                p["status"] = "stopped"
                _save_approved_tasks(data)
        else:
            indicator = status_labels.get(status, status)

        prd_path = Path(f"/home/moltbot/workspace/{name}/scripts/ralph/prd.json")
        stories_info = ""
        if prd_path.exists():
            try:
                prd = json.loads(prd_path.read_text())
                total = len(prd.get("userStories", []))
                done = sum(1 for s in prd.get("userStories", []) if s.get("passes"))
                stories_info = f" | {done}/{total} stories"
            except Exception:
                pass

        lines.append(f"\n  {name}    {indicator}{stories_info} | Start: {started}")
        if desc:
            lines.append(f"     └ {desc[:80]}")

    return "\n".join(lines)


def _ralph_stop(slug: str) -> str:
    """Oprește Ralph loop (SIGTERM) pentru un proiect."""
    data = _load_approved_tasks()

    for p in data["projects"]:
        if p["name"].lower() == slug.lower():
            desc = p.get("description", "")
            pid = p.get("pid")
            if pid:
                try:
                    os.kill(pid, signal.SIGTERM)
                    p["status"] = "stopped"
                    p["stopped_at"] = datetime.now(timezone.utc).isoformat()
                    _save_approved_tasks(data)
                    return f"⏹ Oprit: {p['name']} (PID {pid})\n   └ {desc[:80]}"
                except ProcessLookupError:
                    p["status"] = "stopped"
                    _save_approved_tasks(data)
                    return f"PID {pid} nu mai rula pentru {p['name']}. Status actualizat."
                except PermissionError:
                    return f"❌ Nu am permisiune să opresc PID {pid}."
            else:
                return f"{p['name']} nu are PID activ (status: {p.get('status', 'unknown')})."

    return f"Proiect '{slug}' nu găsit. Verifică /l pentru lista completă."


def _get_channel_config(channel_id: str) -> dict | None:
    """Find channel config by ID."""
    channels = _get_config().get("channels", {})
    for alias, ch in channels.items():
        if ch.get("id") == channel_id:
            return ch
    return None


# ---------------------------------------------------------------------------
# Planning session entry points (W2)
# ---------------------------------------------------------------------------


def start_planning_session(
    slug: str,
    description: str,
    channel_id: str,
    adapter_name: str,
    on_text: Callable[[str], None] | None = None,
) -> str:
    """Begin a conversational planning session for `slug` on this channel.

    Updates approved-tasks.json: status `planning`, `planning_session_id` set.
    Returns the first response text from the planning agent — the adapter
    will display it and the user replies in the same channel.
    """
    data = _load_approved_tasks()

    # Locate or create the project entry.
    entry = None
    for p in data["projects"]:
        if p["name"].lower() == slug.lower():
            entry = p
            break
    if entry is None:
        entry = {
            "name": slug,
            "description": description,
            "status": "pending",
            "planning_session_id": None,
            "final_plan_path": None,
            "proposed_at": datetime.now(timezone.utc).isoformat(),
            "approved_at": None,
            "started_at": None,
            "pid": None,
        }
        data["projects"].append(entry)

    # Kick off orchestrator (this can take ~60s on first turn — caller should
    # have already shown a "Echo se gândește..." indicator).
    try:
        session, first_response = PlanningOrchestrator.start(
            slug=slug,
            description=description,
            channel_id=channel_id,
            adapter=adapter_name or "echo",
            on_text=on_text,
        )
    except Exception as e:
        log.error("Planning session start failed for %s: %s", slug, e)
        return f"Planning blocat: {e}\n\nÎncearcă din nou cu /plan {slug} <descriere>."

    entry["status"] = "planning"
    entry["planning_session_id"] = session.planning_session_id
    if not entry.get("description"):
        entry["description"] = description
    _save_approved_tasks(data)
    return first_response


def _revert_status_for_slug(slug: str, to: str = "pending") -> None:
    """Revert a project's status (planning → `to`) given its slug."""
    if not slug:
        return
    data = _load_approved_tasks()
    changed = False
    for p in data["projects"]:
        if p["name"].lower() == slug.lower() and p.get("status") == "planning":
            p["status"] = to
            p["planning_session_id"] = None
            changed = True
            break
    if changed:
        _save_approved_tasks(data)


def _approve_from_planning(channel_id: str, adapter_name: str) -> str:
    """User clicked 'Dau drumul' inside an active planning session.

    Promotes status `planning` → `approved` and clears planning state.
    Returns confirmation text.
    """
    state = get_planning_state(adapter_name, channel_id)
    if not state:
        return "Nu există o sesiune de planning activă."
    slug = state.get("slug")
    if not slug:
        return "Sesiunea de planning nu are slug — anulează cu /cancel și ia-o de la capăt."

    data = _load_approved_tasks()
    final_plan_path = state.get("final_plan_path") or str(
        PlanningOrchestrator.final_plan_path(slug)
    )
    found = False
    for p in data["projects"]:
        if p["name"].lower() == slug.lower():
            p["status"] = "approved"
            p["approved_at"] = datetime.now(timezone.utc).isoformat()
            p["planning_session_id"] = None
            p["final_plan_path"] = final_plan_path
            found = True
            break
    if not found:
        return f"Proiectul `{slug}` lipsește din approved-tasks.json. Anulează cu /cancel."
    _save_approved_tasks(data)
    clear_planning_state(adapter_name, channel_id)
    return (
        f"✅ Aprobat: `{slug}`. Ralph începe la 23:00.\n"
        f"   Plan: `{final_plan_path}`"
    )


# Public helpers — re-exported for adapter wiring.
def planning_state_for(channel_id: str, adapter_name: str) -> dict | None:
    """Return current planning state for (adapter, channel) — adapter helper."""
    return get_planning_state(adapter_name, channel_id)


def planning_advance(
    channel_id: str,
    adapter_name: str,
    on_text: Callable[[str], None] | None = None,
) -> tuple[str, bool]:
    """Advance the planning pipeline by one phase.

    Returns (response_text, completed_bool).
    """
    _session, text, completed = PlanningOrchestrator.advance(
        adapter_name, channel_id, on_text=on_text,
    )
    return text, completed


def planning_cancel(channel_id: str, adapter_name: str) -> str:
    """Cancel an active planning session and revert project status."""
    state = get_planning_state(adapter_name, channel_id)
    if not state:
        return "Nu era nicio sesiune de planning activă."
    slug = state.get("slug")
    PlanningOrchestrator.cancel(adapter_name, channel_id)
    if slug:
        _revert_status_for_slug(slug, to="pending")
    return "Planning anulat. Status revenit la pending."


def planning_approve(channel_id: str, adapter_name: str) -> str:
    """Promote planning → approved (e.g. button click 'Dau drumul')."""
    return _approve_from_planning(channel_id, adapter_name)
