"""
Standalone .bat file launcher for all Brynq agent types.

Creates Windows batch files that launch agents in new console windows.
Supports three agent backends:
    1. Claude Code CLI  (claude --yes ...)
    2. Gemini CLI        (gemini ...)
    3. Local LLM         (python -m brynq.agent_loop ...)

Plus a swarm helper that creates N batch files for coordinated multi-agent tasks.

All stdlib -- no external dependencies.

Usage:
    from brynq.launcher import create_claude_bat, create_local_bat, launch_bat

    bat = create_claude_bat("reviewer", "Review the PR", "/path/to/repo")
    pid = launch_bat(bat)
"""

from __future__ import annotations

import os
import subprocess
import sys
from pathlib import Path
from typing import Optional


# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

# Default output directory for generated .bat files.
# Callers may override via the BRYNQ_LAUNCHERS_DIR env var.
_DEFAULT_LAUNCHERS_DIR = Path("state/broker/launchers")


def _launchers_dir() -> Path:
    """Resolve the directory where .bat files are written.

    Priority:
        1. BRYNQ_LAUNCHERS_DIR env var (absolute path)
        2. state/broker/launchers/ relative to cwd
    """
    env = os.environ.get("BRYNQ_LAUNCHERS_DIR")
    if env:
        d = Path(env)
    else:
        d = _DEFAULT_LAUNCHERS_DIR
    d.mkdir(parents=True, exist_ok=True)
    return d


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------


def _safe_bat_string(text: str) -> str:
    """Sanitise arbitrary text for embedding inside a batch file.

    Double-quotes are replaced with single-quotes, newlines collapsed to
    spaces, and percent signs are doubled (batch escaping).
    """
    return (
        text.replace('"', "'")
        .replace("\n", " ")
        .replace("\r", "")
        .replace("%", "%%")
    )


def _write_bat(name: str, content: str, suffix: str = "") -> Path:
    """Write *content* to a .bat file and return its path.

    The file is named ``{name}{suffix}.bat`` inside the launchers directory.
    """
    filename = f"{name}{suffix}.bat"
    bat_path = _launchers_dir() / filename
    bat_path.write_text(content, encoding="utf-8")
    return bat_path


# ---------------------------------------------------------------------------
# Project Context Loader
# ---------------------------------------------------------------------------


def _load_project_context(cwd: str, max_chars: int = 1500) -> str:
    """Read project context from the working directory.

    Checks for CLAUDE.md, README.md, or package.json to understand
    what the project is. Returns a summary string for the system prompt.
    """
    project_dir = Path(cwd)
    context_parts = []

    # Project name from directory
    project_name = project_dir.name
    context_parts.append(f"Project: {project_name}")
    context_parts.append(f"Path: {cwd}")

    # Try CLAUDE.md first (richest context)
    claude_md = project_dir / "CLAUDE.md"
    if claude_md.exists():
        try:
            text = claude_md.read_text("utf-8", errors="replace")
            # Extract first section (usually project stack + critical rules)
            lines = text.splitlines()
            summary_lines = []
            char_count = 0
            for line in lines:
                if char_count > max_chars:
                    break
                # Skip long code blocks and tables
                if line.startswith("```") or line.startswith("|"):
                    continue
                summary_lines.append(line)
                char_count += len(line)
            context_parts.append("Project instructions (from CLAUDE.md):")
            context_parts.append("\n".join(summary_lines))
        except OSError:
            pass

    # Fallback to README.md
    elif (project_dir / "README.md").exists():
        try:
            text = (project_dir / "README.md").read_text("utf-8", errors="replace")
            # Take first 1000 chars
            context_parts.append("Project overview (from README.md):")
            context_parts.append(text[:max_chars])
        except OSError:
            pass

    # Check for pyproject.toml description
    elif (project_dir / "pyproject.toml").exists():
        try:
            text = (project_dir / "pyproject.toml").read_text("utf-8", errors="replace")
            for line in text.splitlines():
                if line.strip().startswith("description"):
                    context_parts.append(f"Project: {line.strip()}")
                    break
        except OSError:
            pass

    # List key directories so agent knows the structure
    key_dirs = []
    for d in sorted(project_dir.iterdir()):
        if d.is_dir() and not d.name.startswith(".") and d.name not in ("node_modules", "__pycache__", ".git", "dist", "build"):
            key_dirs.append(d.name)
    if key_dirs:
        context_parts.append(f"Key directories: {', '.join(key_dirs[:15])}")

    return "\n".join(context_parts)


