from __future__ import annotations

"""Async Nexure SDK client.

Usage:
    from nexure import AsyncNexure

    async with AsyncNexure(api_key="nx_live_...") as nx:
        record = await nx.decisions.record(
            decision_type="credit_underwriting",
            evidence={"fico_score": 711},
            action="approve",
            decided_by="reviewer_schen",
        )
"""

import os
from datetime import datetime
from typing import Any

import httpx

from nexure.exceptions import NexureError
from nexure.models import (
    AnalyticsComparisons,
    AnalyticsDrift,
    AnalyticsOverrides,
    AnalyticsOverview,
    AnomalyDetail,
    AnomalyList,
    ApiKeyCreated,
    ApiKeyList,
    BaselineResult,
    BatchOutcomeResult,
    DecisionBrief,
    DecisionList,
    DecisionOutcomes,
    DecisionPackage,
    DecisionRecord,
    DetectionResult,
    ExplanationInfo,
    ExplanationListResult,
    ExplanationTemplateDetail,
    ExplanationTemplateInfo,
    ExplanationTemplateList,
    ForensicReport,
    LedgerVerification,
    OutcomeRecord,
    OutcomeStats,
    OutcomeTypeInfo,
    OutcomeTypeList,
    OverrideOutcomes,
    PortalExplanation,
    PortalLink,
    ReviewerOutcomes,
    TemplateDetail,
    TemplateInfo,
    TemplateList,
    VisibilityScore,
)
from nexure.resources._async_base import AsyncBaseResource

DEFAULT_BASE_URL = "https://api.nexuredata.com/v1"


# ── Async Resource Classes ──────────────────────────────────────


class AsyncDecisionsResource(AsyncBaseResource):
    """Async nx.decisions — record, retrieve, and list decisions."""

    async def record(
        self,
        *,
        decision_type: str,
        evidence: dict[str, Any],
        action: str,
        decided_by: str,
        package_id: str | None = None,
        brief_id: str | None = None,
        subject: dict[str, Any] | None = None,
        conditions: str | None = None,
        reasoning: str | None = None,
        override_reason: str | None = None,
        policy_context: dict[str, Any] | None = None,
        model_output: dict[str, Any] | None = None,
        source_system: str | None = None,
        source_agent: str | None = None,
        decided_at: datetime | None = None,
        trust_score: int | None = None,
        classification: str | None = None,
        metadata: dict[str, Any] | None = None,
    ) -> DecisionRecord:
        """Record a new decision.

        Args:
            package_id: Reuse an existing package instead of creating one.
            brief_id: Link an existing brief (enables overrode_brief detection).
        """
        payload: dict[str, Any] = {
            "decision_type": decision_type,
            "evidence": evidence,
            "action": action,
            "decided_by": decided_by,
        }
        if package_id is not None:
            payload["package_id"] = package_id
        if brief_id is not None:
            payload["brief_id"] = brief_id
        if subject is not None:
            payload["subject"] = subject
        if conditions is not None:
            payload["conditions"] = conditions
        if reasoning is not None:
            payload["reasoning"] = reasoning
        if override_reason is not None:
            payload["override_reason"] = override_reason
        if policy_context is not None:
            payload["policy_context"] = policy_context
        if model_output is not None:
            payload["model_output"] = model_output
        if source_system is not None:
            payload["source_system"] = source_system
        if source_agent is not None:
            payload["source_agent"] = source_agent
        if decided_at is not None:
            payload["decided_at"] = decided_at.isoformat()
        if trust_score is not None:
            payload["trust_score"] = trust_score
        if classification is not None:
            payload["classification"] = classification
        if metadata is not None:
            payload["metadata"] = metadata

        data = await self._post("/decisions", json=payload)
        return DecisionRecord(**data)

    async def get(self, decision_id: str) -> DecisionRecord:
        """Get a decision record by ID."""
        data = await self._get(f"/decisions/{decision_id}")
        return DecisionRecord(**data)

    async def forensics(self, decision_id: str) -> ForensicReport:
        """Full forensic reconstruction of a decision."""
        data = await self._get(f"/decisions/{decision_id}/forensics")
        return ForensicReport(**data)

    async def list(
        self,
        *,
        limit: int = 50,
        offset: int = 0,
        decision_type: str | None = None,
        action: str | None = None,
        decided_by: str | None = None,
        overrides_only: bool = False,
        start_date: str | None = None,
        end_date: str | None = None,
    ) -> DecisionList:
        """List decision records with optional filters."""
        params: dict[str, Any] = {"limit": limit, "offset": offset}
        if decision_type:
            params["decision_type"] = decision_type
        if action:
            params["action"] = action
        if decided_by:
            params["decided_by"] = decided_by
        if overrides_only:
            params["overrode_model"] = "true"
        if start_date:
            params["start_date"] = start_date
        if end_date:
            params["end_date"] = end_date

        data = await self._get("/decisions", params=params)
        return DecisionList(**data)

    async def export(
        self,
        *,
        format: str = "csv",
        decision_type: str | None = None,
        action: str | None = None,
        decided_by: str | None = None,
        start_date: str | None = None,
        end_date: str | None = None,
        overrides_only: bool = False,
    ) -> bytes:
        """Export decisions as CSV or JSON bytes (Pro tier)."""
        params: dict[str, Any] = {"format": format}
        if decision_type:
            params["decision_type"] = decision_type
        if action:
            params["action"] = action
        if decided_by:
            params["decided_by"] = decided_by
        if overrides_only:
            params["overrode_model"] = "true"
        if start_date:
            params["start_date"] = start_date
        if end_date:
            params["end_date"] = end_date

        resp = await self._client.get("/decisions/export", params=params)
        if resp.status_code >= 400:
            from nexure.exceptions import _handle_response

            body = resp.json() if resp.content else {}
            _handle_response(resp.status_code, body, dict(resp.headers))
        return resp.content


