"""A2A local-mode facade routing operations to application services.

In local mode every A2A operation maps 1:1 to a method call on the
appropriate application service.  No serialization, network, or auth.

Service dependencies are supplied via the ``services`` dict passed to the
constructor.  Expected keys:

| Key                          | Type                        | Used by               |
|------------------------------|-----------------------------|-----------------------|
| ``session_service``          | ``SessionService``          | session.create/close  |
| ``plan_lifecycle_service``   | ``PlanLifecycleService``    | plan.* operations     |
| ``tool_registry``            | ``ToolRegistry``            | registry.list_tools   |
| ``resource_registry_service``| ``ResourceRegistryService`` | registry.list_resources|
| ``event_queue``              | ``A2aEventQueue``           | event.subscribe       |

When a service is absent the handler falls back to a safe stub
response so the facade never crashes due to missing wiring.
"""

from __future__ import annotations

import time
from typing import TYPE_CHECKING, Any

import structlog
from ulid import ULID

from cleveragents.a2a.errors import (
    A2aOperationNotFoundError,
    map_domain_error,
)
from cleveragents.a2a.models import (
    A2aErrorDetail,
    A2aRequest,
    A2aResponse,
)

if TYPE_CHECKING:
    from cleveragents.a2a.events import A2aEventQueue
    from cleveragents.application.services.plan_lifecycle_service import (
        PlanLifecycleService,
    )
    from cleveragents.application.services.resource_registry_service import (
        ResourceRegistryService,
    )
    from cleveragents.domain.models.core.session import SessionService
    from cleveragents.tool.registry import ToolRegistry

logger: structlog.stdlib.BoundLogger = structlog.get_logger(__name__)

# ---------------------------------------------------------------------------
# Supported operations
# ---------------------------------------------------------------------------

# Spec-aligned _cleveragents/ extension method names (ADR-047).
_EXTENSION_OPERATIONS: list[str] = [
    # Plan lifecycle
    "_cleveragents/plan/use",
    "_cleveragents/plan/execute",
    "_cleveragents/plan/apply",
    "_cleveragents/plan/cancel",
    "_cleveragents/plan/status",
    "_cleveragents/plan/tree",
    "_cleveragents/plan/explain",
    "_cleveragents/plan/correct",
    "_cleveragents/plan/diff",
    "_cleveragents/plan/artifacts",
    "_cleveragents/plan/prompt",
    "_cleveragents/plan/rollback",
    "_cleveragents/plan/list",
    # Registry
    "_cleveragents/registry/tool/list",
    "_cleveragents/registry/resource/list",
    "_cleveragents/registry/actor/list",
    "_cleveragents/registry/skill/list",
    "_cleveragents/registry/action/list",
    "_cleveragents/registry/project/list",
    # Context
    "_cleveragents/context/show",
    "_cleveragents/context/inspect",
    "_cleveragents/context/simulate",
    "_cleveragents/context/set",
    # Health / Diagnostics
    "_cleveragents/health/check",
    "_cleveragents/diagnostics/run",
    # Sync (stub)
    "_cleveragents/sync/pull",
    "_cleveragents/sync/push",
    "_cleveragents/sync/status",
    # Namespace (stub)
    "_cleveragents/namespace/list",
    "_cleveragents/namespace/show",
    "_cleveragents/namespace/members",
]

# Legacy proprietary operation names (deprecated, kept for backward compat).
_LEGACY_OPERATIONS: list[str] = [
    "session.create",
    "session.close",
    "plan.create",
    "plan.execute",
    "plan.status",
    "plan.diff",
    "plan.apply",
    "registry.list_tools",
    "registry.list_resources",
    "context.get",
    "event.subscribe",
]

_SUPPORTED_OPERATIONS: list[str] = _EXTENSION_OPERATIONS + _LEGACY_OPERATIONS


