"""MCP server for Docker environment access with session isolation.

All output passes through sanitization and redaction before leaving.
Handlers are async, dispatching sync Docker API calls to threads.
"""

from __future__ import annotations

import re

import anyio
import docker.errors as _docker_errors
from mcp.server.fastmcp import FastMCP

from roustabout.audit_renderer import render_findings
from roustabout.auditor import audit
from roustabout.collector import collect
from roustabout.config import Config, load_config
from roustabout.connection import connect
from roustabout.dr_plan import generate as gen_dr_plan
from roustabout.generator import generate
from roustabout.models import DockerEnvironment, make_environment
from roustabout.redactor import redact, resolve_patterns, sanitize, sanitize_environment
from roustabout.renderer import render
from roustabout.session import RateLimiter

RESPONSE_ENVELOPE = "[roustabout]"

# Shared across all docker_manage calls so rate limiting persists
_mcp_rate_limiter = RateLimiter()

mcp = FastMCP(
    "roustabout",
    instructions="Safe, read-only Docker environment visibility. Secrets are redacted.",
)


def _load_cfg() -> Config:
    """Load config, falling back to defaults on any error."""
    try:
        return load_config()
    except (FileNotFoundError, ValueError):
        return Config()


def _collect_redacted() -> tuple[DockerEnvironment, Config]:
    """Collect, sanitize, and redact the Docker environment."""
    cfg = _load_cfg()
    client = connect(cfg.docker_host)
    try:
        env = collect(client)
    finally:
        client.close()
    env = sanitize_environment(env)
    patterns = resolve_patterns(cfg.redact_patterns)
    return redact(env, patterns=patterns), cfg


def _envelope(text: str) -> str:
    """Wrap response in roustabout envelope for structural framing."""
    return f"{RESPONSE_ENVELOPE} {text}"


def _safe_error(exc: Exception) -> str:
    """Return a sanitized error string, stripping potential credential leaks."""
    msg = sanitize(str(exc))
    # Strip URL credentials (user:pass@host patterns)
    msg = re.sub(r"://[^@\s]+@", "://***@", msg)
    return msg


def _enforce_size_limit(text: str, cap: int = 262144) -> str:
    """Truncate response if it exceeds the byte limit."""
    encoded = text.encode("utf-8")
    if len(encoded) <= cap:
        return text
    truncated = encoded[:cap].decode("utf-8", errors="ignore")
    return truncated + "\n\n[Response truncated. Use container-specific queries for details.]"


@mcp.tool()
async def docker_snapshot(show_env: bool = False, show_labels: bool = True) -> str:
    """[OBSERVE] Generate a markdown snapshot of the Docker environment.

    Use when: you need an overview of all running containers.
    Returns: markdown with containers, ports, volumes, networks.

    Args:
        show_env: Include environment variables in output (redacted).
        show_labels: Include container labels in output.
    """
    try:
        env, cfg = await anyio.to_thread.run_sync(_collect_redacted, abandon_on_cancel=False)
    except (ConnectionError, OSError, _docker_errors.DockerException, ValueError) as exc:
        return _envelope(f"Error: Cannot connect to Docker: {_safe_error(exc)}")
    result = render(env, show_env=show_env, show_labels=show_labels)
    return _enforce_size_limit(result, cfg.response_size_cap)


@mcp.tool()
async def docker_audit() -> str:
    """[OBSERVE] Run security checks against the Docker environment.

    Use when: you want to find security issues in the Docker setup.
    Returns: prioritized findings with severity, explanation, and fix.
    """
    try:
        cfg = _load_cfg()

        def _run_audit() -> str:
            client = connect(cfg.docker_host)
            try:
                env = collect(client)
            finally:
                client.close()
            findings = audit(env, patterns=cfg.redact_patterns)
            return render_findings(findings)

        result = await anyio.to_thread.run_sync(_run_audit, abandon_on_cancel=False)
    except (ConnectionError, OSError, _docker_errors.DockerException, ValueError) as exc:
        return _envelope(f"Error: Cannot connect to Docker: {_safe_error(exc)}")
    return _enforce_size_limit(result, cfg.response_size_cap)


@mcp.tool()
async def docker_container(name: str) -> str:
    """[OBSERVE] Get details for a single named container.

    Use when: you need full details on one specific container.
    Returns: markdown for that container with env vars and labels.

    Args:
        name: The container name to look up.
    """
    name = sanitize(name)[:128]
    try:
        env, _cfg = await anyio.to_thread.run_sync(_collect_redacted, abandon_on_cancel=False)
    except (ConnectionError, OSError, _docker_errors.DockerException, ValueError) as exc:
        return _envelope(f"Error: Cannot connect to Docker: {_safe_error(exc)}")
    matches = [c for c in env.containers if c.name == name]
    if not matches:
        available = ", ".join(c.name for c in env.containers)
        return _envelope(f"Container '{name}' not found. Available: {available}")

    single_env = make_environment(
        containers=matches,
        generated_at=env.generated_at,
        docker_version=env.docker_version,
    )
    return render(single_env, show_env=True, show_labels=True)


