"""Normbase client abstraction and adapters for EDA engine."""
from __future__ import annotations

from typing import Any, Dict, List, Optional, Protocol, Literal

import aiohttp

from security.auth.verifier import build_internal_auth_headers


class NormbaseClient(Protocol):
    """Abstract interface for EDA to consume norm retrieval services."""

    async def retrieve_norms(
        self,
        context: Dict[str, Any],
        query_mode: Literal["structured", "semantic", "hybrid"] = "hybrid",
        nl_query: Optional[str] = None,
        agent_role: Optional[str] = None,
        agent_ids: Optional[List[str]] = None,
        tag_filters: Optional[Dict[str, List[str]]] = None,
        field_ids: Optional[List[str]] = None,
        top_k: int = 5,
    ) -> List[Dict[str, Any]]:
        ...

    async def retrieve_norms_with_meta(
        self,
        context: Dict[str, Any],
        query_mode: Literal["structured", "semantic", "hybrid"] = "hybrid",
        nl_query: Optional[str] = None,
        agent_role: Optional[str] = None,
        agent_ids: Optional[List[str]] = None,
        tag_filters: Optional[Dict[str, List[str]]] = None,
        field_ids: Optional[List[str]] = None,
        top_k: int = 5,
    ) -> Dict[str, Any]:
        ...

    async def get_default_norms(self, agent_role: str) -> List[Dict[str, Any]]:
        ...

    async def report_outcome(
        self, agent_id: str, norm_id: str, outcome: str, details: Dict[str, Any]
    ) -> None:
        ...


class MockNormbaseClient:
    """Simple in-memory normbase adapter for local prototype tests."""

    def __init__(self, norms: Optional[List[Dict[str, Any]]] = None):
        self.norms = norms or []

    async def retrieve_norms(
        self,
        context: Dict[str, Any],
        query_mode: Literal["structured", "semantic", "hybrid"] = "hybrid",
        nl_query: Optional[str] = None,
        agent_role: Optional[str] = None,
        agent_ids: Optional[List[str]] = None,
        tag_filters: Optional[Dict[str, List[str]]] = None,
        field_ids: Optional[List[str]] = None,
        top_k: int = 5,
    ) -> List[Dict[str, Any]]:
        if query_mode == "semantic" and not str(nl_query or "").strip():
            raise ValueError("nl_query is required when query_mode='semantic'.")
        del context, field_ids, nl_query
        effective_tag_filters: Dict[str, List[str]] = {}
        if isinstance(tag_filters, dict):
            for category, raw_values in tag_filters.items():
                key = str(category).strip()
                if not key:
                    continue
                if not isinstance(raw_values, list):
                    continue
                values = [str(v).strip() for v in raw_values if str(v).strip()]
                if values:
                    effective_tag_filters[key] = values
        if agent_role:
            role_values = effective_tag_filters.get("role", [])
            if agent_role not in role_values:
                effective_tag_filters["role"] = [*role_values, agent_role]
        normalized_agent_ids = sorted(
            {
                str(agent_id).strip()
                for agent_id in (agent_ids or [])
                if str(agent_id).strip()
            }
        )

        def _has_tag(norm: Dict[str, Any], category: str, values: List[str]) -> bool:
            if not values:
                return True
            expected_values = list(values)
            if category == "role" and "*" not in expected_values:
                # role='*' means global role rule (applies to everyone)
                expected_values.append("*")
            tags = norm.get("tags", []) or []
            for tag in tags:
                if not isinstance(tag, dict):
                    continue
                if str(tag.get("tag_category", "")).strip() != category:
                    continue
                if str(tag.get("tag", "")).strip() in expected_values:
                    return True
            return False

        def _passes_filters(norm: Dict[str, Any]) -> bool:
            if normalized_agent_ids:
                subject = norm.get("subject", {}) if isinstance(norm, dict) else {}
                canonical = (
                    subject.get("canonical", {}) if isinstance(subject, dict) else {}
                )
                raw_agents = canonical.get("agents", []) if isinstance(canonical, dict) else []
                norm_agents = {
                    str(item).strip()
                    for item in (raw_agents if isinstance(raw_agents, list) else [])
                    if str(item).strip()
                }
                if not norm_agents.intersection(normalized_agent_ids):
                    return False
            for category, values in effective_tag_filters.items():
                if category == "role":
                    if _has_tag(norm, "role", values):
                        continue
                    return False
                if not _has_tag(norm, category, values):
                    return False
            return True

        scoped = [
            n
            for n in self.norms
            if _passes_filters(n) and n.get("status", "active") == "active"
        ]
        return scoped[:top_k]

    async def retrieve_norms_with_meta(
        self,
        context: Dict[str, Any],
        query_mode: Literal["structured", "semantic", "hybrid"] = "hybrid",
        nl_query: Optional[str] = None,
        agent_role: Optional[str] = None,
        agent_ids: Optional[List[str]] = None,
        tag_filters: Optional[Dict[str, List[str]]] = None,
        field_ids: Optional[List[str]] = None,
        top_k: int = 5,
    ) -> Dict[str, Any]:
        norms = await self.retrieve_norms(
            context=context,
            query_mode=query_mode,
            nl_query=nl_query,
            agent_role=agent_role,
            agent_ids=agent_ids,
            tag_filters=tag_filters,
            field_ids=field_ids,
            top_k=top_k,
        )
        return {
            "norms": norms,
            "scores": [1.0 for _ in norms],
            "total": len(norms),
            "usage_token": "",
            "cache_hit": False,
            "meta": {
                "query_mode_effective": query_mode,
                "semantic_used": False,
                "semantic_attempted": bool(str(nl_query or "").strip()),
                "semantic_query_source": "nl_query" if str(nl_query or "").strip() else "none",
                "sql_candidates": len(norms),
                "vector_hits": 0,
                "semantic_hits": 0,
                "fusion_strategy": "structured_only",
            },
        }

    async def get_default_norms(self, agent_role: str) -> List[Dict[str, Any]]:
        return await self.retrieve_norms(context={}, agent_role=agent_role, top_k=20)

    async def report_outcome(
        self, agent_id: str, norm_id: str, outcome: str, details: Dict[str, Any]
    ) -> None:
        del agent_id, norm_id, outcome, details
        return None


