#!/usr/bin/env python3
"""Asistente de vinculación Linux MCP CCSM.

El código temporal y el token solo se envían sobre HTTPS verificado.
El instalador incorpora la CA pública de confianza de PRINCIPAL y verifica
HTTPS automáticamente. No solicita certificados, huellas ni rutas.
"""
from __future__ import annotations

import getpass
import hashlib
import json
import os
import pwd
import re
import ssl
import subprocess
import sys
import uuid
from pathlib import Path
from urllib.error import HTTPError, URLError
from urllib.parse import urlsplit
from urllib.request import HTTPRedirectHandler, HTTPSHandler, Request, build_opener

CONFIG_DIR = Path("/etc/ccsm-linux-bridge")
CONFIG = CONFIG_DIR / "agent.json"
CA_TARGET = CONFIG_DIR / "lan-ca.pem"
BUNDLED_CA = Path("/opt/ccsm-linux-bridge/ccsm-root-ca.pem")
TRUSTED_CA_SHA256 = "a5cee740c46d0c31dae4e5950b0da8d656e7e5b6f7aee3770cf59e21388a2d11"
DEFAULT_ORIGIN = "https://192.168.100.250"
DISCOVERY_PATH = "/.well-known/ccsm-bridge.json"
PAIR_PATH = "/api/v1/agent/pair"


class PairingError(Exception):
    pass


class NoRedirect(HTTPRedirectHandler):
    def redirect_request(self, request, fp, code, msg, headers, newurl):
        return None


def verified_origin(origin: str) -> str:
    origin = origin.strip().rstrip("/")
    parsed = urlsplit(origin)
    if (parsed.scheme != "https" or not parsed.netloc or parsed.path
            or parsed.query or parsed.fragment or parsed.username or parsed.password):
        raise PairingError("Se requiere una URL de origen HTTPS sin rutas ni credenciales.")
    return origin


def linked_agent_id(path: Path) -> str | None:
    if not path.exists():
        return None
    try:
        config = json.loads(path.read_text(encoding="utf-8"))
        token = config.get("agent_token", "")
        agent_id = config.get("agent_id", "")
        if not isinstance(token, str) or len(token) < 32 or token.startswith("REPLACE_"):
            return None
        return str(uuid.UUID(str(agent_id)))
    except (ValueError, OSError, TypeError, AttributeError):
        raise PairingError("Existe una configuración local inválida. No se reemplazará automáticamente.") from None


def http_opener(context: ssl.SSLContext):
    return build_opener(NoRedirect(), HTTPSHandler(context=context))


def discover(origin: str, opener) -> None:
    req = Request(origin + DISCOVERY_PATH, headers={"Accept": "application/json"})
    with opener.open(req, timeout=15) as response:
        if response.status != 200:
            raise PairingError("Descubrimiento MCP no disponible.")
        payload = response.read(4097)
    if len(payload) > 4096:
        raise PairingError("Respuesta de descubrimiento demasiado extensa.")
    manifest = json.loads(payload)
    if not isinstance(manifest, dict) or (
        manifest.get("service") != "ccsm-linux-bridge"
        or manifest.get("schema_version") != 1
        or manifest.get("transport") != "https-outbound-poll"
        or manifest.get("agent_api_path") != "/api/v1/agent"
    ):
        raise PairingError("El servidor no es el MCP CCSM esperado.")


