"""Run command implementation."""

from __future__ import annotations

import json
import os
import sys
import time
from datetime import datetime, timezone
from pathlib import Path
from typing import Annotated, Any

import typer
from rich.prompt import Confirm, Prompt

from vibepod import __version__
from vibepod.constants import EXIT_DOCKER_NOT_RUNNING, SUPPORTED_AGENTS
from vibepod.core.agents import (
    agent_config_dir,
    effective_agent_image,
    get_agent_shortcut,
    get_agent_spec,
    resolve_agent_name,
)
from vibepod.core.config import get_config
from vibepod.core.docker import DockerClientError, DockerManager, _is_latest_tag
from vibepod.core.session_logger import SessionLogger
from vibepod.utils.console import error, info, success, warning


def _parse_env_pairs(values: list[str]) -> dict[str, str]:
    parsed: dict[str, str] = {}
    for entry in values:
        if "=" not in entry:
            raise typer.BadParameter(f"Invalid --env value '{entry}', expected KEY=VALUE")
        key, value = entry.split("=", 1)
        if not key:
            raise typer.BadParameter("Environment variable key cannot be empty")
        parsed[key] = value
    return parsed


def _agent_init_commands(agent: str, agent_cfg: dict[str, Any]) -> list[str]:
    """Read and validate per-agent init commands from config."""
    raw_init = agent_cfg.get("init", [])
    if raw_init is None:
        return []

    if isinstance(raw_init, str):
        items = [raw_init]
    elif isinstance(raw_init, list):
        items = raw_init
    else:
        raise typer.BadParameter(
            f"Invalid agents.{agent}.init value, expected a string or list of strings."
        )

    commands: list[str] = []
    for index, item in enumerate(items, start=1):
        if not isinstance(item, str):
            raise typer.BadParameter(
                f"Invalid agents.{agent}.init[{index}] value, expected a string."
            )
        command = item.strip()
        if not command:
            raise typer.BadParameter(
                f"Invalid agents.{agent}.init[{index}] value, cannot be empty."
            )
        commands.append(command)
    return commands


def _init_entrypoint(init_commands: list[str]) -> list[str]:
    """Build a shell entrypoint that runs init commands before the agent command."""
    script = "\n".join(
        [
            "set -e",
            *init_commands,
            'exec "$@"',
        ]
    )
    return ["/bin/sh", "-lc", script, "--"]


def _get_container_ip(container: Any, network: str) -> str | None:
    """Extract the container's IP address on the given Docker network."""
    try:
        network_settings = container.attrs.get("NetworkSettings")
        if not isinstance(network_settings, dict):
            return None
        networks = network_settings.get("Networks")
        if not isinstance(networks, dict):
            return None
        network_data = networks.get(network)
        if not isinstance(network_data, dict):
            return None
        ip = network_data.get("IPAddress")
        return ip if isinstance(ip, str) and ip else None
    except AttributeError:
        return None


def _update_container_mapping(
    mapping_path: Path,
    ip: str,
    container_id: str,
    container_name: str,
    agent: str,
) -> bool:
    """Merge a new IP→container entry into containers.json atomically."""
    mapping: dict[str, dict[str, str]] = {}
    try:
        if mapping_path.exists():
            try:
                mapping = json.loads(mapping_path.read_text())
            except (json.JSONDecodeError, OSError):
                pass

        mapping[ip] = {
            "container_id": container_id,
            "container_name": container_name,
            "agent": agent,
            "started_at": datetime.now(timezone.utc).isoformat(),
        }

        tmp_path = mapping_path.with_suffix(".tmp")
        tmp_path.write_text(json.dumps(mapping, indent=2))
        os.replace(tmp_path, mapping_path)
    except OSError:
        return False
    return True


def _agent_extra_volumes(agent: str, config_dir: Path) -> list[tuple[str, str, str]]:
    """Return agent-specific bind mounts as (host_path, container_path, mode)."""
    if agent == "auggie":
        host = str(config_dir / ".augment")
        return [
            (host, "/root/.augment", "rw"),
            (host, "/home/node/.augment", "rw"),
        ]
    if agent == "copilot":
        host = str(config_dir / ".copilot")
        return [
            (host, "/root/.copilot", "rw"),
            (host, "/home/node/.copilot", "rw"),
            (host, "/home/coder/.copilot", "rw"),
        ]
    return []