class AsyncPackagesResource(AsyncBaseResource):
    """Async nx.packages — create and retrieve decision packages."""

    async def create(
        self,
        *,
        decision_type: str,
        evidence: dict[str, Any],
        subject: dict[str, Any] | None = None,
        policy_context: dict[str, Any] | None = None,
        model_output: dict[str, Any] | None = None,
        source_system: str | None = None,
        source_agent: str | None = None,
        metadata: dict[str, Any] | None = None,
    ) -> DecisionPackage:
        """Create a standalone decision package."""
        payload: dict[str, Any] = {
            "decision_type": decision_type,
            "evidence": evidence,
        }
        if subject is not None:
            payload["subject"] = subject
        if policy_context is not None:
            payload["policy_context"] = policy_context
        if model_output is not None:
            payload["model_output"] = model_output
        if source_system is not None:
            payload["source_system"] = source_system
        if source_agent is not None:
            payload["source_agent"] = source_agent
        if metadata is not None:
            payload["metadata"] = metadata

        data = await self._post("/packages", json=payload)
        return DecisionPackage(**data)

    async def get(self, package_id: str) -> DecisionPackage:
        """Get a decision package by ID."""
        data = await self._get(f"/packages/{package_id}")
        return DecisionPackage(**data)


class AsyncBriefsResource(AsyncBaseResource):
    """Async nx.briefs — generate decision briefs."""

    async def generate(
        self,
        *,
        package_id: str | None = None,
        decision_type: str | None = None,
        evidence: dict[str, Any] | None = None,
        policy_context: dict[str, Any] | None = None,
        model_output: dict[str, Any] | None = None,
        include_comparables: bool = False,
    ) -> DecisionBrief:
        """Generate a decision brief."""
        payload: dict[str, Any] = {
            "include_comparables": include_comparables,
        }
        if package_id is not None:
            payload["package_id"] = package_id
        if decision_type is not None:
            payload["decision_type"] = decision_type
        if evidence is not None:
            payload["evidence"] = evidence
        if policy_context is not None:
            payload["policy_context"] = policy_context
        if model_output is not None:
            payload["model_output"] = model_output

        data = await self._post("/briefs/generate", json=payload)
        return DecisionBrief(**data)

    async def get(self, brief_id: str) -> DecisionBrief:
        """Get a previously generated brief by ID."""
        data = await self._get(f"/briefs/{brief_id}")
        return DecisionBrief(**data)