class A2aLocalFacade:
    """Local-mode facade that dispatches A2A operations to services.

    Accepts service dependencies via the ``services`` dict.  Each handler
    checks for the presence of the required service and delegates to it
    when available; otherwise a minimal stub response is returned.
    """

    def __init__(self, services: dict[str, Any] | None = None) -> None:
        if services is not None and not isinstance(services, dict):
            raise TypeError("services must be a dict or None")
        self._services: dict[str, Any] = dict(services) if services else {}
        # PERF-1 fix: cache the handler dispatch dict instead of
        # rebuilding it on every A2A request.
        self._handler_map: dict[str, Any] | None = None

    # ------------------------------------------------------------------
    # Service accessors (typed, nullable)
    # ------------------------------------------------------------------

    @property
    def _session_service(self) -> SessionService | None:
        return self._services.get("session_service")  # type: ignore[return-value]

    @property
    def _plan_lifecycle_service(self) -> PlanLifecycleService | None:
        svc: object | None = self._services.get("plan_lifecycle_service")
        return svc  # type: ignore[return-value]

    @property
    def _tool_registry(self) -> ToolRegistry | None:
        return self._services.get("tool_registry")  # type: ignore[return-value]

    @property
    def _resource_registry_service(self) -> ResourceRegistryService | None:
        svc: object | None = self._services.get("resource_registry_service")
        return svc  # type: ignore[return-value]

    @property
    def _event_queue(self) -> A2aEventQueue | None:
        return self._services.get("event_queue")  # type: ignore[return-value]

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def dispatch(self, request: A2aRequest) -> A2aResponse:
        """Route an :class:`A2aRequest` to the appropriate handler.

        Returns an :class:`A2aResponse` with ``result`` set on success or
        ``error`` set when the operation fails.  Domain exceptions
        are mapped to A2A error codes via :func:`map_domain_error`.
        """
        if not isinstance(request, A2aRequest):
            raise TypeError("request must be an A2aRequest instance")

        start = time.monotonic()
        try:
            data = self._route_operation(request.method, request.params)
            elapsed = (time.monotonic() - start) * 1000.0
            logger.info(
                "a2a.local.dispatch",
                method=request.method,
                request_id=request.id,
                timing_ms=round(elapsed, 2),
            )
            return A2aResponse(
                id=request.id,
                result=data,
            )
        except A2aOperationNotFoundError:
            raise
        except Exception as exc:
            elapsed = (time.monotonic() - start) * 1000.0
            code, message = map_domain_error(exc)
            logger.error(
                "a2a.local.dispatch.error",
                method=request.method,
                request_id=request.id,
                error_code=code,
                error=message,
            )
            return A2aResponse(
                id=request.id,
                error=A2aErrorDetail(
                    code=code,
                    message=message,
                ),
            )

    def register_service(self, name: str, service: Any) -> None:
        """Register a named service for operation routing."""
        if not name or not isinstance(name, str):
            raise ValueError("name must be a non-empty string")
        self._services[name] = service
        # PERF-1 fix: invalidate cached handler map so new service
        # wiring is picked up on the next dispatch.
        self._handler_map = None
        logger.debug("a2a.local.service_registered", service_name=name)

    def list_operations(self) -> list[str]:
        """Return the list of supported A2A operation names."""
        return list(_SUPPORTED_OPERATIONS)

    # ------------------------------------------------------------------
    # Internal routing
    # ------------------------------------------------------------------

    def _route_operation(
        self,
        operation: str,
        params: dict[str, Any],
    ) -> dict[str, Any]:
        """Map *operation* to a handler and return the result dict."""
        handler = self._handlers().get(operation)
        if handler is None:
            raise A2aOperationNotFoundError(
                message=f"Unknown A2A method: {operation}",
                operation=operation,
            )
        return handler(params)

    def _handlers(self) -> dict[str, Any]:
        """Return the cached operation -> handler mapping.

        PERF-1 fix: the dispatch dict is built once and cached.
        Invalidated when a new service is registered.
        """
        if self._handler_map is None:
            self._handler_map = {
                # --- _cleveragents/ extension methods (spec-aligned) ---
                # Plan lifecycle
                "_cleveragents/plan/use": self._handle_plan_create,
                "_cleveragents/plan/execute": self._handle_plan_execute,
                "_cleveragents/plan/apply": self._handle_plan_apply,
                "_cleveragents/plan/cancel": self._handle_plan_cancel,
                "_cleveragents/plan/status": self._handle_plan_status,
                "_cleveragents/plan/tree": self._handle_plan_tree,
                "_cleveragents/plan/explain": self._handle_plan_explain,
                "_cleveragents/plan/correct": self._handle_plan_correct,
                "_cleveragents/plan/diff": self._handle_plan_diff,
                "_cleveragents/plan/artifacts": self._handle_plan_artifacts,
                "_cleveragents/plan/prompt": self._handle_plan_prompt,
                "_cleveragents/plan/rollback": self._handle_plan_rollback,
                "_cleveragents/plan/list": self._handle_plan_list,
                # Registry
                "_cleveragents/registry/tool/list": self._handle_registry_list_tools,
                "_cleveragents/registry/resource/list": (
                    self._handle_registry_list_resources
                ),
                "_cleveragents/registry/actor/list": self._handle_registry_list_stub,
                "_cleveragents/registry/skill/list": self._handle_registry_list_stub,
                "_cleveragents/registry/action/list": self._handle_registry_list_stub,
                "_cleveragents/registry/project/list": self._handle_registry_list_stub,
                # Context
                "_cleveragents/context/show": self._handle_context_get,
                "_cleveragents/context/inspect": self._handle_context_stub,
                "_cleveragents/context/simulate": self._handle_context_stub,
                "_cleveragents/context/set": self._handle_context_stub,
                # Health / Diagnostics
                "_cleveragents/health/check": self._handle_health_check,
                "_cleveragents/diagnostics/run": self._handle_diagnostics_run,
                # Sync (stub)
                "_cleveragents/sync/pull": self._handle_sync_stub,
                "_cleveragents/sync/push": self._handle_sync_stub,
                "_cleveragents/sync/status": self._handle_sync_stub,
                # Namespace (stub)
                "_cleveragents/namespace/list": self._handle_namespace_stub,
                "_cleveragents/namespace/show": self._handle_namespace_stub,
                "_cleveragents/namespace/members": self._handle_namespace_stub,
                # --- Legacy proprietary names (deprecated) ---
                "session.create": self._handle_session_create,
                "session.close": self._handle_session_close,
                "plan.create": self._handle_plan_create,
                "plan.execute": self._handle_plan_execute,
                "plan.status": self._handle_plan_status,
                "plan.diff": self._handle_plan_diff,
                "plan.apply": self._handle_plan_apply,
                "registry.list_tools": self._handle_registry_list_tools,
                "registry.list_resources": self._handle_registry_list_resources,
                "context.get": self._handle_context_get,
                "event.subscribe": self._handle_event_subscribe,
            }
        return self._handler_map

    # ------------------------------------------------------------------
    # Operation handlers — session
    # ------------------------------------------------------------------

    def _handle_session_create(self, params: dict[str, Any]) -> dict[str, Any]:
        # If a session_id is already supplied the caller has created the
        # session directly via the service layer (e.g. the CLI create
        # command).  Acknowledge without creating a duplicate — this keeps
        # the handler idempotent and prevents the double-insert bug
        # reported in Forgejo #1141.
        existing_id: str | None = params.get("session_id")
        if existing_id:
            return {"session_id": existing_id, "status": "created"}

        svc = self._session_service
        if svc is None:
            return {"session_id": str(ULID()), "status": "created"}
        actor_name: str | None = params.get("actor_name")
        session = svc.create(actor_name=actor_name)
        return {"session_id": session.session_id, "status": "created"}

    def _handle_session_close(self, params: dict[str, Any]) -> dict[str, Any]:
        session_id = params.get("session_id", "")

        svc = self._session_service
        if svc is None:
            # R7-F4 fix: still run container cleanup even without a
            # session service — containers may exist in local/test mode.
            self._cleanup_session_devcontainers(session_id)
            return {"status": "closed"}

        if not session_id:
            raise ValueError("session_id is required")
        svc.delete(session_id)

        # R7-F4 fix: run container cleanup after session deletion.
        self._cleanup_session_devcontainers(session_id)
        return {"status": "closed"}

    def _cleanup_session_devcontainers(self, session_id: str) -> None:
        """Best-effort stop of devcontainers associated with a session.

        Failures are logged but never propagated so session close always
        succeeds.
        """
        if not session_id:
            return
        try:
            from cleveragents.application.services.cleanup_service import (
                CleanupService,
            )

            stopped = CleanupService.stop_active_devcontainers(
                session_id=session_id,
            )
            if stopped:
                logger.info(
                    "a2a.session.close.devcontainer_cleanup",
                    session_id=session_id,
                    stopped_count=len(stopped),
                )
        except Exception:
            logger.warning(
                "a2a.session.close.devcontainer_cleanup_failed",
                session_id=session_id,
                exc_info=True,
            )

    # ------------------------------------------------------------------
    # Operation handlers — plan lifecycle
    # ------------------------------------------------------------------

    def _handle_plan_create(self, params: dict[str, Any]) -> dict[str, Any]:
        svc = self._plan_lifecycle_service
        if svc is None:
            return {"plan_id": str(ULID()), "status": "created"}
        action_name = params.get("action_name", "")
        if not action_name:
            raise ValueError("action_name is required")
        plan = svc.use_action(
            action_name=action_name,
            arguments=params.get("arguments"),
            created_by=params.get("created_by"),
        )
        return {"plan_id": plan.identity.plan_id, "status": "created"}

    def _handle_plan_execute(self, params: dict[str, Any]) -> dict[str, Any]:
        svc = self._plan_lifecycle_service
        plan_id = params.get("plan_id", "")
        if svc is None:
            return {"plan_id": plan_id, "status": "queued"}
        if not plan_id:
            raise ValueError("plan_id is required")
        # If the plan has already reached (or passed) the execute phase,
        # acknowledge idempotently instead of attempting a duplicate
        # transition that would raise InvalidPhaseTransitionError.
        plan = svc.get_plan(plan_id)
        if plan.phase.value in ("execute", "apply"):
            return {"plan_id": plan.identity.plan_id, "status": plan.phase.value}
        plan = svc.execute_plan(plan_id)
        return {"plan_id": plan.identity.plan_id, "status": plan.phase.value}

    def _handle_plan_status(self, params: dict[str, Any]) -> dict[str, Any]:
        svc = self._plan_lifecycle_service
        plan_id = params.get("plan_id", "")
        if svc is None:
            return {"plan_id": plan_id, "phase": "unknown"}
        if not plan_id:
            raise ValueError("plan_id is required")
        plan = svc.get_plan(plan_id)
        state_value = plan.state.value if plan.state else "unknown"
        return {
            "plan_id": plan.identity.plan_id,
            "phase": plan.phase.value,
            "state": state_value,
        }

    def _handle_plan_diff(self, params: dict[str, Any]) -> dict[str, Any]:
        svc = self._plan_lifecycle_service
        plan_id = params.get("plan_id", "")
        if svc is None:
            return {"plan_id": plan_id, "changes": []}
        if not plan_id:
            raise ValueError("plan_id is required")
        plan = svc.get_plan(plan_id)
        return {
            "plan_id": plan.identity.plan_id,
            "changes": [],
            "phase": plan.phase.value,
        }

    def _handle_plan_apply(self, params: dict[str, Any]) -> dict[str, Any]:
        svc = self._plan_lifecycle_service
        plan_id = params.get("plan_id", "")
        if svc is None:
            return {"plan_id": plan_id, "status": "applied"}
        if not plan_id:
            raise ValueError("plan_id is required")
        plan = svc.apply_plan(plan_id)
        return {"plan_id": plan.identity.plan_id, "status": plan.phase.value}

    # ------------------------------------------------------------------
    # Operation handlers — registry
    # ------------------------------------------------------------------

    def _handle_registry_list_tools(self, params: dict[str, Any]) -> dict[str, Any]:
        registry = self._tool_registry
        if registry is None:
            return {"tools": []}
        namespace: str | None = params.get("namespace")
        specs = registry.list_tools(namespace=namespace)
        return {
            "tools": [{"name": s.name, "description": s.description} for s in specs],
        }

    def _handle_registry_list_resources(self, params: dict[str, Any]) -> dict[str, Any]:
        svc = self._resource_registry_service
        if svc is None:
            return {"resources": []}
        type_name: str | None = params.get("type_name")
        resources = svc.list_resources(type_name=type_name)
        return {
            "resources": [
                {
                    "resource_id": r.resource_id,
                    "name": r.name,
                    "type_name": r.resource_type_name,
                }
                for r in resources
            ],
        }

    # ------------------------------------------------------------------
    # Operation handlers — context
    # ------------------------------------------------------------------

    def _handle_context_get(self, params: dict[str, Any]) -> dict[str, Any]:
        # TODO: Wire to ACMS ContextAssemblyPipeline once available.
        # For now return a stub response indicating the pipeline is pending.
        return {
            "context": {},
            "stub": True,
            "message": "ACMS ContextAssemblyPipeline not yet wired",
        }

    # ------------------------------------------------------------------
    # Operation handlers — events
    # ------------------------------------------------------------------

    def _handle_event_subscribe(self, params: dict[str, Any]) -> dict[str, Any]:
        queue = self._event_queue
        if queue is None:
            return {"subscription_id": str(ULID()), "status": "subscribed"}

        def _noop_callback(_event: Any) -> None:
            """Default no-op callback for subscriptions."""

        sub_id = queue.subscribe_local(_noop_callback)
        return {"subscription_id": sub_id, "status": "subscribed"}

    # ------------------------------------------------------------------
    # Extension handlers — _cleveragents/plan/* (new operations)
    # ------------------------------------------------------------------

    def _handle_plan_cancel(self, params: dict[str, Any]) -> dict[str, Any]:
        plan_id = params.get("plan_id", "")
        svc = self._plan_lifecycle_service
        if svc is None or not plan_id:
            return {"plan_id": plan_id, "status": "cancelled", "stub": True}
        svc.cancel_plan(plan_id)
        return {"plan_id": plan_id, "status": "cancelled"}

    def _handle_plan_tree(self, params: dict[str, Any]) -> dict[str, Any]:
        plan_id = params.get("plan_id", "")
        svc = self._plan_lifecycle_service
        if svc is None or not plan_id:
            return {"plan_id": plan_id, "tree": [], "stub": True}
        plan = svc.get_plan(plan_id)
        return {
            "plan_id": plan_id,
            "decision_root_id": plan.decision_root_id if plan else None,
            "tree": [],
        }

    def _handle_plan_explain(self, params: dict[str, Any]) -> dict[str, Any]:
        plan_id = params.get("plan_id", "")
        decision_id = params.get("decision_id", "")
        return {
            "plan_id": plan_id,
            "decision_id": decision_id,
            "explanation": "Not yet implemented",
            "stub": True,
        }

    def _handle_plan_correct(self, params: dict[str, Any]) -> dict[str, Any]:
        plan_id = params.get("plan_id", "")
        return {"plan_id": plan_id, "status": "corrected", "stub": True}

    def _handle_plan_artifacts(self, params: dict[str, Any]) -> dict[str, Any]:
        plan_id = params.get("plan_id", "")
        svc = self._plan_lifecycle_service
        if svc is None or not plan_id:
            return {"plan_id": plan_id, "artifacts": [], "stub": True}
        plan = svc.get_plan(plan_id)
        return {
            "plan_id": plan_id,
            "changeset_id": plan.changeset_id if plan else None,
            "sandbox_refs": list(plan.sandbox_refs) if plan else [],
        }

    def _handle_plan_prompt(self, params: dict[str, Any]) -> dict[str, Any]:
        plan_id = params.get("plan_id", "")
        guidance = params.get("guidance", "")
        svc = self._plan_lifecycle_service
        if svc is not None and plan_id:
            prompt_payload = svc.prompt_plan(plan_id, guidance)
            return {
                "plan_id": plan_id,
                "guidance": guidance,
                "status": "guidance_injected",
                **prompt_payload,
            }
        return {
            "plan_id": plan_id,
            "guidance": guidance,
            "status": "guidance_injected",
            "stub": True,
        }

    def _handle_plan_rollback(self, params: dict[str, Any]) -> dict[str, Any]:
        plan_id = params.get("plan_id", "")
        return {"plan_id": plan_id, "status": "rolled_back", "stub": True}

    def _handle_plan_list(self, params: dict[str, Any]) -> dict[str, Any]:
        svc = self._plan_lifecycle_service
        if svc is None:
            return {"plans": [], "stub": True}
        plans = svc.list_plans()
        return {
            "plans": [
                {"plan_id": p.identity.plan_id, "phase": p.phase.value} for p in plans
            ],
            "total": len(plans),
        }

    # ------------------------------------------------------------------
    # Extension handlers — _cleveragents/registry/* (stub for new entities)
    # ------------------------------------------------------------------

    def _handle_registry_list_stub(self, params: dict[str, Any]) -> dict[str, Any]:
        return {"items": [], "stub": True}

    # ------------------------------------------------------------------
    # Extension handlers — _cleveragents/context/* (stubs)
    # ------------------------------------------------------------------

    def _handle_context_stub(self, params: dict[str, Any]) -> dict[str, Any]:
        return {"context": {}, "stub": True}

    # ------------------------------------------------------------------
    # Extension handlers — _cleveragents/health/*
    # ------------------------------------------------------------------

    def _handle_health_check(self, params: dict[str, Any]) -> dict[str, Any]:
        return {"status": "healthy", "services": {}}

    def _handle_diagnostics_run(self, params: dict[str, Any]) -> dict[str, Any]:
        return {"status": "ok", "diagnostics": {}, "stub": True}

    # ------------------------------------------------------------------
    # Extension handlers — _cleveragents/sync/* (stubs)
    # ------------------------------------------------------------------

    def _handle_sync_stub(self, params: dict[str, Any]) -> dict[str, Any]:
        return {"status": "not_implemented", "stub": True}

    # ------------------------------------------------------------------
    # Extension handlers — _cleveragents/namespace/* (stubs)
    # ------------------------------------------------------------------

    def _handle_namespace_stub(self, params: dict[str, Any]) -> dict[str, Any]:
        return {"status": "not_implemented", "stub": True}


__all__ = [
    "A2aLocalFacade",
]
