"""Agent deployment API routes for the Claude MPM Dashboard.

Phase 3: Endpoint handlers for deploying/undeploying agents and
managing agent collections. Every destructive operation follows the
safety protocol: backup -> journal -> execute -> verify -> prune.

All blocking service calls are wrapped in asyncio.to_thread().
"""

import asyncio
import time
from typing import Any, Dict

from aiohttp import web

from claude_mpm.core.agent_name_registry import CORE_AGENT_IDS
from claude_mpm.core.deployment_context import DeploymentContext
from claude_mpm.core.logging_config import get_logger
from claude_mpm.services.config_api.validation import (
    validate_path_containment,
    validate_safe_name,
)
from claude_mpm.utils.agent_filters import normalize_agent_id

logger = get_logger(__name__)

# Agents that cannot be undeployed (BR-01) — sourced from canonical registry.
CORE_AGENTS = sorted(CORE_AGENT_IDS)

# Lazy-initialized service singletons
_backup_manager = None
_operation_journal = None
_deployment_verifier = None
_agent_deployment_service = None


def _get_backup_manager():
    """Return the module-level BackupManager singleton, creating it on first call.

    WHY: Lazy initialisation avoids importing BackupManager at module load time and
    ensures DeploymentContext.from_project() is called once, sharing the agents_dir
    across all agent route handlers in this module.
    WHAT: Creates BackupManager seeded with the project agents directory; caches it.
    TEST: Call twice; assert both return values are the same object (identity check).
    """
    global _backup_manager
    if _backup_manager is None:
        from claude_mpm.services.config_api.backup_manager import BackupManager

        ctx = DeploymentContext.from_project()
        _backup_manager = BackupManager(agents_dir=ctx.agents_dir)
    return _backup_manager


def _get_operation_journal():
    """Return the module-level OperationJournal singleton, creating it on first call.

    WHY: A single shared journal ensures all agent operations in this process are
    recorded in the same log without competing initialisation.
    WHAT: Lazily imports and instantiates OperationJournal; caches in module global.
    TEST: Call twice; assert both return values are the same object.
    """
    global _operation_journal
    if _operation_journal is None:
        from claude_mpm.services.config_api.operation_journal import OperationJournal

        _operation_journal = OperationJournal()
    return _operation_journal


def _get_deployment_verifier():
    """Return the module-level DeploymentVerifier singleton, creating it on first call.

    WHY: Verification runs after every deploy/undeploy; a singleton avoids repeated
    construction overhead and keeps a single verifier state for this process.
    WHAT: Lazily imports and instantiates DeploymentVerifier; caches in module global.
    TEST: Call twice; assert both return values are the same object.
    """
    global _deployment_verifier
    if _deployment_verifier is None:
        from claude_mpm.services.config_api.deployment_verifier import (
            DeploymentVerifier,
        )

        _deployment_verifier = DeploymentVerifier()
    return _deployment_verifier


def _get_agent_deployment_service():
    """Return the module-level AgentDeploymentService singleton, creating it on first call.

    WHY: AgentDeploymentService is used across every agent deploy/undeploy route; a
    singleton avoids re-initialisation and reuses any internal caches or connections.
    WHAT: Lazily imports and instantiates AgentDeploymentService; caches in module global.
    TEST: Call twice; assert both return values are the same object.
    """
    global _agent_deployment_service
    if _agent_deployment_service is None:
        from claude_mpm.services.agents.deployment.agent_deployment import (
            AgentDeploymentService,
        )

        _agent_deployment_service = AgentDeploymentService()
    return _agent_deployment_service


def _error_response(status: int, error: str, code: str) -> web.Response:
    """Build a standardised JSON error response for agent deployment API endpoints.

    WHY: Consistent error shape lets front-end code rely on a single schema for all
    failure responses instead of handling ad-hoc formats from each handler.
    WHAT: Returns a web.Response with JSON body ``{success: false, error: ..., code: ...}``
    and the given HTTP status code.
    TEST: Call with (404, "agent not found", "NOT_FOUND"); assert response.status==404
    and parsed JSON contains success=false and the expected error string.
    """
    return web.json_response(
        {"success": False, "error": error, "code": code},
        status=status,
    )


def _verification_to_dict(result) -> Dict[str, Any]:
    """Convert a VerificationResult to a JSON-serialisable dict."""
    return {
        "passed": result.passed,
        "timestamp": result.timestamp,
        "checks": [
            {
                "check": c.check,
                "passed": c.passed,
                "path": c.path,
                "details": c.details,
            }
            for c in result.checks
        ],
    }


