"""Sync and async clients for the AmbientMeta Privacy Gateway."""

from __future__ import annotations

from typing import Any

import httpx

from ambientmeta.exceptions import (
    AmbientMetaError,
    AuthenticationError,
    NotFoundError,
    RateLimitError,
    ValidationError,
)
from ambientmeta.models import (
    BatchFeedbackResponse,
    DocumentJob,
    EntityResult,
    EntityStrength,
    FeedbackResponse,
    InsightItem,
    InsightsResponse,
    KnowledgeDepth,
    PatternResponse,
    RehydrateResponse,
    SanitizeResponse,
)

_DEFAULT_BASE_URL = "https://api.ambientmeta.com"
_DEFAULT_TIMEOUT = 30.0


def _raise_for_error(response: httpx.Response) -> None:
    """Raise an appropriate exception for error responses."""
    if response.status_code < 400:
        return

    try:
        body = response.json()
        err = body.get("error") or body.get("detail", {}).get("error", {})
        code = err.get("code", "unknown_error")
        message = err.get("message", response.text)
        suggestion = err.get("suggestion", "")
    except Exception:
        code = "unknown_error"
        message = response.text
        suggestion = ""

    if response.status_code == 401:
        raise AuthenticationError(code, message, suggestion)
    if response.status_code == 429:
        raise RateLimitError(code, message, suggestion)
    if response.status_code == 404:
        raise NotFoundError(code, message, suggestion)
    if response.status_code == 422:
        raise ValidationError(code, message, suggestion)
    raise AmbientMetaError(code, message, suggestion)


def _parse_sanitize(data: dict[str, Any], client: Any = None) -> SanitizeResponse:
    return SanitizeResponse(
        sanitized=data["sanitized"],
        session_id=data.get("session_id"),
        entities_found=data["entities_found"],
        entities=[
            EntityResult(
                placeholder=e["placeholder"],
                type=e["type"],
                confidence=e["confidence"],
            )
            for e in data["entities"]
        ],
        processing_ms=data["processing_ms"],
        redacted=data.get("redacted", False),
        _client=client,
    )


def _parse_batch_feedback(data: dict[str, Any]) -> BatchFeedbackResponse:
    return BatchFeedbackResponse(
        accepted=data["accepted"],
        correction_ids=data.get("correction_ids", []),
        contradictions_detected=data.get("contradictions_detected", 0),
        message=data.get("message", ""),
    )


def _parse_rehydrate(data: dict[str, Any]) -> RehydrateResponse:
    return RehydrateResponse(
        text=data["text"],
        entities_restored=data["entities_restored"],
        processing_ms=data["processing_ms"],
    )


def _parse_pattern(data: dict[str, Any]) -> PatternResponse:
    return PatternResponse(
        pattern_id=data["pattern_id"],
        name=data["name"],
        status=data.get("status", "active"),
    )


def _parse_insights(data: dict[str, Any]) -> InsightsResponse:
    def _parse_item(d: dict[str, Any]) -> InsightItem:
        return InsightItem(
            id=d["id"],
            type=d["type"],
            title=d["title"],
            description=d.get("description"),
            status=d.get("status", "pending"),
            data=d.get("data", {}),
            created_at=d.get("created_at", ""),
        )

    kd_data = data.get("knowledge_depth", {})
    strongest = kd_data.get("strongest_entity")
    weakest = kd_data.get("weakest_entity")

    knowledge_depth = KnowledgeDepth(
        ckd_score=kd_data.get("ckd_score", 0.0),
        maturity=kd_data.get("maturity", "nascent"),
        strongest_entity=EntityStrength(**strongest) if strongest else None,
        weakest_entity=EntityStrength(**weakest) if weakest else None,
        contradictions_pending=kd_data.get("contradictions_pending", 0),
        contradictions_resolved=kd_data.get("contradictions_resolved", 0),
        golden_set_size=kd_data.get("golden_set_size", 0),
        compiled_rules=kd_data.get("compiled_rules", 0),
        on_demand_hit_rate=kd_data.get("on_demand_hit_rate", 0.0),
    )

    return InsightsResponse(
        pending_conflicts=[_parse_item(i) for i in data.get("pending_conflicts", [])],
        proposed_improvements=[_parse_item(i) for i in data.get("proposed_improvements", [])],
        discovered_patterns=[_parse_item(i) for i in data.get("discovered_patterns", [])],
        knowledge_depth=knowledge_depth,
    )