class AsyncAnalyticsResource(AsyncBaseResource):
    """Async nx.analytics — decision analytics."""

    async def overview(
        self, *, days: int = 30, decision_type: str | None = None
    ) -> AnalyticsOverview:
        """Get decision overview metrics."""
        params: dict[str, Any] = {"days": days}
        if decision_type is not None:
            params["decision_type"] = decision_type
        data = await self._get("/analytics/overview", params=params)
        return AnalyticsOverview(**data)

    async def override_rates(
        self, *, lookback_days: int = 30, decision_type: str | None = None
    ) -> AnalyticsOverrides:
        """Get per-reviewer override rates with anomaly detection.

        Args:
            lookback_days: Number of days to look back (default 30).
            decision_type: Filter by decision type.
        """
        params: dict[str, Any] = {"lookback_days": lookback_days}
        if decision_type is not None:
            params["decision_type"] = decision_type
        data = await self._get("/analytics/overrides", params=params)
        return AnalyticsOverrides(**data)

    async def drift(self, *, weeks: int = 12, decision_type: str | None = None) -> AnalyticsDrift:
        """Get week-over-week decision pattern drift."""
        params: dict[str, Any] = {"weeks": weeks}
        if decision_type is not None:
            params["decision_type"] = decision_type
        data = await self._get("/analytics/drift", params=params)
        return AnalyticsDrift(**data)

    async def comparisons(
        self, *, limit: int = 20, min_similarity: float = 0.70, decision_type: str | None = None
    ) -> AnalyticsComparisons:
        """Find inconsistent decision pairs.

        Args:
            limit: Maximum number of pairs to return.
            min_similarity: Minimum similarity score (0.0-1.0, default 0.70).
            decision_type: Filter by decision type.
        """
        params: dict[str, Any] = {"limit": limit, "min_similarity": min_similarity}
        if decision_type is not None:
            params["decision_type"] = decision_type
        data = await self._get("/analytics/comparisons", params=params)
        return AnalyticsComparisons(**data)

    async def visibility_score(self, *, decision_type: str | None = None) -> VisibilityScore:
        """Get Decision Visibility Score (composite 0-100 metric)."""
        params: dict[str, Any] = {}
        if decision_type is not None:
            params["decision_type"] = decision_type
        data = await self._get("/analytics/visibility-score", params=params or None)
        return VisibilityScore(**data)


