"""
Database Client with Agendex Governance.

Routes all database queries through Agendex for policy evaluation.
"""
from __future__ import annotations

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

from .context import get_reasoning

if TYPE_CHECKING:
    from .client import AgendexClient

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


class GovernedDBClient:
    """
    Database client that routes all queries through Agendex governance.

    The agent never has direct database credentials - all queries go through
    Agendex proxy which holds credentials and enforces policy.

    Example:
        from agendex import AgendexClient
        from agendex.db import GovernedDBClient

        client = AgendexClient()
        db = GovernedDBClient(client, task="monthly_report")

        # This query will be governed
        result = db.query("monthly_revenue", {"month": "2025-01"})

        # With reasoning for audit trail
        result = db.query(
            "monthly_revenue",
            {"month": "2025-01"},
            reasoning="User requested Q1 financial summary"
        )

        # Task switching (returns clone, safe for concurrent use)
        with db.with_task("audit") as db_audit:
            db_audit.query("all_transactions", {})
    """

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

        Args:
            agendex: The AgendexClient instance
            task: Task context for all queries (used for policy scoping)
            action: Override action name (default: from config or "db.query.run")
            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._action = action
        self.on_event = on_event
        self.default_context = default_context or {}

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

    @property
    def action(self) -> str:
        """
        Get the action name (custom → global config → default).
        """
        if self._action:
            return self._action
        from .config import get_global_config
        return get_global_config().get_action("db.query", "db.query.run")

    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 db.with_task("audit") as db_audit:
                db_audit.query("sensitive_data", {})
            # Original db still has original task
        """
        return _TaskContext(self, task)

    def query(
        self,
        query_id: str,
        params: Optional[Dict[str, Any]] = None,
        context: Optional[Dict[str, Any]] = None,
        reasoning: Optional[str] = None,
    ) -> Dict[str, Any]:
        """
        Execute a governed database query.

        Args:
            query_id: Identifier for the query template (e.g., "monthly_revenue")
            params: Parameters to bind to the query
            context: Optional additional context for governance
            reasoning: Optional reasoning/explanation for audit trail
                       (auto-captured from contextvar if not provided)

        Returns:
            Query result from the database adapter

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

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

        result = self.agendex.invoke(
            action=self.action,
            params={
                "query_id": query_id,
                "params": params or {},
            },
            task=self._task,
            context=merged_context if merged_context else None,
            resources=[{
                "type": "db_query",
                "query_id": query_id,
            }],
            on_event=self.on_event,
        )

        return result

    def execute(
        self,
        query_id: str,
        params: Optional[Dict[str, Any]] = None,
        reasoning: Optional[str] = None,
    ) -> List[Dict[str, Any]]:
        """
        Execute query and return rows directly.

        Convenience method that extracts rows from result.
        Reasoning is auto-captured from contextvar if not provided.
        """
        # Auto-capture reasoning from contextvar if not provided
        if reasoning is None:
            reasoning = get_reasoning()
        result = self.query(query_id, params, reasoning=reasoning)
        return result.get("rows", [])


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: GovernedDBClient, task: str):
        self.client = client
        self.new_task = task
        self.clone: Optional[GovernedDBClient] = None

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

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