def _parse_document_job(data: dict[str, Any]) -> DocumentJob:
    return DocumentJob(
        job_id=data["job_id"],
        filename=data.get("filename", ""),
        file_type=data.get("file_type", ""),
        status=data["status"],
        page_count=data.get("page_count"),
        total_entities=data.get("total_entities", 0),
        entity_counts=data.get("entity_counts", {}),
        error_message=data.get("error_message"),
        processing_ms=data.get("processing_ms"),
        download_available=data.get("download_available", False),
        created_at=data.get("created_at", ""),
        completed_at=data.get("completed_at"),
    )


class AmbientMeta:
    """Synchronous client for the AmbientMeta Privacy Gateway."""

    def __init__(
        self,
        api_key: str,
        base_url: str = _DEFAULT_BASE_URL,
        timeout: float = _DEFAULT_TIMEOUT,
    ) -> None:
        self._base_url = base_url.rstrip("/")
        self._client = httpx.Client(
            base_url=self._base_url,
            headers={"X-API-Key": api_key, "Content-Type": "application/json"},
            timeout=timeout,
        )

    def sanitize(
        self,
        text: str,
        entities: list[str] | None = None,
        mode: str = "sanitize",
    ) -> SanitizeResponse:
        """Sanitize text by detecting and replacing PII."""
        body: dict[str, Any] = {"text": text, "mode": mode}
        if entities is not None:
            body["config"] = {"entities": entities}
        resp = self._client.post("/v1/sanitize", json=body)
        _raise_for_error(resp)
        return _parse_sanitize(resp.json(), client=self)

    def rehydrate(self, text: str, session_id: str) -> RehydrateResponse:
        """Restore original PII from placeholders using a session ID."""
        resp = self._client.post(
            "/v1/rehydrate",
            json={"text": text, "session_id": session_id},
        )
        _raise_for_error(resp)
        return _parse_rehydrate(resp.json())

    def create_pattern(
        self,
        name: str,
        pattern: str,
        description: str = "",
        examples: list[str] | None = None,
    ) -> PatternResponse:
        """Register a custom detection pattern."""
        resp = self._client.post(
            "/v1/patterns",
            json={
                "name": name,
                "pattern": pattern,
                "description": description,
                "examples": examples or [],
            },
        )
        _raise_for_error(resp)
        return _parse_pattern(resp.json())

    def send_feedback(
        self,
        session_id: str,
        feedback_type: str,
        text_snippet: str,
        expected_type: str = "",
    ) -> FeedbackResponse:
        """Submit feedback on detection accuracy."""
        resp = self._client.post(
            "/v1/feedback",
            json={
                "session_id": session_id,
                "feedback_type": feedback_type,
                "text_snippet": text_snippet,
                "expected_type": expected_type,
            },
        )
        _raise_for_error(resp)
        return FeedbackResponse(status=resp.json().get("status", "recorded"))

    def _send_batch_feedback_sync(
        self,
        session_id: str,
        corrections: list[dict[str, str]],
    ) -> BatchFeedbackResponse:
        """Send batch feedback (internal — called by SanitizeResponse.submit_corrections)."""
        resp = self._client.post(
            "/v1/feedback/batch",
            json={"session_id": session_id, "corrections": corrections},
        )
        _raise_for_error(resp)
        return _parse_batch_feedback(resp.json())

    def get_insights(self) -> InsightsResponse:
        """Get pending insights and knowledge depth metrics."""
        resp = self._client.get("/v1/insights")
        _raise_for_error(resp)
        return _parse_insights(resp.json())

    def resolve_insight(
        self,
        insight_id: str,
        resolution: str,
        modifications: dict[str, Any] | None = None,
        reason: str | None = None,
    ) -> dict[str, Any]:
        """Resolve an insight (approve, refine, or reject)."""
        body: dict[str, Any] = {"resolution": resolution}
        if modifications is not None:
            body["modifications"] = modifications
        if reason is not None:
            body["reason"] = reason
        resp = self._client.post(f"/v1/insights/{insight_id}/resolve", json=body)
        _raise_for_error(resp)
        return resp.json()

    def redact_document(
        self,
        file_path: str,
        mode_tag: str = "",
        poll: bool = True,
        poll_interval: float = 1.0,
        timeout: float = 600.0,
    ) -> DocumentJob:
        """Upload a document for permanent PII redaction.

        Args:
            file_path: Path to the document file (PDF, DOCX, TXT, or CSV).
            mode_tag: Optional label (e.g., "FOIA", "DSAR").
            poll: If True, polls until processing completes.
            poll_interval: Seconds between status checks.
            timeout: Max seconds to wait for completion.

        Returns:
            DocumentJob with status, entity counts, and download availability.
        """
        import os
        import time

        filename = os.path.basename(file_path)
        with open(file_path, "rb") as f:
            resp = self._client.post(
                "/v1/dashboard/documents/upload",
                files={"file": (filename, f)},
                data={"mode_tag": mode_tag},
                headers={"X-API-Key": self._client.headers["X-API-Key"]},
            )
        _raise_for_error(resp)
        job = _parse_document_job(resp.json())

        if not poll:
            return job

        start = time.time()
        while job.status in ("pending", "processing"):
            if time.time() - start > timeout:
                raise AmbientMetaError("timeout", f"Document processing timed out after {timeout}s", "")
            time.sleep(poll_interval)
            resp = self._client.get(f"/v1/dashboard/documents/{job.job_id}")
            _raise_for_error(resp)
            job = _parse_document_job(resp.json())

        return job

    def download_redacted(self, job_id: str, output_path: str) -> str:
        """Download a redacted document to a local file.

        Returns the output file path.
        """
        resp = self._client.get(f"/v1/dashboard/documents/{job_id}/download")
        _raise_for_error(resp)
        with open(output_path, "wb") as f:
            f.write(resp.content)
        return output_path

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

    def __enter__(self) -> AmbientMeta:
        return self

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