class AsyncAnomaliesResource(AsyncBaseResource):
    """Async nx.anomalies — anomaly detection and management."""

    async def list(
        self,
        *,
        status: str | None = None,
        severity: str | None = None,
        metric_type: str | None = None,
        since: datetime | None = None,
        limit: int = 50,
        offset: int = 0,
    ) -> AnomalyList:
        """List anomalies with optional filters."""
        params: dict[str, Any] = {"limit": limit, "offset": offset}
        if status is not None:
            params["status"] = status
        if severity is not None:
            params["severity"] = severity
        if metric_type is not None:
            params["metric_type"] = metric_type
        if since is not None:
            params["since"] = since.isoformat()
        data = await self._get("/anomalies", params=params)
        return AnomalyList(**data)

    async def get(self, anomaly_id: str) -> AnomalyDetail:
        """Get full anomaly detail including trend data."""
        data = await self._get(f"/anomalies/{anomaly_id}")
        return AnomalyDetail(**data)

    async def acknowledge(self, anomaly_id: str) -> AnomalyDetail:
        """Mark an anomaly as acknowledged."""
        data = await self._patch(f"/anomalies/{anomaly_id}", json={"status": "acknowledged"})
        return AnomalyDetail(**data)

    async def resolve(self, anomaly_id: str) -> AnomalyDetail:
        """Mark an anomaly as resolved."""
        data = await self._patch(f"/anomalies/{anomaly_id}", json={"status": "resolved"})
        return AnomalyDetail(**data)

    async def dismiss(self, anomaly_id: str, *, reason: str) -> AnomalyDetail:
        """Dismiss an anomaly with a reason."""
        data = await self._patch(
            f"/anomalies/{anomaly_id}",
            json={"status": "dismissed", "dismiss_reason": reason},
        )
        return AnomalyDetail(**data)

    async def detect(self) -> DetectionResult:
        """Trigger anomaly detection."""
        data = await self._post("/anomalies/detect")
        return DetectionResult(**data)

    async def calculate_baselines(self, *, window_days: int = 30) -> BaselineResult:
        """Recalculate metric baselines."""
        from nexure.exceptions import _handle_response

        resp = await self._client.post(
            "/anomalies/calculate-baselines",
            params={"window_days": window_days},
        )
        body = resp.json()
        _handle_response(resp.status_code, body, dict(resp.headers))
        return BaselineResult(**body)


class AsyncExplanationsResource(AsyncBaseResource):
    """Async nx.explanations — customer explanation generation."""

    async def list_templates(
        self,
        *,
        decision_type: str | None = None,
        template_type: str | None = None,
    ) -> ExplanationTemplateList:
        """List explanation templates."""
        params: dict[str, Any] = {}
        if decision_type is not None:
            params["decision_type"] = decision_type
        if template_type is not None:
            params["template_type"] = template_type
        data = await self._get("/explanation-templates", params=params or None)
        return ExplanationTemplateList(**data)

    async def create_template(self, **kwargs: Any) -> ExplanationTemplateInfo:
        """Create an explanation template."""
        data = await self._post("/explanation-templates", json=kwargs)
        return ExplanationTemplateInfo(**data)

    async def get_template(self, template_id: str) -> ExplanationTemplateDetail:
        """Get full explanation template detail."""
        data = await self._get(f"/explanation-templates/{template_id}")
        return ExplanationTemplateDetail(**data)

    async def update_template(self, template_id: str, **kwargs: Any) -> ExplanationTemplateInfo:
        """Update an explanation template."""
        data = await self._put(f"/explanation-templates/{template_id}", json=kwargs)
        return ExplanationTemplateInfo(**data)

    async def delete_template(self, template_id: str) -> None:
        """Delete an explanation template."""
        await self._delete(f"/explanation-templates/{template_id}")

    async def generate(
        self,
        decision_id: str,
        *,
        template_id: str | None = None,
        language: str = "en",
    ) -> ExplanationInfo:
        """Generate a customer-facing explanation."""
        payload: dict[str, Any] = {"language": language}
        if template_id is not None:
            payload["template_id"] = template_id
        data = await self._post(f"/decisions/{decision_id}/explanation", json=payload)
        return ExplanationInfo(**data)

    async def list(self, decision_id: str) -> ExplanationListResult:
        """List all explanations for a decision."""
        data = await self._get(f"/decisions/{decision_id}/explanations")
        return ExplanationListResult(**data)

    async def portal_link(
        self,
        decision_id: str,
        explanation_id: str,
        *,
        expires_hours: int = 72,
    ) -> PortalLink:
        """Generate a portal access link."""
        data = await self._post(
            f"/decisions/{decision_id}/explanation/portal-link",
            json={
                "explanation_id": explanation_id,
                "expires_hours": expires_hours,
            },
        )
        return PortalLink(**data)

    async def portal_view(self, token: str) -> PortalExplanation:
        """View an explanation via portal token (public)."""
        data = await self._get(f"/portal/explanation/{token}")
        return PortalExplanation(**data)