@mcp.tool()
async def docker_networks() -> str:
    """[OBSERVE] Get network topology showing container connectivity.

    Use when: you need to understand which containers can talk to each other.
    Returns: network list with container memberships.
    """
    try:
        env, _cfg = await anyio.to_thread.run_sync(_collect_redacted, abandon_on_cancel=False)
    except (ConnectionError, OSError, _docker_errors.DockerException, ValueError) as exc:
        return _envelope(f"Error: Cannot connect to Docker: {_safe_error(exc)}")

    from roustabout.renderer import render_network_topology

    return render_network_topology(env)


@mcp.tool()
async def docker_generate(include_stopped: bool = False) -> str:
    """[OBSERVE] Generate docker-compose.yml from running containers.

    Use when: you need a compose file reconstructed from live state.
    Returns: YAML with secrets redacted.

    Args:
        include_stopped: Include stopped containers (default: running only).
    """
    try:
        env, cfg = await anyio.to_thread.run_sync(_collect_redacted, abandon_on_cancel=False)
    except (ConnectionError, OSError, _docker_errors.DockerException, ValueError) as exc:
        return _envelope(f"Error: Cannot connect to Docker: {_safe_error(exc)}")
    return generate(env, include_stopped=include_stopped)


@mcp.tool()
async def docker_dr_plan() -> str:
    """[OBSERVE] Generate a disaster recovery plan summary.

    Use when: you need rebuild instructions for the Docker environment.
    Returns: summary with one line per container showing name, image,
    volume count, network, and restore order position. Use
    docker_dr_detail for full per-container instructions.
    """
    try:
        env, cfg = await anyio.to_thread.run_sync(_collect_redacted, abandon_on_cancel=False)
    except (ConnectionError, OSError, _docker_errors.DockerException, ValueError) as exc:
        return _envelope(f"Error: Cannot connect to Docker: {_safe_error(exc)}")

    def _build_summary() -> str:
        from roustabout.dr_plan import _resolve_dependency_order

        ordered = _resolve_dependency_order(env)
        lines = [
            f"# DR Plan Summary ({len(ordered)} containers)",
            f"Generated: {env.generated_at}",
            "",
            "| # | Container | Image | Volumes | Networks | Status |",
            "|---|-----------|-------|---------|----------|--------|",
        ]
        for i, c in enumerate(ordered, 1):
            nets = ", ".join(n.name for n in c.networks) or c.network_mode or "default"
            lines.append(f"| {i} | {c.name} | {c.image} | {len(c.mounts)} | {nets} | {c.status} |")
        lines.append("")
        lines.append("Use `docker_dr_detail(container_name)` for full restore steps.")
        return "\n".join(lines)

    return _enforce_size_limit(_build_summary(), cfg.response_size_cap)


@mcp.tool()
async def docker_dr_detail(name: str) -> str:
    """[OBSERVE] Get full DR restore instructions for one container.

    Use when: you need step-by-step restore commands for a specific container.
    Returns: volumes, docker run command, network setup, verification steps.

    Args:
        name: The container name to get DR detail for.
    """
    name = sanitize(name)[:128]
    try:
        env, cfg = await anyio.to_thread.run_sync(_collect_redacted, abandon_on_cancel=False)
    except (ConnectionError, OSError, _docker_errors.DockerException, ValueError) as exc:
        return _envelope(f"Error: Cannot connect to Docker: {_safe_error(exc)}")

    matches = [c for c in env.containers if c.name == name]
    if not matches:
        available = ", ".join(c.name for c in env.containers)
        return _envelope(f"Container '{name}' not found. Available: {available}")

    single_env = make_environment(
        containers=matches,
        generated_at=env.generated_at,
        docker_version=env.docker_version,
    )
    return _enforce_size_limit(gen_dr_plan(single_env), cfg.response_size_cap)