def _get_project_from_registry(cwd: str) -> dict:
    """Look up a project in the brynq registry by its path."""
    registry_file = Path.home() / ".brynq" / "registry.json"
    if not registry_file.exists():
        return {}
    try:
        registry = __import__("json").loads(registry_file.read_text("utf-8"))
        for key, meta in registry.get("brokers", {}).items():
            broker_dir = meta.get("broker_dir", "")
            # Check if cwd is within this project
            if cwd.replace("\\", "/").startswith(broker_dir.replace("\\", "/").replace("/state/broker", "")):
                return meta
        return {}
    except Exception:
        return {}


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------


def create_claude_bat(name: str, task: str, cwd: str) -> Path:
    """Create a .bat that launches a Claude Code CLI agent.

    Parameters
    ----------
    name:
        Agent session name (used in the window title and env vars).
    task:
        Plain-text task description appended to the system prompt.
    cwd:
        Working directory the agent starts in.

    Returns
    -------
    Path to the generated .bat file.
    """
    safe_task = _safe_bat_string(task)
    project_context = _safe_bat_string(_load_project_context(cwd, max_chars=800))
    system_prompt = (
        f"You are agent '{name}'. "
        f"CONTEXT: {project_context} "
        f"YOUR TASK: {safe_task} "
        f"RULES: Check broker_inbox first. Post status to #general. "
        f"Claim files with broker_claim_file before editing. "
        f"Read files before modifying them. Never overwrite without reading first."
    )

    content = (
        f"@echo off\n"
        f'title Brynq Agent: {name}\n'
        f'cd /d "{cwd}"\n'
        f"set PYTHONPATH=src\n"
        f"set BROKER_SESSION_NAME={name}\n"
        f"set BROKER_PLATFORM=claude\n"
        f'claude --yes --append-system-prompt "{system_prompt}"\n'
        f"pause\n"
    )
    return _write_bat(name, content)


def create_gemini_bat(name: str, task: str, cwd: str) -> Path:
    """Create a .bat that launches a Gemini CLI agent.

    Parameters
    ----------
    name:
        Agent session name.
    task:
        Plain-text task description appended to the system prompt.
    cwd:
        Working directory the agent starts in.

    Returns
    -------
    Path to the generated .bat file.
    """
    safe_task = _safe_bat_string(task)
    project_context = _safe_bat_string(_load_project_context(cwd, max_chars=800))
    system_prompt = (
        f"You are agent '{name}'. "
        f"CONTEXT: {project_context} "
        f"YOUR TASK: {safe_task} "
        f"RULES: Check broker_inbox first. Post status to #general. "
        f"Claim files with broker_claim_file before editing. "
        f"Read files before modifying them. Never overwrite without reading first."
    )

    content = (
        f"@echo off\n"
        f'title Brynq Agent: {name}\n'
        f'cd /d "{cwd}"\n'
        f"set PYTHONPATH=src\n"
        f"set BROKER_SESSION_NAME={name}\n"
        f"set BROKER_PLATFORM=gemini\n"
        f'gemini --yolo -i "{system_prompt}"\n'
        f"pause\n"
    )
    return _write_bat(name, content)


def create_local_bat(
    name: str,
    backend: str,
    model: str,
    channel: str,
    cwd: str,
) -> Path:
    """Create a .bat that launches a local LLM agent via ``brynq.agent_loop``.

    Parameters
    ----------
    name:
        Agent session name (shown in the console title).
    backend:
        LLM backend identifier (e.g. ``"ollama"``, ``"lmstudio"``, ``"openai"``).
    model:
        Model name to pass to the backend (e.g. ``"llama3:latest"``).
    channel:
        Brynq channel the agent monitors.
    cwd:
        Working directory the agent starts in.

    Returns
    -------
    Path to the generated .bat file.
    """
    content = (
        f"@echo off\n"
        f"title Brynq Local Agent: {name} ({model})\n"
        f'cd /d "{cwd}"\n'
        f"set PYTHONPATH=src\n"
        f"set BROKER_SESSION_NAME={name}\n"
        f"set BROKER_PLATFORM={backend}\n"
        f'python -m brynq.agent_loop --name "{name}" --backend "{backend}"'
        f' --model "{model}" --channel "{channel}"\n'
        f"pause\n"
    )
    return _write_bat(name, content, suffix="_local")