class AsyncOutcomesResource(AsyncBaseResource):
    """Async nx.outcomes — outcome tracking and analytics."""

    # ── Outcome Types ──────────────────────────────────────────

    async def list_types(self, *, decision_type: str | None = None) -> OutcomeTypeList:
        """List outcome type definitions."""
        params: dict[str, Any] = {}
        if decision_type is not None:
            params["decision_type"] = decision_type
        data = await self._get("/outcome-types", params=params or None)
        return OutcomeTypeList(**data)

    async def create_type(
        self,
        *,
        decision_type: str,
        code: str,
        label: str,
        sentiment: str,
        description: str | None = None,
        display_order: int = 0,
        is_terminal: bool = True,
    ) -> OutcomeTypeInfo:
        """Create an outcome type definition."""
        payload: dict[str, Any] = {
            "decision_type": decision_type,
            "code": code,
            "label": label,
            "sentiment": sentiment,
            "display_order": display_order,
            "is_terminal": is_terminal,
        }
        if description is not None:
            payload["description"] = description
        data = await self._post("/outcome-types", json=payload)
        return OutcomeTypeInfo(**data)

    async def update_type(self, outcome_type_id: str, **kwargs: Any) -> OutcomeTypeInfo:
        """Update an outcome type."""
        data = await self._put(f"/outcome-types/{outcome_type_id}", json=kwargs)
        return OutcomeTypeInfo(**data)

    async def delete_type(self, outcome_type_id: str) -> None:
        """Delete an outcome type."""
        await self._delete(f"/outcome-types/{outcome_type_id}")

    # ── Outcomes ───────────────────────────────────────────────

    async def record(
        self,
        *,
        decision_id: str,
        outcome_code: str,
        outcome_date: datetime | str,
        outcome_value: float | None = None,
        outcome_metadata: dict[str, Any] | None = None,
        source_reference: str | None = None,
    ) -> OutcomeRecord:
        """Record a single outcome for a decision."""
        date_str = outcome_date.isoformat() if isinstance(outcome_date, datetime) else outcome_date
        payload: dict[str, Any] = {
            "decision_id": decision_id,
            "outcome_code": outcome_code,
            "outcome_date": date_str,
        }
        if outcome_value is not None:
            payload["outcome_value"] = outcome_value
        if outcome_metadata is not None:
            payload["outcome_metadata"] = outcome_metadata
        if source_reference is not None:
            payload["source_reference"] = source_reference
        data = await self._post("/outcomes", json=payload)
        return OutcomeRecord(**data)

    async def batch(self, items: list[dict[str, Any]]) -> BatchOutcomeResult:
        """Import outcomes in batch (up to 1000)."""
        data = await self._post("/outcomes/batch", json={"items": items})
        return BatchOutcomeResult(**data)

    async def for_decision(self, decision_id: str) -> DecisionOutcomes:
        """Get all outcomes for a decision."""
        data = await self._get(f"/decisions/{decision_id}/outcomes")
        return DecisionOutcomes(**data)

    # ── Analytics ──────────────────────────────────────────────

    async def stats(self, *, decision_type: str) -> OutcomeStats:
        """Get outcome statistics."""
        data = await self._get(
            "/analytics/outcomes",
            params={"decision_type": decision_type},
        )
        return OutcomeStats(**data)

    async def by_reviewer(self, *, decision_type: str) -> ReviewerOutcomes:
        """Get outcome stats broken down by reviewer."""
        data = await self._get(
            "/analytics/outcomes/reviewers",
            params={"decision_type": decision_type},
        )
        return ReviewerOutcomes(**data)

    async def by_override(self, *, decision_type: str) -> OverrideOutcomes:
        """Get outcome stats comparing overridden vs non-overridden."""
        data = await self._get(
            "/analytics/outcomes/overrides",
            params={"decision_type": decision_type},
        )
        return OverrideOutcomes(**data)