class AsyncAmbientMeta:
    """Async client for the AmbientMeta Privacy Gateway."""

    def __init__(
        self,
        api_key: str,
        base_url: str = _DEFAULT_BASE_URL,
        timeout: float = _DEFAULT_TIMEOUT,
    ) -> None:
        self._base_url = base_url.rstrip("/")
        self._client = httpx.AsyncClient(
            base_url=self._base_url,
            headers={"X-API-Key": api_key, "Content-Type": "application/json"},
            timeout=timeout,
        )

    async def sanitize(
        self,
        text: str,
        entities: list[str] | None = None,
        mode: str = "sanitize",
    ) -> SanitizeResponse:
        """Sanitize text by detecting and replacing PII."""
        body: dict[str, Any] = {"text": text, "mode": mode}
        if entities is not None:
            body["config"] = {"entities": entities}
        resp = await self._client.post("/v1/sanitize", json=body)
        _raise_for_error(resp)
        return _parse_sanitize(resp.json(), client=self)

    async def rehydrate(self, text: str, session_id: str) -> RehydrateResponse:
        """Restore original PII from placeholders using a session ID."""
        resp = await self._client.post(
            "/v1/rehydrate",
            json={"text": text, "session_id": session_id},
        )
        _raise_for_error(resp)
        return _parse_rehydrate(resp.json())

    async def create_pattern(
        self,
        name: str,
        pattern: str,
        description: str = "",
        examples: list[str] | None = None,
    ) -> PatternResponse:
        """Register a custom detection pattern."""
        resp = await self._client.post(
            "/v1/patterns",
            json={
                "name": name,
                "pattern": pattern,
                "description": description,
                "examples": examples or [],
            },
        )
        _raise_for_error(resp)
        return _parse_pattern(resp.json())

    async def send_feedback(
        self,
        session_id: str,
        feedback_type: str,
        text_snippet: str,
        expected_type: str = "",
    ) -> FeedbackResponse:
        """Submit feedback on detection accuracy."""
        resp = await self._client.post(
            "/v1/feedback",
            json={
                "session_id": session_id,
                "feedback_type": feedback_type,
                "text_snippet": text_snippet,
                "expected_type": expected_type,
            },
        )
        _raise_for_error(resp)
        return FeedbackResponse(status=resp.json().get("status", "recorded"))

    async def _send_batch_feedback_async(
        self,
        session_id: str,
        corrections: list[dict[str, str]],
    ) -> BatchFeedbackResponse:
        """Send batch feedback (internal — called by SanitizeResponse.asubmit_corrections)."""
        resp = await self._client.post(
            "/v1/feedback/batch",
            json={"session_id": session_id, "corrections": corrections},
        )
        _raise_for_error(resp)
        return _parse_batch_feedback(resp.json())

    async def get_insights(self) -> InsightsResponse:
        """Get pending insights and knowledge depth metrics."""
        resp = await self._client.get("/v1/insights")
        _raise_for_error(resp)
        return _parse_insights(resp.json())

    async def resolve_insight(
        self,
        insight_id: str,
        resolution: str,
        modifications: dict[str, Any] | None = None,
        reason: str | None = None,
    ) -> dict[str, Any]:
        """Resolve an insight (approve, refine, or reject)."""
        body: dict[str, Any] = {"resolution": resolution}
        if modifications is not None:
            body["modifications"] = modifications
        if reason is not None:
            body["reason"] = reason
        resp = await self._client.post(
            f"/v1/insights/{insight_id}/resolve", json=body
        )
        _raise_for_error(resp)
        return resp.json()

    async def redact_document(
        self,
        file_path: str,
        mode_tag: str = "",
        poll: bool = True,
        poll_interval: float = 1.0,
        timeout: float = 600.0,
    ) -> DocumentJob:
        """Upload a document for permanent PII redaction.

        Args:
            file_path: Path to the document file (PDF, DOCX, TXT, or CSV).
            mode_tag: Optional label (e.g., "FOIA", "DSAR").
            poll: If True, polls until processing completes.
            poll_interval: Seconds between status checks.
            timeout: Max seconds to wait for completion.

        Returns:
            DocumentJob with status, entity counts, and download availability.
        """
        import asyncio
        import os
        import time

        filename = os.path.basename(file_path)
        with open(file_path, "rb") as f:
            resp = await self._client.post(
                "/v1/dashboard/documents/upload",
                files={"file": (filename, f)},
                data={"mode_tag": mode_tag},
                headers={"X-API-Key": self._client.headers["X-API-Key"]},
            )
        _raise_for_error(resp)
        job = _parse_document_job(resp.json())

        if not poll:
            return job

        start = time.time()
        while job.status in ("pending", "processing"):
            if time.time() - start > timeout:
                raise AmbientMetaError("timeout", f"Document processing timed out after {timeout}s", "")
            await asyncio.sleep(poll_interval)
            resp = await self._client.get(f"/v1/dashboard/documents/{job.job_id}")
            _raise_for_error(resp)
            job = _parse_document_job(resp.json())

        return job

    async def download_redacted(self, job_id: str, output_path: str) -> str:
        """Download a redacted document to a local file."""
        resp = await self._client.get(f"/v1/dashboard/documents/{job_id}/download")
        _raise_for_error(resp)
        with open(output_path, "wb") as f:
            f.write(resp.content)
        return output_path

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

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

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