def create_swarm_bats(
    swarm_name: str,
    task: str,
    agents: list[dict],
    cwd: str,
) -> list[Path]:
    """Create one .bat file per agent in a coordinated swarm.

    Parameters
    ----------
    swarm_name:
        Human-readable swarm identifier.
    task:
        Shared task description for the swarm.
    agents:
        List of agent definitions.  Each dict must contain:
            - ``name`` (str): unique agent name
            - ``platform`` (str): ``"claude"``, ``"gemini"``, or ``"local"``
        For ``"local"`` agents the dict should also include:
            - ``backend`` (str): e.g. ``"ollama"``
            - ``model`` (str): e.g. ``"llama3:latest"``
            - ``channel`` (str, optional): defaults to ``"swarm-{swarm_name}"``
    cwd:
        Working directory for all agents.

    Returns
    -------
    List of Paths to the generated .bat files (one per agent).
    """
    safe_task = _safe_bat_string(task)
    all_names = [a.get("name", f"{swarm_name}-agent-{i+1}") for i, a in enumerate(agents)]
    swarm_channel = f"swarm-{swarm_name}"
    results: list[Path] = []

    for idx, agent_def in enumerate(agents):
        agent_name = agent_def.get("name", f"{swarm_name}-agent-{idx+1}")
        platform = agent_def.get("platform", "claude").strip().lower()
        others = [n for n in all_names if n != agent_name]

        swarm_preamble = (
            f"You are part of swarm '{swarm_name}'. "
            f"Coordinate via #{swarm_channel}. "
            f"Other agents in your swarm: {', '.join(others)}. "
        )

        if platform == "local":
            # Local LLM agent -- uses brynq.agent_loop
            backend = agent_def.get("backend", "ollama")
            model = agent_def.get("model", "llama3:latest")
            channel = agent_def.get("channel", swarm_channel)

            content = (
                f"@echo off\n"
                f"title Brynq Local Agent: {agent_name} ({model})\n"
                f'cd /d "{cwd}"\n'
                f"set PYTHONPATH=src\n"
                f"set BROKER_SESSION_NAME={agent_name}\n"
                f"set BROKER_PLATFORM={backend}\n"
                f"set BRYNQ_SWARM={swarm_name}\n"
                f"set BRYNQ_SWARM_TASK={safe_task}\n"
                f'python -m brynq.agent_loop --name "{agent_name}" --backend "{backend}"'
                f' --model "{model}" --channel "{channel}"\n'
                f"pause\n"
            )
            bat_path = _write_bat(agent_name, content, suffix="_local")

        else:
            # Claude or Gemini CLI agent
            cli_cmd = "gemini" if platform == "gemini" else "claude"
            system_prompt = (
                f"{swarm_preamble}"
                f"Your task: {safe_task} "
                f"Check broker_inbox immediately. "
                f"Post status updates to #{swarm_channel}. "
                f"Claim files before editing (broker_claim_file). "
                f"Follow the Brynq Constitution."
            )

            if platform == "claude":
                cli_line = f'claude --yes --append-system-prompt "{system_prompt}"'
            else:
                cli_line = f'gemini --yolo -i "{system_prompt}"'

            content = (
                f"@echo off\n"
                f"title Brynq Agent: {agent_name}\n"
                f'cd /d "{cwd}"\n'
                f"set PYTHONPATH=src\n"
                f"set BROKER_SESSION_NAME={agent_name}\n"
                f"set BROKER_PLATFORM={platform}\n"
                f"set BRYNQ_SWARM={swarm_name}\n"
                f"{cli_line}\n"
                f"pause\n"
            )
            bat_path = _write_bat(agent_name, content)

        results.append(bat_path)

    return results


def launch_bat(bat_path: Path) -> int:
    """Launch a .bat file in a new console window and return the process PID.

    Parameters
    ----------
    bat_path:
        Path to the .bat file to execute.

    Returns
    -------
    PID of the launched process.

    Raises
    ------
    FileNotFoundError:
        If *bat_path* does not exist.
    OSError:
        If the process cannot be started.
    """
    bat_path = Path(bat_path)
    if not bat_path.exists():
        raise FileNotFoundError(f"Batch file not found: {bat_path}")

    CREATE_NEW_CONSOLE = 0x00000010
    proc = subprocess.Popen(
        ["cmd", "/c", "start", "", str(bat_path)],
        creationflags=CREATE_NEW_CONSOLE,
    )
    return proc.pid