@mcp.tool()
async def docker_capabilities() -> str:
    """[OBSERVE] Get operational context — session tier, available tools, lockdown status.

    Use when: you need to understand what operations are available, check
    rate limits, or plan multi-step workflows.
    Returns: JSON with session info, capabilities, and system status.
    """
    import json as _json

    from roustabout.lockdown import LockdownError
    from roustabout.lockdown import check as lockdown_check
    from roustabout.permissions import list_capabilities
    from roustabout.session import (
        DockerSession,
        PermissionTier,
        RateLimiter,
        Session,
        capabilities_for_tier,
    )

    # Default MCP session is OPERATE tier
    tier = PermissionTier.OPERATE
    session = Session(
        id="mcp",
        docker=DockerSession(client=None, host="localhost"),
        tier=tier,
        capabilities=capabilities_for_tier(tier),
        rate_limiter=RateLimiter(),
        created_at="",
    )

    locked = False
    lockdown_reason = None
    try:
        lockdown_check()
    except LockdownError as e:
        locked = True
        lockdown_reason = str(e)

    caps = list_capabilities(session)

    try:
        from importlib.metadata import PackageNotFoundError, version

        ver = version("roustabout")
    except PackageNotFoundError:
        ver = "unknown"

    result = {
        "version": ver,
        "session_tier": tier.value,
        "lockdown": {
            "active": locked,
            "reason": lockdown_reason,
        },
        "capabilities": caps,
    }

    return _json.dumps(result, indent=2)


@mcp.tool()
async def docker_health(container_name: str | None = None) -> str:
    """[OBSERVE] Get container health status and diagnostic detail.

    Use when: you need to check if containers are healthy, see restart
    counts, OOM events, or health check results.
    Returns: markdown table with health status for all or one container.

    Args:
        container_name: Optional — filter to a single container.
    """
    if container_name:
        container_name = sanitize(container_name)[:128]

    try:
        cfg = _load_cfg()

        def _run() -> str:
            from roustabout.health_stats import collect_health, render_health

            client = connect(cfg.docker_host)
            try:
                healths = collect_health(client)
            finally:
                client.close()
            if container_name:
                healths = [h for h in healths if h.name == container_name]
                if not healths:
                    return f"Container '{container_name}' not found"
            return render_health(healths)

        result = await anyio.to_thread.run_sync(_run, abandon_on_cancel=False)
    except (ConnectionError, OSError, _docker_errors.DockerException, ValueError) as exc:
        return _envelope(f"Error: {_safe_error(exc)}")
    return _enforce_size_limit(result, cfg.response_size_cap)


@mcp.tool()
async def docker_stats(container_name: str | None = None) -> str:
    """[OBSERVE] Get live resource usage (CPU, memory, network, I/O).

    Use when: you need to diagnose performance issues or check resource usage.
    Returns: markdown table with resource stats.

    Args:
        container_name: Optional — filter to a single container.
    """
    if container_name:
        container_name = sanitize(container_name)[:128]

    try:
        cfg = _load_cfg()

        def _run() -> str:
            from roustabout.health_stats import collect_stats, render_stats

            client = connect(cfg.docker_host)
            try:
                stats = collect_stats(client, target=container_name)
            finally:
                client.close()
            return render_stats(stats)

        result = await anyio.to_thread.run_sync(_run, abandon_on_cancel=False)
    except (ConnectionError, OSError, _docker_errors.DockerException, ValueError) as exc:
        return _envelope(f"Error: {_safe_error(exc)}")
    return _enforce_size_limit(result, cfg.response_size_cap)


@mcp.tool()
async def docker_logs(
    container_name: str,
    tail: int = 100,
    since: str | None = None,
    grep: str | None = None,
) -> str:
    """[OBSERVE] Read recent container logs.

    Use when: you need to diagnose container issues from log output.
    Note: logs may contain application-generated secrets.
    Returns: sanitized log text.

    Args:
        container_name: The container to read logs from.
        tail: Number of lines from end (default 100).
        since: Time filter — relative ("5m", "1h") or ISO 8601.
        grep: Substring filter — only return matching lines.
    """
    container_name = sanitize(container_name)[:128]

    try:
        cfg = _load_cfg()

        def _run() -> str:
            from roustabout.log_access import (
                ContainerNotFoundError,
                UnsupportedLogDriver,
                collect_logs,
            )

            client = connect(cfg.docker_host)
            try:
                return collect_logs(
                    client,
                    container_name,
                    tail=tail,
                    since=since,
                    grep=grep,
                )
            except ContainerNotFoundError:
                return f"Container '{container_name}' not found"
            except UnsupportedLogDriver as e:
                return str(e)
            finally:
                client.close()

        result = await anyio.to_thread.run_sync(_run, abandon_on_cancel=False)
    except (ConnectionError, OSError, _docker_errors.DockerException, ValueError) as exc:
        return _envelope(f"Error: {_safe_error(exc)}")
    return _enforce_size_limit(result, cfg.response_size_cap)