def securely_write_config(data: dict, ca_pem: bytes | None) -> None:
    account = pwd.getpwnam("ccsm-bridge")
    if not CONFIG_DIR.is_dir():
        raise PairingError("El agente no está instalado. Primero ejecute el instalador.")

    def write_private(target: Path, contents: bytes):
        temporary = target.with_name(target.name + ".pending")
        fd = os.open(str(temporary), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
        try:
            with os.fdopen(fd, "wb") as handle:
                handle.write(contents)
                handle.flush()
                os.fsync(handle.fileno())
            os.chown(temporary, account.pw_uid, account.pw_gid)
            os.replace(temporary, target)
        finally:
            if temporary.exists():
                temporary.unlink()

    if ca_pem is not None:
        write_private(CA_TARGET, ca_pem)

    config = {
        "agent_id": data["agent_id"],
        "agent_token": data["agent_token"],
        "discovery_origin": data["origin"],
        "tls_ca_file": str(CA_TARGET) if ca_pem is not None else "",
        "poll_seconds": 5,
        "command_timeout_seconds": 20,
        "allowed_services": [],
    }
    # No secrets in stdout, terminal args, URLs, audit events or Git.
    write_private(CONFIG, (json.dumps(config, indent=2) + "\n").encode("utf-8"))


def pair() -> None:
    if os.geteuid() != 0:
        raise PairingError("Ejecute el asistente con sudo /usr/local/bin/ccsm-pair.")
    origin = verified_origin(DEFAULT_ORIGIN)
    existing_id = linked_agent_id(CONFIG)

    # The CA is distributed inside the installer. Never accept an untrusted
    # certificate, download a replacement CA or ask users for fingerprints.
    if not BUNDLED_CA.is_file():
        raise PairingError("Falta la CA institucional integrada. Actualice el instalador.")
    pinned_ca = BUNDLED_CA.read_bytes()
    if hashlib.sha256(pinned_ca).hexdigest() != TRUSTED_CA_SHA256:
        raise PairingError("La CA integrada no coincide con el origen institucional.")
    context = ssl.create_default_context()
    context.load_verify_locations(cadata=pinned_ca.decode("ascii"))

    opener = http_opener(context)
    discover(origin, opener)
    print("Conexión HTTPS y origen MCP CCSM verificados.")

    code = getpass.getpass("Código temporal de vinculación (no aparecerá en pantalla): ").strip().lower()
    if not re.fullmatch(r"[a-f0-9]{48}", code):
        raise PairingError("El código debe contener 48 caracteres hexadecimales.")

    payload = {"code": code}
    if existing_id:
        payload["existing_agent_id"] = existing_id

    req = Request(
        origin + PAIR_PATH,
        data=json.dumps(payload).encode("utf-8"),
        headers={"Content-Type": "application/json", "Accept": "application/json"},
        method="POST",
    )
    try:
        with opener.open(req, timeout=15) as response:
            result = json.load(response)
    except HTTPError as error:
        raise PairingError("Vinculación no autorizada o código caducado (HTTP " + str(error.code) + ").") from None

    if not isinstance(result, dict):
        raise PairingError("Respuesta de vinculación inválida.")
    agent_id = str(uuid.UUID(str(result.get("agent_id", ""))))
    secret = result.get("agent_token")
    if (result.get("platform") != "linux" or not isinstance(secret, str)
            or len(secret) < 32 or (existing_id and existing_id != agent_id)):
        raise PairingError("Identidad de agente inesperada. No se escribirá la configuración.")

    securely_write_config({"origin": origin, "agent_id": agent_id, "agent_token": secret}, pinned_ca)
    print("Credenciales e identidad guardadas con permisos restringidos.")
    subprocess.run(["systemctl", "enable", "ccsm-linux-bridge.service"], check=True)
    subprocess.run(["systemctl", "restart", "ccsm-linux-bridge.service"], check=True)
    subprocess.run(["systemctl", "is-active", "--quiet", "ccsm-linux-bridge.service"], check=True)
    print("Agente iniciado. Compruebe la última señal en el tablero MCP CCSM.")


def main() -> int:
    try:
        pair()
        return 0
    except (PairingError, OSError, URLError, HTTPError, ValueError, json.JSONDecodeError,
            UnicodeDecodeError, ssl.SSLError, subprocess.CalledProcessError, KeyError) as error:
        print("No se completó la vinculación: " + str(error), file=sys.stderr)
        print("El servicio puede estar detenido. Consulte el estado; no copie tokens ni claves al chat.", file=sys.stderr)
        return 1


if __name__ == "__main__":
    sys.exit(main())