def register_agent_deployment_routes(app, config_event_handler, config_file_watcher):
    """Register agent deployment routes on the aiohttp app.

    Args:
        app: The aiohttp web application.
        config_event_handler: ConfigEventHandler for Socket.IO events.
        config_file_watcher: ConfigFileWatcher for mtime tracking.
    """
    _handler = config_event_handler

    # ------------------------------------------------------------------
    # POST /api/config/agents/deploy
    # ------------------------------------------------------------------
    async def deploy_agent(request: web.Request) -> web.Response:
        """Deploy an agent from cache to the project."""
        try:
            body = await request.json()
        except Exception:
            return _error_response(400, "Invalid JSON body", "VALIDATION_ERROR")

        agent_name = body.get("agent_name", "").strip()
        _source_id = body.get("source_id")
        force = body.get("force", False)

        # Scope validation (R-3: null-safe)
        scope_str = body.get("scope", "project") or "project"
        try:
            ctx = DeploymentContext.from_request_scope(scope_str)
        except ValueError as e:
            return _error_response(400, str(e), "VALIDATION_ERROR")

        if not agent_name:
            return _error_response(400, "agent_name is required", "VALIDATION_ERROR")

        # C-01: Validate agent name to prevent path traversal
        valid, err_msg = validate_safe_name(agent_name, "agent")
        if not valid:
            return _error_response(400, err_msg, "VALIDATION_ERROR")

        agents_dir = ctx.agents_dir
        agent_path = agents_dir / f"{agent_name}.md"

        # Path containment check (defence in depth)
        valid, err_msg = validate_path_containment(agent_path, agents_dir, "agent")
        if not valid:
            return _error_response(400, err_msg, "VALIDATION_ERROR")

        # Conflict check
        if agent_path.exists() and not force:
            return _error_response(
                409,
                f"Agent '{agent_name}' already deployed. Use force=true to redeploy.",
                "CONFLICT",
            )

        try:

            def _deploy_sync():
                backup_mgr = _get_backup_manager()
                journal = _get_operation_journal()
                verifier = _get_deployment_verifier()
                svc = _get_agent_deployment_service()

                # 1. Backup
                backup = backup_mgr.create_backup("deploy_agent", "agent", agent_name)

                # 2. Journal
                op_id = journal.begin_operation(
                    "deploy_agent", "agent", agent_name, backup.backup_id
                )

                try:
                    # 3. Deploy via service
                    agents_dir.mkdir(parents=True, exist_ok=True)
                    success = svc.deploy_agent(
                        agent_name, agents_dir, force_rebuild=force
                    )

                    if not success:
                        raise RuntimeError(
                            f"AgentDeploymentService.deploy_agent returned False for '{agent_name}'"
                        )

                    # 4. Verify
                    verification = verifier.verify_agent_deployed(
                        agent_name, agents_dir=agents_dir
                    )

                    # 5. Complete journal
                    journal.complete_operation(op_id)

                    return {
                        "backup_id": backup.backup_id,
                        "verification": _verification_to_dict(verification),
                    }

                except Exception as exc:
                    journal.fail_operation(op_id, str(exc))
                    raise

            result = await asyncio.to_thread(_deploy_sync)

            # Detect active sessions (non-blocking best effort)
            try:
                from claude_mpm.services.config_api.session_detector import (
                    detect_active_claude_sessions,
                )

                sessions = await asyncio.to_thread(detect_active_claude_sessions)
            except Exception:
                sessions = []

            # Socket.IO event
            await _handler.emit_config_event(
                operation="agent_deployed",
                entity_type="agent",
                entity_id=agent_name,
                status="completed",
                data={"agent_name": agent_name, "action": "deploy", "scope": scope_str},
            )

            return web.json_response(
                {
                    "success": True,
                    "message": f"Agent '{agent_name}' deployed successfully",
                    "agent_name": agent_name,
                    "scope": scope_str,
                    "backup_id": result["backup_id"],
                    "verification": result["verification"],
                    "active_sessions_warning": len(sessions) > 0,
                    "active_sessions": sessions,
                },
                status=201,
            )

        except Exception as e:
            logger.error("Error deploying agent '%s': %s", agent_name, e)
            return _error_response(500, str(e), "DEPLOY_FAILED")

    # ------------------------------------------------------------------
    # DELETE /api/config/agents/{agent_name}
    # ------------------------------------------------------------------
    async def undeploy_agent(request: web.Request) -> web.Response:
        """Remove a deployed agent."""
        agent_name = request.match_info["agent_name"]

        # Scope validation (query param for DELETE)
        scope_str = request.rel_url.query.get("scope", "project") or "project"
        try:
            ctx = DeploymentContext.from_request_scope(scope_str)
        except ValueError as e:
            return _error_response(400, str(e), "VALIDATION_ERROR")

        # C-01: Validate agent name to prevent path traversal
        valid, err_msg = validate_safe_name(agent_name, "agent")
        if not valid:
            return _error_response(400, err_msg, "VALIDATION_ERROR")

        # Normalize agent name so that e.g. "dart_engineer" finds "dart-engineer.md"
        agent_name = normalize_agent_id(agent_name)

        # BR-01: core agent protection
        if agent_name in CORE_AGENTS:
            return _error_response(
                403,
                f"Cannot undeploy core agent '{agent_name}'. "
                "Core agents are protected.",
                "CORE_AGENT_PROTECTED",
            )

        agents_dir = ctx.agents_dir
        agent_path = agents_dir / f"{agent_name}.md"

        # Path containment check (defence in depth)
        valid, err_msg = validate_path_containment(agent_path, agents_dir, "agent")
        if not valid:
            return _error_response(400, err_msg, "VALIDATION_ERROR")

        if not agent_path.exists():
            return _error_response(
                404,
                f"Agent '{agent_name}' is not deployed",
                "NOT_FOUND",
            )

        try:

            def _undeploy_sync():
                backup_mgr = _get_backup_manager()
                journal = _get_operation_journal()
                verifier = _get_deployment_verifier()

                # 1. Backup
                backup = backup_mgr.create_backup("undeploy_agent", "agent", agent_name)

                # 2. Journal
                op_id = journal.begin_operation(
                    "undeploy_agent", "agent", agent_name, backup.backup_id
                )

                try:
                    # 3. Remove file
                    agent_path.unlink()

                    # 4. Verify removal
                    verification = verifier.verify_agent_undeployed(
                        agent_name, agents_dir=agents_dir
                    )

                    # 5. Complete
                    journal.complete_operation(op_id)

                    return {
                        "backup_id": backup.backup_id,
                        "verification": _verification_to_dict(verification),
                    }

                except Exception as exc:
                    journal.fail_operation(op_id, str(exc))
                    raise

            result = await asyncio.to_thread(_undeploy_sync)

            # Socket.IO event
            await _handler.emit_config_event(
                operation="agent_undeployed",
                entity_type="agent",
                entity_id=agent_name,
                status="completed",
                data={
                    "agent_name": agent_name,
                    "action": "undeploy",
                    "scope": scope_str,
                },
            )

            return web.json_response(
                {
                    "success": True,
                    "message": f"Agent '{agent_name}' undeployed",
                    "agent_name": agent_name,
                    "scope": scope_str,
                    "backup_id": result["backup_id"],
                    "verification": result["verification"],
                }
            )

        except Exception as e:
            logger.error("Error undeploying agent '%s': %s", agent_name, e)
            return _error_response(500, str(e), "SERVICE_ERROR")

    # ------------------------------------------------------------------
    # POST /api/config/agents/deploy-collection
    # ------------------------------------------------------------------
    async def deploy_collection(request: web.Request) -> web.Response:
        """Batch deploy agents. Sequential, continues on individual failure."""
        try:
            body = await request.json()
        except Exception:
            return _error_response(400, "Invalid JSON body", "VALIDATION_ERROR")

        agent_names = body.get("agent_names", [])
        _source_id = body.get("source_id")
        force = body.get("force", False)

        # Scope validation (R-3: null-safe)
        scope_str = body.get("scope", "project") or "project"
        try:
            ctx = DeploymentContext.from_request_scope(scope_str)
        except ValueError as e:
            return _error_response(400, str(e), "VALIDATION_ERROR")

        if not agent_names or not isinstance(agent_names, list):
            return _error_response(
                400, "agent_names must be a non-empty list", "VALIDATION_ERROR"
            )

        # Compute agents_dir once before the loop (captured by closure)
        batch_agents_dir = ctx.agents_dir

        results = []
        deployed = []
        failed = []
        start_time = time.time()

        for idx, agent_name in enumerate(agent_names):
            # C-01: Validate each agent name to prevent path traversal
            valid, err_msg = validate_safe_name(agent_name, "agent")
            if not valid:
                failed.append(agent_name)
                results.append(
                    {"agent_name": agent_name, "success": False, "error": err_msg}
                )
                logger.warning(
                    "Batch deploy: agent '%s' rejected: %s", agent_name, err_msg
                )
                continue

            try:

                def _deploy_one(name=agent_name):
                    agents_dir = batch_agents_dir
                    agents_dir.mkdir(parents=True, exist_ok=True)

                    backup_mgr = _get_backup_manager()
                    journal = _get_operation_journal()
                    verifier = _get_deployment_verifier()
                    svc = _get_agent_deployment_service()

                    backup = backup_mgr.create_backup(
                        "deploy_agent_batch", "agent", name
                    )
                    op_id = journal.begin_operation(
                        "deploy_agent_batch", "agent", name, backup.backup_id
                    )
                    try:
                        success = svc.deploy_agent(
                            name, agents_dir, force_rebuild=force
                        )
                        if not success:
                            raise RuntimeError(
                                f"deploy_agent returned False for '{name}'"
                            )
                        verification = verifier.verify_agent_deployed(
                            name, agents_dir=agents_dir
                        )
                        journal.complete_operation(op_id)
                        return {
                            "success": True,
                            "verification": _verification_to_dict(verification),
                        }
                    except Exception as exc:
                        journal.fail_operation(op_id, str(exc))
                        raise

                result = await asyncio.to_thread(_deploy_one)
                deployed.append(agent_name)
                results.append({"agent_name": agent_name, **result})

                # Per-agent Socket.IO progress
                await _handler.emit_config_event(
                    operation="agent_deployed",
                    entity_type="agent",
                    entity_id=agent_name,
                    status="completed",
                    data={
                        "agent_name": agent_name,
                        "action": "deploy",
                        "scope": scope_str,
                        "batch_progress": f"{idx + 1}/{len(agent_names)}",
                    },
                )

            except Exception as e:
                failed.append(agent_name)
                results.append(
                    {"agent_name": agent_name, "success": False, "error": str(e)}
                )
                logger.warning("Batch deploy: agent '%s' failed: %s", agent_name, e)

        duration_ms = int((time.time() - start_time) * 1000)

        return web.json_response(
            {
                "success": len(failed) == 0,
                "scope": scope_str,
                "results": results,
                "summary": {
                    "total": len(agent_names),
                    "deployed": len(deployed),
                    "failed": len(failed),
                    "duration_ms": duration_ms,
                },
                "deployed_agents": deployed,
                "failed_agents": failed,
            }
        )

    # ------------------------------------------------------------------
    # GET /api/config/agents/collections
    # ------------------------------------------------------------------
    async def list_collections(request: web.Request) -> web.Response:
        """List agent collections from source metadata."""
        try:

            def _list():
                from claude_mpm.config.agent_sources import AgentSourceConfiguration

                config = AgentSourceConfiguration.load()
                collections = []
                for repo in config.repositories:
                    collections.append(
                        {
                            "id": repo.identifier,
                            "url": getattr(repo, "url", ""),
                            "subdirectory": getattr(repo, "subdirectory", None),
                            "enabled": getattr(repo, "enabled", True),
                            "priority": getattr(repo, "priority", 100),
                        }
                    )
                return collections

            collections = await asyncio.to_thread(_list)
            return web.json_response(
                {
                    "success": True,
                    "collections": collections,
                    "total": len(collections),
                }
            )

        except Exception as e:
            logger.error("Error listing agent collections: %s", e)
            return _error_response(500, str(e), "SERVICE_ERROR")

    # ------------------------------------------------------------------
    # GET /api/config/active-sessions
    # ------------------------------------------------------------------
    async def get_active_sessions(request: web.Request) -> web.Response:
        """Detect active Claude Code sessions."""
        try:
            from claude_mpm.services.config_api.session_detector import (
                detect_active_claude_sessions,
            )

            sessions = await asyncio.to_thread(detect_active_claude_sessions)

            has_active = len(sessions) > 0
            warning = (
                "Active Claude Code sessions detected. "
                "Changes may not take effect until sessions are restarted."
                if has_active
                else ""
            )

            return web.json_response(
                {
                    "success": True,
                    "active_sessions": sessions,
                    "has_active_sessions": has_active,
                    "warning_message": warning,
                }
            )

        except Exception as e:
            logger.error("Error detecting active sessions: %s", e)
            return _error_response(500, str(e), "SERVICE_ERROR")

    # Register routes
    app.router.add_post("/api/config/agents/deploy", deploy_agent)
    app.router.add_delete("/api/config/agents/{agent_name}", undeploy_agent)
    app.router.add_post("/api/config/agents/deploy-collection", deploy_collection)
    app.router.add_get("/api/config/agents/collections", list_collections)
    app.router.add_get("/api/config/active-sessions", get_active_sessions)

    logger.info(
        "Registered 5 agent deployment routes under /api/config/agents/ "
        "(+ /api/config/active-sessions)"
    )
