"""CAP Agent Registry — Shared SQLite-based agent discovery.

All SOAGILA instances on the same machine register here so they can
discover and communicate with each other. The registry lives at
~/.soagila/agent_registry.db and uses WAL mode for safe concurrent
access from multiple products.

Security: each agent gets a random auth_token on registration,
verified on every cross-agent request. Tokens rotate per startup.
"""

import json
import logging
import os
import secrets
import sys
from datetime import datetime, timezone
from pathlib import Path

import aiosqlite

logger = logging.getLogger("soagila.cap.registry")

_REGISTRY_DB: str | None = None


def _get_registry_path() -> str:
    """Get the path to the shared agent registry DB."""
    global _REGISTRY_DB
    if _REGISTRY_DB:
        return _REGISTRY_DB
    home = Path.home() / ".soagila"
    home.mkdir(parents=True, exist_ok=True)
    _REGISTRY_DB = str(home / "agent_registry.db")
    return _REGISTRY_DB


async def _get_conn() -> aiosqlite.Connection:
    """Open a connection to the registry DB with WAL mode."""
    db = await aiosqlite.connect(_get_registry_path(), timeout=5.0)
    await db.execute("PRAGMA journal_mode=WAL")
    await db.execute("PRAGMA busy_timeout=5000")
    await db.execute("PRAGMA foreign_keys=ON")
    return db


async def _ensure_schema(db: aiosqlite.Connection) -> None:
    """Create registry tables if they don't exist."""
    await db.execute("""
        CREATE TABLE IF NOT EXISTS agents (
            agent_id        TEXT PRIMARY KEY,
            product_type    TEXT NOT NULL,
            pid             INTEGER NOT NULL,
            port            INTEGER DEFAULT 0,
            project_hash    TEXT NOT NULL,
            project_name    TEXT DEFAULT '',
            capabilities    TEXT DEFAULT '[]',
            auth_token      TEXT NOT NULL,
            started_at      TEXT NOT NULL,
            last_heartbeat  TEXT NOT NULL,
            version         TEXT DEFAULT ''
        )
    """)
    await db.execute(
        "CREATE INDEX IF NOT EXISTS idx_agents_project ON agents(project_hash)"
    )
    await db.execute(
        "CREATE INDEX IF NOT EXISTS idx_agents_product ON agents(product_type)"
    )
    await db.commit()


def _pid_alive(pid: int) -> bool:
    """Check if a process is still alive."""
    if pid <= 0:
        return False
    try:
        if sys.platform == "win32":
            import ctypes
            kernel32 = ctypes.windll.kernel32
            SYNCHRONIZE = 0x00100000
            handle = kernel32.OpenProcess(SYNCHRONIZE, False, pid)
            if handle:
                kernel32.CloseHandle(handle)
                return True
            return False
        else:
            os.kill(pid, 0)
            return True
    except (OSError, PermissionError):
        return False


