"""Check gateway security configuration."""

import os
import subprocess
import sys
from pathlib import Path

from clawguard.models import Finding, Severity
from clawguard.utils import get_total_memory_gb


def _check_port_exposed(port: int) -> bool:
    """Check if a port is listening on all interfaces by parsing ``ss -tlnp``.

    ``ss -tlnp`` columns (0-indexed):
      0: State   1: Recv-Q   2: Send-Q   3: Local Address:Port   4: Peer Address:Port

    Column 3 (Local Address) tells us what the socket is bound to:
      - ``127.0.0.1:PORT`` or ``[::1]:PORT``  => loopback only (safe)
      - ``0.0.0.0:PORT`` or ``*:PORT`` or ``[::]:PORT`` => all interfaces (exposed)

    Column 4 (Peer Address) is always ``0.0.0.0:*`` or ``*:*`` for LISTEN
    sockets and must NOT be used to determine binding.
    """
    if sys.platform != "linux":
        return False
    try:
        result = subprocess.run(
            ["ss", "-tlnp"],
            capture_output=True,
            text=True,
            timeout=5,
        )
        for line in result.stdout.splitlines():
            columns = line.split()
            if len(columns) < 5:
                continue

            local_addr = columns[3]  # e.g. "127.0.0.1:18789" or "0.0.0.0:18789"

            # Extract port from local address — port is after the last ':'
            last_colon = local_addr.rfind(":")
            if last_colon == -1:
                continue
            port_str = local_addr[last_colon + 1:]
            try:
                if int(port_str) != port:
                    continue
            except ValueError:
                continue

            # Extract the host part (everything before the last colon)
            host = local_addr[:last_colon]

            # Check if bound to all interfaces
            if host in ("0.0.0.0", "*", "[::]", "::"):
                return True
        return False
    except (subprocess.SubprocessError, FileNotFoundError, OSError):
        return False


def check_gateway(openclaw_path: Path) -> list[Finding]:
    """Check gateway security settings."""
    findings = []
    config_file = openclaw_path / "openclaw.json"

    if not config_file.exists():
        findings.append(Finding(
            severity=Severity.INFO,
            title="No OpenClaw config file found",
            details=[f"Expected at {config_file}"],
            category="gateway",
        ))
        return findings

    try:
        import json5
        config = json5.loads(config_file.read_text(errors="ignore"))
    except Exception:
        findings.append(Finding(
            severity=Severity.MEDIUM,
            title="Could not parse OpenClaw config file",
            details=["openclaw.json may be malformed"],
            category="gateway",
        ))
        return findings

    gateway = config.get("gateway", {})

    # Check bind setting
    bind = gateway.get("bind", "loopback")
    if bind != "loopback":
        findings.append(Finding(
            severity=Severity.CRITICAL,
            title=f"Gateway bound to non-loopback: {bind}",
            details=[
                f'gateway.bind = "{bind}"',
                "This exposes the gateway to network access",
            ],
            fix='Set gateway.bind to "loopback" in openclaw.json',
            category="gateway",
        ))

    # Check auth token
    token = gateway.get("auth", {}).get("token") or gateway.get("token")
    if not token:
        findings.append(Finding(
            severity=Severity.CRITICAL,
            title="No gateway authentication token configured",
            details=["Anyone with network access can control your OpenClaw agent"],
            fix="Set gateway.auth.token in openclaw.json or re-run openclaw setup",
            category="gateway",
        ))
    elif len(str(token)) < 32:
        findings.append(Finding(
            severity=Severity.HIGH,
            title=f"Gateway auth token is weak ({len(str(token))} chars)",
            details=["Minimum recommended length: 32 characters"],
            fix="Generate a strong token: openssl rand -hex 32",
            category="gateway",
        ))

    # Check port exposure
    port = gateway.get("port", 18789)
    if _check_port_exposed(port):
        findings.append(Finding(
            severity=Severity.HIGH,
            title=f"Gateway port {port} is reachable on all interfaces",
            details=[
                f"Port {port} appears to be listening on 0.0.0.0",
                "The gateway may be accessible from the network",
            ],
            fix="Bind to loopback only and use a reverse proxy if external access is needed",
            category="gateway",
        ))

    # Infrastructure check: RAM
    ram_gb = get_total_memory_gb()
    node_options = os.environ.get("NODE_OPTIONS", "")
    if ram_gb > 0 and ram_gb < 2:
        if "max-old-space-size" not in node_options:
            findings.append(Finding(
                severity=Severity.HIGH,
                title=f"System has only {ram_gb:.1f}GB RAM without NODE_OPTIONS set",
                details=[
                    f"Total RAM: {ram_gb:.1f}GB",
                    "OpenClaw may be killed by OOM without memory limits",
                    "NODE_OPTIONS environment variable is not set",
                ],
                fix='Set NODE_OPTIONS="--max-old-space-size=512" in your service file or shell profile',
                category="gateway",
            ))

    # Infrastructure check: systemd service (Linux only)
    if sys.platform == "linux":
        service_files = [
            Path("/etc/systemd/system/openclaw.service"),
            Path("/etc/systemd/system/openclaw-gateway.service"),
        ]
        for service_file in service_files:
            if not service_file.exists():
                continue
            try:
                service_content = service_file.read_text(errors="ignore")
                # Check missing NODE_OPTIONS in service
                if ram_gb > 0 and ram_gb < 2 and "NODE_OPTIONS" not in service_content:
                    findings.append(Finding(
                        severity=Severity.HIGH,
                        title=f"Systemd service missing NODE_OPTIONS ({service_file.name})",
                        details=[
                            f"Low RAM system ({ram_gb:.1f}GB) but service has no NODE_OPTIONS",
                            "Add Environment=NODE_OPTIONS=--max-old-space-size=512 to [Service] section",
                        ],
                        fix=f"Edit {service_file} and add NODE_OPTIONS, then systemctl daemon-reload",
                        category="gateway",
                    ))
                # Check Restart=always without RestartSec
                if "Restart=always" in service_content and "RestartSec" not in service_content:
                    findings.append(Finding(
                        severity=Severity.MEDIUM,
                        title=f"Systemd service has Restart=always without RestartSec ({service_file.name})",
                        details=[
                            "Service will restart immediately on crash, potentially causing a restart storm",
                            "Add RestartSec=5 to [Service] section to prevent rapid restarts",
                        ],
                        fix=f"Add RestartSec=5 to {service_file} [Service] section",
                        category="gateway",
                    ))
            except (PermissionError, FileNotFoundError):
                pass

    return findings