class AsyncTemplatesResource(AsyncBaseResource):
    """Async nx.templates — template management."""

    async def list(self, *, industry: str | None = None) -> TemplateList:
        """List available templates."""
        params = {}
        if industry is not None:
            params["industry"] = industry
        data = await self._get("/templates", params=params or None)
        return TemplateList(**data)

    async def get(self, template_id: str) -> TemplateDetail:
        """Get full template detail by ID."""
        data = await self._get(f"/templates/{template_id}")
        return TemplateDetail(**data)

    async def register(
        self,
        *,
        template_yaml: str | None = None,
        filepath: str | None = None,
    ) -> TemplateInfo:
        """Register a custom template from YAML or file path."""
        from nexure.resources.templates import _resolve_yaml

        yaml_content = _resolve_yaml(template_yaml, filepath)
        data = await self._post("/templates", json={"template_yaml": yaml_content})
        return TemplateInfo(**data)

    async def update(self, template_id: str, *, template_yaml: str) -> TemplateInfo:
        """Update a template (creates new version)."""
        data = await self._put(f"/templates/{template_id}", json={"template_yaml": template_yaml})
        return TemplateInfo(**data)


class AsyncLedgerResource(AsyncBaseResource):
    """Async nx.ledger — verify hash chain integrity."""

    async def verify(self) -> LedgerVerification:
        """Verify the hash chain integrity for the authenticated tenant."""
        data = await self._get("/ledger/verify")
        return LedgerVerification(**data)


class AsyncApiKeysResource(AsyncBaseResource):
    """Async nx.api_keys — manage API keys."""

    async def create(self, *, name: str | None = None) -> ApiKeyCreated:
        """Create a new API key. The raw key is returned only once."""
        body = {}
        if name is not None:
            body["name"] = name
        data = await self._post("/api-keys", json=body)
        return ApiKeyCreated(**data)

    async def list(self) -> ApiKeyList:
        """List all API keys for the tenant (masked)."""
        data = await self._get("/api-keys")
        return ApiKeyList(**data)

    async def revoke(self, key_id: str) -> None:
        """Revoke an API key."""
        await self._delete(f"/api-keys/{key_id}")


# ── Main Async Client ───────────────────────────────────────────


class AsyncNexure:
    """Async Nexure SDK client.

    Args:
        api_key: API key (or set NEXURE_API_KEY env var).
        base_url: API base URL (or set NEXURE_API_BASE_URL env var).
        timeout: Request timeout in seconds (default 30).
    """

    def __init__(
        self,
        api_key: str | None = None,
        base_url: str | None = None,
        timeout: float = 30.0,
    ):
        self.api_key = api_key or os.environ.get("NEXURE_API_KEY")
        if not self.api_key:
            raise NexureError(
                "API key is required. Pass api_key= or set NEXURE_API_KEY environment variable."
            )

        self.base_url = base_url or os.environ.get("NEXURE_API_BASE_URL") or DEFAULT_BASE_URL

        self._client = httpx.AsyncClient(
            base_url=self.base_url,
            headers={
                "Authorization": f"Bearer {self.api_key}",
                "Content-Type": "application/json",
                "User-Agent": "nexure-python/0.1.0",
            },
            timeout=timeout,
        )

        # Resource namespaces
        self.decisions = AsyncDecisionsResource(self._client)
        self.packages = AsyncPackagesResource(self._client)
        self.briefs = AsyncBriefsResource(self._client)
        self.analytics = AsyncAnalyticsResource(self._client)
        self.anomalies = AsyncAnomaliesResource(self._client)
        self.outcomes = AsyncOutcomesResource(self._client)
        self.explanations = AsyncExplanationsResource(self._client)
        self.templates = AsyncTemplatesResource(self._client)
        self.ledger = AsyncLedgerResource(self._client)
        self.api_keys = AsyncApiKeysResource(self._client)

    async def close(self) -> None:
        """Close the HTTP client."""
        await self._client.aclose()

    async def __aenter__(self) -> AsyncNexure:
        return self

    async def __aexit__(self, *args: Any) -> None:
        await self.close()