async def register_agent(
    agent_id: str,
    product_type: str,
    pid: int,
    port: int,
    project_hash: str,
    project_name: str = "",
    capabilities: list[str] | None = None,
    version: str = "",
) -> str:
    """Register this agent in the shared registry. Returns auth_token."""
    auth_token = secrets.token_urlsafe(32)
    now = datetime.now(timezone.utc).isoformat()
    caps = json.dumps(capabilities or ["recall", "remember", "receipt", "status"])

    db = await _get_conn()
    try:
        await _ensure_schema(db)

        # Clean stale entries first
        await _cleanup_stale_inner(db)

        await db.execute(
            """INSERT OR REPLACE INTO agents
               (agent_id, product_type, pid, port, project_hash, project_name,
                capabilities, auth_token, started_at, last_heartbeat, version)
               VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
            (agent_id, product_type, pid, port, project_hash, project_name,
             caps, auth_token, now, now, version),
        )
        await db.commit()
        logger.info(
            f"Registered agent {agent_id} ({product_type}) on port {port} "
            f"for project {project_hash[:8]}..."
        )
    finally:
        await db.close()

    return auth_token


async def deregister_agent(agent_id: str) -> None:
    """Remove this agent from the registry."""
    db = await _get_conn()
    try:
        await _ensure_schema(db)
        await db.execute("DELETE FROM agents WHERE agent_id = ?", (agent_id,))
        await db.commit()
        logger.info(f"Deregistered agent {agent_id}")
    finally:
        await db.close()


async def heartbeat(agent_id: str) -> None:
    """Update last_heartbeat timestamp for this agent."""
    now = datetime.now(timezone.utc).isoformat()
    db = await _get_conn()
    try:
        await _ensure_schema(db)
        await db.execute(
            "UPDATE agents SET last_heartbeat = ? WHERE agent_id = ?",
            (now, agent_id),
        )
        await db.commit()
    finally:
        await db.close()


async def get_peers(
    project_hash: str | None = None,
    exclude_agent_id: str | None = None,
) -> list[dict]:
    """Get all live agents, optionally filtered by project_hash."""
    db = await _get_conn()
    try:
        await _ensure_schema(db)

        # Clean stale entries
        await _cleanup_stale_inner(db)

        if project_hash:
            cursor = await db.execute(
                "SELECT * FROM agents WHERE project_hash = ? ORDER BY started_at DESC",
                (project_hash,),
            )
        else:
            cursor = await db.execute(
                "SELECT * FROM agents ORDER BY started_at DESC"
            )

        rows = await cursor.fetchall()
        cols = [d[0] for d in cursor.description]
        agents = []
        for row in rows:
            agent = dict(zip(cols, row))
            if exclude_agent_id and agent["agent_id"] == exclude_agent_id:
                continue
            # Parse capabilities JSON
            try:
                agent["capabilities"] = json.loads(agent.get("capabilities", "[]"))
            except (json.JSONDecodeError, TypeError):
                agent["capabilities"] = []
            # Remove auth_token from peer listings
            agent.pop("auth_token", None)
            agents.append(agent)
        return agents
    finally:
        await db.close()


async def _cleanup_stale_inner(db: aiosqlite.Connection, max_age_seconds: int = 60) -> int:
    """Remove agents whose heartbeat is stale AND process is dead. Requires open db."""
    cutoff = datetime.now(timezone.utc)
    cursor = await db.execute("SELECT agent_id, pid, last_heartbeat FROM agents")
    rows = await cursor.fetchall()

    removed = 0
    for agent_id, pid, last_hb in rows:
        try:
            hb_time = datetime.fromisoformat(last_hb)
            if hb_time.tzinfo is None:
                hb_time = hb_time.replace(tzinfo=timezone.utc)
            age = (cutoff - hb_time).total_seconds()
        except (ValueError, TypeError):
            age = max_age_seconds + 1

        if age > max_age_seconds and not _pid_alive(pid):
            await db.execute("DELETE FROM agents WHERE agent_id = ?", (agent_id,))
            removed += 1
            logger.debug(f"Cleaned stale agent {agent_id} (pid {pid} dead, {age:.0f}s stale)")

    if removed:
        await db.commit()
    return removed


async def cleanup_stale(max_age_seconds: int = 60) -> int:
    """Remove stale agents (public API)."""
    db = await _get_conn()
    try:
        await _ensure_schema(db)
        return await _cleanup_stale_inner(db, max_age_seconds)
    finally:
        await db.close()


async def verify_peer(agent_id: str, auth_token: str) -> bool:
    """Verify an agent's auth token for cross-agent requests."""
    if not agent_id or not auth_token:
        return False
    db = await _get_conn()
    try:
        await _ensure_schema(db)
        cursor = await db.execute(
            "SELECT auth_token FROM agents WHERE agent_id = ?",
            (agent_id,),
        )
        row = await cursor.fetchone()
        if row is None:
            return False
        return secrets.compare_digest(row[0], auth_token)
    finally:
        await db.close()


async def get_agent_info(agent_id: str) -> dict | None:
    """Get full info for a specific agent (including own info)."""
    db = await _get_conn()
    try:
        await _ensure_schema(db)
        cursor = await db.execute(
            "SELECT * FROM agents WHERE agent_id = ?", (agent_id,)
        )
        row = await cursor.fetchone()
        if row is None:
            return None
        cols = [d[0] for d in cursor.description]
        agent = dict(zip(cols, row))
        try:
            agent["capabilities"] = json.loads(agent.get("capabilities", "[]"))
        except (json.JSONDecodeError, TypeError):
            agent["capabilities"] = []
        agent.pop("auth_token", None)
        return agent
    finally:
        await db.close()
