"""
CRM Client with Agendex Governance.

Routes all CRM operations through Agendex for policy evaluation.
"""
from __future__ import annotations

import copy
import logging
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional

from .context import get_reasoning

if TYPE_CHECKING:
    from .client import AgendexClient

logger = logging.getLogger("agendex.crm")


class GovernedCRMClient:
    """
    CRM client that routes all operations through Agendex governance.

    The agent never has direct CRM access - all operations go through
    Agendex proxy which enforces policy based on agent, task, and action.

    Example:
        from agendex import AgendexClient
        from agendex.crm import GovernedCRMClient

        client = AgendexClient()
        crm = GovernedCRMClient(client, task="crm_workflow")

        # Lookup - allowed for researcher and specialist
        result = crm.lookup("ACME123")

        # Update - requires approval for specialist, denied for researcher
        result = crm.update("ACME123", status="renewal_pending", note="Contacted")

        # With reasoning for audit trail
        result = crm.update(
            "ACME123",
            status="renewal_pending",
            note="Contacted",
            reasoning="Customer requested contract renewal review"
        )

        # Task switching (returns clone, safe for concurrent use)
        with crm.with_task("emergency_update") as crm_emergency:
            crm_emergency.update(...)
    """

    def __init__(
        self,
        agendex: AgendexClient,
        task: str,
        agent_role: Optional[str] = None,
        actions: Optional[Dict[str, str]] = None,
        on_event: Optional[Callable[[Dict], None]] = None,
        default_context: Optional[Dict[str, Any]] = None,
    ):
        """
        Initialize governed CRM client.

        Args:
            agendex: The AgendexClient instance
            task: Task context for all operations (used for policy scoping)
            agent_role: Optional role identifier for multi-agent scenarios
            actions: Override action names dict (e.g., {"lookup": "crm.read", "update": "crm.write"})
            on_event: Optional callback for governance events
            default_context: Default context merged into all operations (e.g., user_prompt)
        """
        self.agendex = agendex
        self._task = task
        self.agent_role = agent_role
        self._actions = actions or {}
        self.on_event = on_event
        self.default_context = default_context or {}

    @property
    def task(self) -> str:
        """Current task context."""
        return self._task

    def _get_action(self, operation: str) -> str:
        """
        Get action name for CRM operation (custom → global config → default).

        Args:
            operation: One of "lookup", "update"
        """
        if operation in self._actions:
            return self._actions[operation]
        from .config import get_global_config
        return get_global_config().get_action(f"crm.{operation}")

    def set_task(self, task: str) -> None:
        """
        Change the task context.

        Note: For concurrent/async code, prefer with_task() which returns
        a clone instead of mutating.
        """
        self._task = task

    def with_task(self, task: str) -> "_TaskContext":
        """
        Context manager for temporary task switch.

        Returns a clone of this client with the new task.
        Safe for concurrent/async usage - original is never mutated.

        Example:
            with crm.with_task("emergency_update") as crm_emergency:
                crm_emergency.update(...)
            # Original crm still has original task
        """
        return _TaskContext(self, task)

    def lookup(
        self,
        account_id: str,
        context: Optional[Dict[str, Any]] = None,
        reasoning: Optional[str] = None,
    ) -> Dict[str, Any]:
        """
        Look up a customer account through governance.

        Args:
            account_id: The account ID to look up
            context: Optional additional context for governance
            reasoning: Optional reasoning/explanation for audit trail
                       (auto-captured from contextvar if not provided)

        Returns:
            Account record from CRM adapter

        Raises:
            DeniedError: If lookup is denied by policy
            PendingApprovalError: If lookup requires approval
        """
        # Auto-capture reasoning from contextvar if not provided
        if reasoning is None:
            reasoning = get_reasoning()

        # Merge contexts: default → operation-specific → reasoning → agent_role
        ctx = {**self.default_context, **(context or {})}
        if reasoning:
            ctx["reasoning"] = reasoning
        if self.agent_role:
            ctx["agent_role"] = self.agent_role

        result = self.agendex.invoke(
            action=self._get_action("lookup"),
            params={"account_id": account_id},
            task=self._task,
            context=ctx if ctx else None,
            on_event=self.on_event,
        )

        return result

    def update(
        self,
        account_id: str,
        status: str,
        note: Optional[str] = None,
        context: Optional[Dict[str, Any]] = None,
        reasoning: Optional[str] = None,
    ) -> Dict[str, Any]:
        """
        Update a customer account through governance.

        Args:
            account_id: The account ID to update
            status: New status value
            note: Optional note to append
            context: Optional additional context for governance
            reasoning: Optional reasoning/explanation for audit trail
                       (auto-captured from contextvar if not provided)

        Returns:
            Updated account record from CRM adapter

        Raises:
            DeniedError: If update is denied by policy
            PendingApprovalError: If update requires approval
        """
        # Auto-capture reasoning from contextvar if not provided
        if reasoning is None:
            reasoning = get_reasoning()

        # Merge contexts: default → operation-specific → reasoning → agent_role
        ctx = {**self.default_context, **(context or {})}
        if reasoning:
            ctx["reasoning"] = reasoning
        if self.agent_role:
            ctx["agent_role"] = self.agent_role

        result = self.agendex.invoke(
            action=self._get_action("update"),
            params={
                "account_id": account_id,
                "status": status,
                "note": note or "",
            },
            task=self._task,
            context=ctx if ctx else None,
            on_event=self.on_event,
        )

        return result


class _TaskContext:
    """
    Context manager for temporary task switching.

    Returns a shallow clone instead of mutating the original,
    making it safe for concurrent/async usage.
    """

    def __init__(self, client: GovernedCRMClient, task: str):
        self.client = client
        self.new_task = task
        self.clone: Optional[GovernedCRMClient] = None

    def __enter__(self) -> GovernedCRMClient:
        self.clone = copy.copy(self.client)
        self.clone._task = self.new_task
        return self.clone

    def __exit__(self, *args: Any) -> None:
        self.clone = None