def _x11_volumes_and_env(display: str) -> tuple[list[tuple[str, str, str]], dict[str, str]]:
    """Return X11 socket volumes and DISPLAY env for paste-image support."""
    volumes: list[tuple[str, str, str]] = [("/tmp/.X11-unix", "/tmp/.X11-unix", "rw")]
    env: dict[str, str] = {"DISPLAY": display}
    return volumes, env


def _host_user() -> str | None:
    """Return current user id in uid:gid format when available."""
    getuid = getattr(os, "getuid", None)
    getgid = getattr(os, "getgid", None)
    if not callable(getuid) or not callable(getgid):
        return None
    return f"{getuid()}:{getgid()}"


def _compose_file_present(workspace: Path) -> bool:
    return (workspace / "docker-compose.yml").exists() or (workspace / "compose.yml").exists()


def _maybe_select_network(
    workspace: Path,
    manager: DockerManager,
    primary_network: str,
) -> str | None:
    if not _compose_file_present(workspace):
        return None
    if not sys.stdin.isatty():
        warning("Compose file detected but stdin is not interactive; skipping network prompt.")
        return None

    networks = manager.networks_with_running_containers()
    excluded = {primary_network, "bridge", "host", "none"}
    candidates = [name for name in networks if name not in excluded]
    if not candidates:
        return None

    if not Confirm.ask(
        "Compose file detected. Connect this container to a network with running containers?",
        default=False,
    ):
        return None

    info("Available networks:")
    choices: dict[str, str] = {}
    for idx, name in enumerate(candidates, start=1):
        key = str(idx)
        choices[key] = name
        info(f"{key}. {name}")

    selected = Prompt.ask("Select a network", choices=list(choices.keys()), default="1")
    return choices[selected]