@mcp.tool()
async def docker_findings(
    severity: str | None = None,
    container: str | None = None,
) -> str:
    """[OBSERVE] Get structured security findings with remediation actions.

    Use when: you need machine-readable audit findings or want to fix issues.
    Returns: JSON array of findings with keys, severity, remediation actions.

    Args:
        severity: Filter by severity (critical, warning, info).
        container: Filter by container name.
    """
    import json as _json

    try:
        env, cfg = await anyio.to_thread.run_sync(_collect_redacted, abandon_on_cancel=False)
    except (ConnectionError, OSError, _docker_errors.DockerException, ValueError) as exc:
        return _envelope(f"Error: {_safe_error(exc)}")

    from roustabout.auditor import audit as run_audit

    findings = run_audit(
        env,
        patterns=cfg.redact_patterns,
        severity_overrides=cfg.severity_overrides,
    )

    if severity:
        severity = sanitize(severity).lower()
        findings = [f for f in findings if f.severity.value == severity]

    if container:
        container = sanitize(container)[:128]
        findings = [f for f in findings if f.container == container]

    result = [
        {
            "key": f.key,
            "severity": f.severity.value,
            "category": f.category,
            "container": f.container,
            "explanation": f.explanation,
            "fix": f.fix,
            "remediation": f.remediation,
            "remediation_action": f.remediation_action,
            "remediation_tier": f.remediation_tier,
        }
        for f in findings
    ]

    return _enforce_size_limit(
        _json.dumps(result, indent=2),
        cfg.response_size_cap,
    )


@mcp.tool()
async def docker_net_check(
    source: str | None = None,
    target: str | None = None,
) -> str:
    """[OBSERVE] Check network connectivity between containers.

    Use when: you need to know if container A can reach container B.
    With both args: checks one pair. With no args: checks all pairs.
    Returns: reachability result with reason (shared networks, host mode, etc).

    Args:
        source: Source container name (optional — omit both for all pairs).
        target: Target container name (optional — omit both for all pairs).
    """
    if source:
        source = sanitize(source)[:128]
    if target:
        target = sanitize(target)[:128]

    if (source is None) != (target is None):
        return _envelope("Error: Provide both source and target, or neither for all pairs.")

    try:
        env, cfg = await anyio.to_thread.run_sync(_collect_redacted, abandon_on_cancel=False)
    except (ConnectionError, OSError, _docker_errors.DockerException, ValueError) as exc:
        return _envelope(f"Error: Cannot connect to Docker: {_safe_error(exc)}")

    from roustabout.net_check import check_all_connectivity, check_connectivity

    if source and target:
        results = [check_connectivity(env, source, target)]
    else:
        results = check_all_connectivity(env)

    lines = []
    for r in results:
        icon = "✓" if r.reachable else "✗"
        lines.append(f"  {icon} {r.source} → {r.target}: {r.reason}")

    if not lines:
        return _envelope("No container pairs to check.")

    return _enforce_size_limit("\n".join(lines), cfg.response_size_cap)


@mcp.tool()
async def docker_manage(
    action: str,
    container_name: str,
    dry_run: bool = False,
) -> str:
    """[OPERATE] Start, stop, or restart a container.

    Use when: you need to manage container lifecycle.
    Returns: result of the operation.

    Args:
        action: One of 'start', 'stop', 'restart'.
        container_name: The container to act on.
        dry_run: Preview without executing (default: false).
    """
    container_name = sanitize(container_name)[:128]
    action = sanitize(action)[:32]

    valid_actions = {"start", "stop", "restart"}
    if action not in valid_actions:
        return _envelope(
            f"Invalid action '{action}'. Must be one of: {', '.join(sorted(valid_actions))}"
        )

    def _run() -> str:
        from roustabout.gateway import MutationCommand
        from roustabout.gateway import execute as gw_execute
        from roustabout.session import (
            DockerSession,
            PermissionTier,
            Session,
            capabilities_for_tier,
        )

        cfg = _load_cfg()
        client = connect(cfg.docker_host)
        docker_session = DockerSession(
            client=client,
            host=cfg.docker_host or "localhost",
        )
        session = Session(
            id="mcp",
            docker=docker_session,
            tier=PermissionTier.OPERATE,
            capabilities=capabilities_for_tier(PermissionTier.OPERATE),
            rate_limiter=_mcp_rate_limiter,
            created_at="",
        )

        cmd = MutationCommand(
            action=action,
            target=container_name,
            dry_run=dry_run,
        )

        try:
            result = gw_execute(cmd, session=session)
        finally:
            client.close()

        if result.success:
            if result.result == "dry-run":
                return f"[dry-run] Would {action} {container_name}"
            return f"{action.capitalize()}ed {container_name}"
        return f"Failed: {result.error or result.gate_failed}"

    try:
        msg = await anyio.to_thread.run_sync(_run, abandon_on_cancel=False)
    except (ConnectionError, OSError, _docker_errors.DockerException, ValueError) as exc:
        return _envelope(f"Error: {_safe_error(exc)}")
    return _envelope(msg)


def main() -> None:
    """Entry point for roustabout-mcp."""
    mcp.run()


if __name__ == "__main__":
    main()