class HttpNormbaseClient:
    """HTTP adapter over the existing Normbase API."""

    def __init__(
        self,
        base_url: str,
        retrieve_timeout_seconds: int = 60,
        outcome_timeout_seconds: int = 20,
    ):
        self.base_url = base_url.rstrip("/")
        self.retrieve_timeout_seconds = retrieve_timeout_seconds
        self.outcome_timeout_seconds = outcome_timeout_seconds

    async def retrieve_norms(
        self,
        context: Dict[str, Any],
        query_mode: Literal["structured", "semantic", "hybrid"] = "hybrid",
        nl_query: Optional[str] = None,
        agent_role: Optional[str] = None,
        agent_ids: Optional[List[str]] = None,
        tag_filters: Optional[Dict[str, List[str]]] = None,
        field_ids: Optional[List[str]] = None,
        top_k: int = 5,
    ) -> List[Dict[str, Any]]:
        data = await self.retrieve_norms_with_meta(
            context=context,
            query_mode=query_mode,
            nl_query=nl_query,
            agent_role=agent_role,
            agent_ids=agent_ids,
            tag_filters=tag_filters,
            field_ids=field_ids,
            top_k=top_k,
        )
        return list(data.get("norms", []) or [])

    async def retrieve_norms_with_meta(
        self,
        context: Dict[str, Any],
        query_mode: Literal["structured", "semantic", "hybrid"] = "hybrid",
        nl_query: Optional[str] = None,
        agent_role: Optional[str] = None,
        agent_ids: Optional[List[str]] = None,
        tag_filters: Optional[Dict[str, List[str]]] = None,
        field_ids: Optional[List[str]] = None,
        top_k: int = 5,
    ) -> Dict[str, Any]:
        payload: Dict[str, Any] = {
            "context": context,
            "query_mode": query_mode,
            "top_k": top_k,
        }
        if nl_query:
            payload["nl_query"] = nl_query
        if agent_role:
            payload["agent_role"] = agent_role
        if agent_ids:
            payload["agent_ids"] = agent_ids
        if tag_filters:
            payload["tag_filters"] = tag_filters
        if field_ids:
            payload["field_ids"] = field_ids
        headers = build_internal_auth_headers()
        async with aiohttp.ClientSession() as session:
            async with session.post(
                f"{self.base_url}/api/v1/retrieve",
                json=payload,
                headers=headers,
                timeout=aiohttp.ClientTimeout(total=self.retrieve_timeout_seconds),
            ) as response:
                response.raise_for_status()
                data = await response.json()
                return data or {}

    async def get_default_norms(self, agent_role: str) -> List[Dict[str, Any]]:
        # Prototype default: use retrieval with empty context.
        return await self.retrieve_norms(context={}, agent_role=agent_role, top_k=20)

    async def report_outcome(
        self, agent_id: str, norm_id: str, outcome: str, details: Dict[str, Any]
    ) -> None:
        usage_token = str((details or {}).get("usage_token", "")).strip()
        if not usage_token:
            # Normbase log-outcome requires usage_token binding; keep this best-effort
            # API as a no-op unless caller supplies it in details.
            return None
        payload = {
            "usage_token": usage_token,
            "agent_id": agent_id,
            "norm_id": norm_id,
            "outcome": outcome,
            "details": details,
        }
        headers = build_internal_auth_headers()
        async with aiohttp.ClientSession() as session:
            async with session.post(
                f"{self.base_url}/api/v1/norms/log-outcome",
                json=payload,
                headers=headers,
                timeout=aiohttp.ClientTimeout(total=self.outcome_timeout_seconds),
            ) as response:
                if response.status >= 400:
                    return None
        return None