def run(
    agent: Annotated[str | None, typer.Argument(help="Agent to run")] = None,
    workspace: Annotated[
        Path, typer.Option("-w", "--workspace", help="Workspace directory")
    ] = Path("."),
    pull: Annotated[bool, typer.Option("--pull", help="Pull latest image before run")] = False,
    detach: Annotated[
        bool, typer.Option("-d", "--detach", help="Run container in background")
    ] = False,
    env: Annotated[
        list[str] | None,
        typer.Option("-e", "--env", help="Environment variable KEY=VALUE", show_default=False),
    ] = None,
    name: Annotated[str | None, typer.Option("--name", help="Custom container name")] = None,
    network: Annotated[
        str | None,
        typer.Option("--network", help="Additional Docker network to connect the container to"),
    ] = None,
    paste_images: Annotated[
        bool,
        typer.Option(
            "--paste-images",
            help="Enable image pasting via X11 clipboard (requires DISPLAY to be set)",
        ),
    ] = False,
    ikwid: Annotated[
        bool,
        typer.Option(
            "--ikwid",
            help="I Know What I'm Doing: enable auto-approval / skip permission prompts",
        ),
    ] = False,
) -> None:
    """Start an agent container."""
    config = get_config()
    selected_agent_input = agent or str(config.get("default_agent", "claude"))
    selected_agent = resolve_agent_name(selected_agent_input)
    if selected_agent is None:
        supported_labels: list[str] = []
        for supported_agent in SUPPORTED_AGENTS:
            shortcut = get_agent_shortcut(supported_agent)
            supported_labels.append(
                f"{supported_agent} ({shortcut})" if shortcut else supported_agent
            )
        error(f"Unknown agent '{selected_agent_input}'. Supported: {', '.join(supported_labels)}")
        raise typer.Exit(1)

    workspace_path = workspace.expanduser().resolve()
    if not workspace_path.exists() or not workspace_path.is_dir():
        raise typer.BadParameter(f"Workspace not found: {workspace_path}")

    agent_cfg = config.get("agents", {}).get(selected_agent, {})
    spec = get_agent_spec(selected_agent)
    init_commands = _agent_init_commands(selected_agent, agent_cfg)
    merged_env = {
        "USER_UID": str(os.getuid()),
        "USER_GID": str(os.getgid()),
        **spec.extra_env,
        **{str(k): str(v) for k, v in agent_cfg.get("env", {}).items()},
        **_parse_env_pairs(env or []),
    }

    llm_cfg = config.get("llm", {})
    llm_command_extra: list[str] = []
    if llm_cfg.get("enabled") and spec.llm_env_map:
        llm_values = {
            "base_url": str(llm_cfg.get("base_url", "")).strip(),
            "api_key": str(llm_cfg.get("api_key", "")).strip(),
            "model": str(llm_cfg.get("model", "")).strip(),
        }
        for key, env_var in spec.llm_env_map.items():
            value = llm_values.get(key, "")
            if value:
                targets = [env_var] if isinstance(env_var, str) else env_var
                for target in targets:
                    merged_env.setdefault(target, value)
        llm_model = llm_values["model"]
        if llm_model and spec.llm_model_args:
            llm_command_extra = [*spec.llm_model_args, llm_model]

    image = effective_agent_image(selected_agent, config)

    try:
        manager = DockerManager()
    except DockerClientError as exc:
        error(str(exc))
        raise typer.Exit(EXIT_DOCKER_NOT_RUNNING) from exc

    network_name = str(config.get("network", "vibepod-network"))
    manager.ensure_network(network_name)
    extra_network = network or _maybe_select_network(workspace_path, manager, network_name)

    agent_auto_pull = agent_cfg.get("auto_pull")
    auto_pull_enabled = (
        agent_auto_pull if agent_auto_pull is not None else bool(config.get("auto_pull", False))
    )
    should_pull = pull or (auto_pull_enabled and _is_latest_tag(image))
    if should_pull:
        info(f"Pulling image: {image}")
        manager.pull_image(image)

    command = spec.command
    entrypoint: list[str] | None = None
    if init_commands:
        info(f"Applying {len(init_commands)} init command(s) before startup")
        try:
            command = manager.resolve_launch_command(image=image, command=spec.command)
        except DockerClientError as exc:
            error(str(exc))
            raise typer.Exit(1) from exc
        entrypoint = _init_entrypoint(init_commands)

    if ikwid:
        if spec.ikwid_args:
            info(f"IKWID mode: appending {spec.ikwid_args} to {selected_agent} command")
            command = list(command or []) + spec.ikwid_args
        else:
            warning(f"IKWID mode not supported for agent '{selected_agent}', ignoring")

    if llm_command_extra:
        command = list(command or []) + llm_command_extra

    config_dir = agent_config_dir(selected_agent)
    config_dir.mkdir(parents=True, exist_ok=True)

    proxy_cfg = config.get("proxy", {})
    proxy_enabled = bool(proxy_cfg.get("enabled", True))
    proxy_ca_dir_value = str(proxy_cfg.get("ca_dir", "")).strip()
    proxy_ca_path_value = str(proxy_cfg.get("ca_path", "")).strip()
    proxy_ca_dir = Path(proxy_ca_dir_value).expanduser().resolve() if proxy_ca_dir_value else None
    proxy_ca_path = (
        Path(proxy_ca_path_value).expanduser().resolve() if proxy_ca_path_value else None
    )
    proxy_db_path: Path | None = None

    extra_volumes = _agent_extra_volumes(selected_agent, config_dir)
    for host_path, _, _ in extra_volumes:
        Path(host_path).mkdir(parents=True, exist_ok=True)

    if paste_images:
        display = os.environ.get("DISPLAY", "")
        if not display:
            warning("--paste-images requires DISPLAY to be set; skipping X11 forwarding")
        else:
            x11_vols, x11_env = _x11_volumes_and_env(display)
            extra_volumes.extend(x11_vols)
            merged_env.update(x11_env)

    if proxy_enabled:
        proxy_image = str(proxy_cfg.get("image", "vibepod/proxy:latest"))
        proxy_db_path = (
            Path(str(proxy_cfg.get("db_path", "~/.config/vibepod/proxy/proxy.db")))
            .expanduser()
            .resolve()
        )

        if _is_latest_tag(proxy_image):
            manager.pull_if_newer(proxy_image)

        manager.ensure_proxy(
            image=proxy_image,
            db_path=proxy_db_path,
            ca_dir=proxy_ca_dir or proxy_db_path.parent / "mitmproxy",
            network=network_name,
        )

        if proxy_ca_path:
            ca_ready = False
            deadline = time.time() + 10
            while time.time() < deadline:
                if proxy_ca_path.exists():
                    ca_ready = True
                    break
                time.sleep(0.25)
            if not ca_ready:
                warning(f"Proxy CA not found yet at {proxy_ca_path}")

        proxy_url = "http://vibepod-proxy:8080"
        merged_env.setdefault("HTTP_PROXY", proxy_url)
        merged_env.setdefault("HTTPS_PROXY", proxy_url)
        merged_env.setdefault("NO_PROXY", "localhost,127.0.0.1,::1")
        _ca = "/etc/vibepod-proxy-ca/mitmproxy-ca-cert.pem"
        merged_env.setdefault("NODE_EXTRA_CA_CERTS", _ca)
        merged_env.setdefault("REQUESTS_CA_BUNDLE", _ca)
        merged_env.setdefault("SSL_CERT_FILE", _ca)
        merged_env.setdefault("CURL_CA_BUNDLE", _ca)

        if proxy_ca_dir:
            extra_volumes.append((str(proxy_ca_dir), "/etc/vibepod-proxy-ca", "ro"))

    info(f"Starting {selected_agent} with image {image}")
    container_user = _host_user() if spec.run_as_host_user else None
    container = manager.run_agent(
        agent=selected_agent,
        image=image,
        workspace=workspace_path,
        config_dir=config_dir,
        config_mount_path=spec.config_mount_path,
        env=merged_env,
        command=command,
        auto_remove=bool(config.get("auto_remove", True)),
        name=name,
        version=__version__,
        network=network_name,
        extra_volumes=extra_volumes,
        platform=spec.platform,
        user=container_user,
        entrypoint=entrypoint,
    )

    container.reload()
    if container.status != "running":
        recent = container.logs(tail=50).decode("utf-8", errors="replace")
        error("Container exited immediately after start.")
        if recent.strip():
            print(recent)
        raise typer.Exit(1)

    if extra_network and extra_network != network_name:
        try:
            manager.connect_network(container, extra_network)
            info(f"Connected to additional network: {extra_network}")
        except DockerClientError as exc:
            warning(str(exc))

    if proxy_db_path is not None:
        container_ip = _get_container_ip(container, network_name)
        if container_ip:
            mapping_path = proxy_db_path.parent / "containers.json"
            mapping_updated = _update_container_mapping(
                mapping_path, container_ip, container.id, container.name, selected_agent
            )
            if not mapping_updated:
                warning(
                    f"Could not write proxy container mapping at {mapping_path}. "
                    "Fix proxy directory permissions to restore container attribution."
                )

    if detach:
        success(f"Started {container.name}")
        return

    log_cfg = config.get("logging", {})
    log_enabled = bool(log_cfg.get("enabled", True))
    log_db_path = (
        Path(str(log_cfg.get("db_path", "~/.config/vibepod/logs.db"))).expanduser().resolve()
    )

    logger = SessionLogger(log_db_path, enabled=log_enabled)
    logger.open_session(
        agent=selected_agent,
        image=image,
        workspace=str(workspace_path),
        container_id=container.id,
        container_name=container.name,
        vibepod_version=__version__,
    )

    exit_reason = "normal"
    warning("Attached to container. Use Ctrl+C to stop.")
    try:
        manager.attach_interactive(container, logger=logger)
    except KeyboardInterrupt:
        exit_reason = "keyboard_interrupt"
        info("Stopping container...")
        container.stop(timeout=10)
        success("Stopped")
    except Exception:
        exit_reason = "error"
        raise
    finally:
        logger.close_session(exit_reason)
