"""Sync and async clients for the Episodial Memory Core API."""

from __future__ import annotations

from typing import Any, Dict, List, Optional
from urllib.parse import quote

import httpx

from episodial.exceptions import EpisodialError, InsufficientBalanceError
from episodial.types import (
    BalanceResponse,
    ConsolidateResponse,
    DependencyStatus,
    ExportResponse,
    FeedbackResponse,
    ForgetResponse,
    HealthResponse,
    ImportResult,
    IngestResponse,
    MemoryBlock,
    OperationPricing,
    PricingResponse,
    Procedure,
    ProcedureMatchResponse,
    RetrievalResponse,
    TopupPack,
    TopupResponse,
)


def _build_headers(api_key: Optional[str], org_id: Optional[str]) -> Dict[str, str]:
    headers: Dict[str, str] = {"Content-Type": "application/json"}
    if api_key:
        headers["Authorization"] = f"Bearer {api_key}"
    if org_id:
        headers["X-Organization-Id"] = org_id
    return headers


def _raise_for_status(response: httpx.Response) -> None:
    if response.status_code == 402:
        raise InsufficientBalanceError(response.text)
    if response.status_code >= 400:
        raise EpisodialError(
            f"{response.request.method} {response.request.url.path} "
            f"returned {response.status_code}: {response.text}",
            status=response.status_code,
            body=response.text,
        )


# ---------------------------------------------------------------------------
# Deserialization helpers
# ---------------------------------------------------------------------------

def _parse_health(data: Dict[str, Any]) -> HealthResponse:
    deps = {
        k: DependencyStatus(**v) for k, v in data.get("dependencies", {}).items()
    }
    return HealthResponse(
        status=data["status"],
        service=data["service"],
        version=data["version"],
        timestamp=data["timestamp"],
        dependencies=deps,
    )


def _parse_ingest(data: Dict[str, Any]) -> IngestResponse:
    return IngestResponse(
        success=data["success"],
        event_id=data["event_id"],
        message=data["message"],
    )


def _parse_retrieval(data: Dict[str, Any]) -> RetrievalResponse:
    memories = [
        MemoryBlock(
            content=m["content"],
            memory_type=m["memory_type"],
            salience=m["salience"],
            relevance_score=m["relevance_score"],
            created_at=m["created_at"],
            source_ids=m.get("source_ids", []),
            chat_id=m.get("chat_id"),
        )
        for m in data.get("memories", [])
    ]
    return RetrievalResponse(
        memories=memories,
        total_tokens=data["total_tokens"],
        truncated=data.get("truncated", False),
    )


def _parse_feedback(data: Dict[str, Any]) -> FeedbackResponse:
    return FeedbackResponse(
        success=data["success"],
        memory_id=data["memory_id"],
        new_salience=data["new_salience"],
        message=data["message"],
    )


def _parse_forget(data: Dict[str, Any]) -> ForgetResponse:
    return ForgetResponse(
        success=data["success"],
        forgotten_count=data["forgotten_count"],
        audit_id=data["audit_id"],
        message=data["message"],
    )


def _parse_consolidate(data: Dict[str, Any]) -> ConsolidateResponse:
    return ConsolidateResponse(
        success=data["success"],
        clusters_found=data.get("clusters_found", 0),
        memories_created=data.get("memories_created", 0),
    )


def _parse_procedure_match(data: Dict[str, Any]) -> ProcedureMatchResponse:
    procedures = [
        Procedure(
            id=p["id"],
            pattern_name=p["pattern_name"],
            readable=p["readable"],
            condition=p["condition"],
            sequence=p["sequence"],
            outcome=p["outcome"],
            confidence=p["confidence"],
            occurrence_count=p["occurrence_count"],
        )
        for p in data.get("procedures", [])
    ]
    return ProcedureMatchResponse(
        success=data["success"],
        procedures=procedures,
        count=data["count"],
    )


def _parse_balance(data: Dict[str, Any]) -> BalanceResponse:
    return BalanceResponse(
        balance_cents=data["balance_cents"],
        balance_dollars=data["balance_dollars"],
        today_operations=data.get("today_operations", {}),
        today_total_operations=data.get("today_total_operations", 0),
        today_cost_cents=data.get("today_cost_cents", 0),
        today_cost_dollars=data.get("today_cost_dollars", 0.0),
    )


def _parse_pricing(data: Dict[str, Any]) -> PricingResponse:
    ops = {
        k: OperationPricing(
            label=v["label"],
            cost_per_1000=v["cost_per_1000"],
            cost_per_unit_cents=v["cost_per_unit_cents"],
        )
        for k, v in data.get("operations", {}).items()
    }
    packs = [
        TopupPack(
            name=p["name"],
            deposit_cents=p["deposit_cents"],
            bonus_pct=p["bonus_pct"],
            balance_cents=p["balance_cents"],
            note=p.get("note"),
        )
        for p in data.get("topup_packs", [])
    ]
    return PricingResponse(
        model=data["model"],
        currency=data["currency"],
        operations=ops,
        topup_packs=packs,
    )


def _parse_topup(data: Dict[str, Any]) -> TopupResponse:
    return TopupResponse(
        pack=data["pack"],
        deposited_cents=data["deposited_cents"],
        bonus_cents=data["bonus_cents"],
        total_credited_cents=data["total_credited_cents"],
        new_balance_cents=data["new_balance_cents"],
    )


def _parse_import(data: Dict[str, Any]) -> ImportResult:
    return ImportResult(
        success=data["success"],
        imported=data["imported"],
        skipped=data["skipped"],
        errors=data["errors"],
        total=data["total"],
        dry_run=data["dry_run"],
        details=data.get("details", []),
    )


def _parse_export(data: Dict[str, Any]) -> ExportResponse:
    return ExportResponse(
        user_id=data["user_id"],
        project_id=data.get("project_id"),
        exported_at=data["exported_at"],
        format_version=data.get("format_version", "1.0"),
        episodic_memories=data.get("episodic_memories", []),
        semantic_memories=data.get("semantic_memories", []),
        total_episodic=data.get("total_episodic", 0),
        total_semantic=data.get("total_semantic", 0),
    )


# =========================================================================
# Synchronous Client
# =========================================================================

class Episodial:
    """Synchronous client for the Episodial Memory Core API."""

    def __init__(
        self,
        *,
        base_url: str = "http://localhost:8000",
        api_key: Optional[str] = None,
        organization_id: Optional[str] = None,
        project_id: Optional[str] = None,
        timeout: float = 30.0,
    ) -> None:
        self.base_url = base_url.rstrip("/")
        self.api_key = api_key
        self.organization_id = organization_id
        self.project_id = project_id or "default"
        self._client = httpx.Client(
            base_url=self.base_url,
            headers=_build_headers(api_key, organization_id),
            timeout=timeout,
        )

    def close(self) -> None:
        self._client.close()

    def __enter__(self) -> "Episodial":
        return self

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

    # -- HTTP helpers -------------------------------------------------------

    def _get(self, path: str) -> Any:
        r = self._client.get(path)
        _raise_for_status(r)
        return r.json()

    def _post(self, path: str, json: Any = None) -> Any:
        r = self._client.post(path, json=json)
        _raise_for_status(r)
        return r.json()

    def _delete(self, path: str) -> Any:
        r = self._client.delete(path)
        _raise_for_status(r)
        return r.json()

    # -- Health -------------------------------------------------------------

    def health(self) -> HealthResponse:
        """Check service health and dependency status."""
        return _parse_health(self._get("/health"))

    # -- Memory: Ingest -----------------------------------------------------

    def ingest(
        self,
        *,
        user_id: str,
        project_id: Optional[str] = None,
        chat_id: Optional[str] = None,
        event_type: str = "llm_interaction",
        payload: Dict[str, Any],
        initial_salience: float = 0.5,
    ) -> IngestResponse:
        """Ingest a single episodic event into memory."""
        return _parse_ingest(
            self._post(
                "/memory/ingest",
                json={
                    "event": {
                        "user_id": user_id,
                        "project_id": project_id or self.project_id,
                        "chat_id": chat_id,
                        "event_type": event_type,
                        "payload": payload,
                        "initial_salience": initial_salience,
                    }
                },
            )
        )

    def remember(
        self,
        *,
        user_id: str,
        user_message: str,
        assistant_message: str,
        project_id: Optional[str] = None,
        chat_id: Optional[str] = None,
        salience: float = 0.5,
    ) -> IngestResponse:
        """Convenience: store an LLM interaction as a memory."""
        return self.ingest(
            user_id=user_id,
            project_id=project_id,
            chat_id=chat_id,
            event_type="llm_interaction",
            payload={
                "user_message": user_message,
                "assistant_message": assistant_message,
            },
            initial_salience=salience,
        )

    # -- Memory: Retrieve ---------------------------------------------------

    def retrieve(
        self,
        *,
        user_id: str,
        task_goal: str,
        project_id: Optional[str] = None,
        chat_id: Optional[str] = None,
        include_global: bool = True,
        chat_only: bool = False,
        token_budget: int = 2000,
    ) -> RetrievalResponse:
        """Retrieve relevant memories for a given context."""
        return _parse_retrieval(
            self._post(
                "/memory/retrieve",
                json={
                    "user_id": user_id,
                    "project_id": project_id or self.project_id,
                    "chat_id": chat_id,
                    "include_global": include_global,
                    "chat_only": chat_only,
                    "task_goal": task_goal,
                    "token_budget": token_budget,
                },
            )
        )

    def recall(
        self,
        user_id: str,
        query: str,
        *,
        project_id: Optional[str] = None,
        chat_id: Optional[str] = None,
        token_budget: int = 2000,
    ) -> RetrievalResponse:
        """Convenience: recall memories for a user's current query."""
        return self.retrieve(
            user_id=user_id,
            task_goal=query,
            project_id=project_id,
            chat_id=chat_id,
            token_budget=token_budget,
        )

    # -- Memory: Feedback ---------------------------------------------------

    def feedback(
        self,
        *,
        memory_id: str,
        feedback_type: str,
        value: float = 1.0,
        context: Optional[str] = None,
    ) -> FeedbackResponse:
        """Provide feedback on a memory to adjust its salience."""
        body: Dict[str, Any] = {
            "memory_id": memory_id,
            "feedback_type": feedback_type,
            "value": value,
        }
        if context:
            body["context"] = context
        return _parse_feedback(self._post("/memory/feedback", json=body))

    def helpful(self, memory_id: str) -> FeedbackResponse:
        """Shortcut: mark a memory as helpful."""
        return self.feedback(memory_id=memory_id, feedback_type="positive", value=1.0)

    def unhelpful(self, memory_id: str) -> FeedbackResponse:
        """Shortcut: mark a memory as unhelpful."""
        return self.feedback(memory_id=memory_id, feedback_type="negative", value=1.0)

    # -- Memory: Forget -----------------------------------------------------

    def forget(
        self,
        *,
        reason: str,
        memory_ids: Optional[List[str]] = None,
        user_id: Optional[str] = None,
        project_id: Optional[str] = None,
    ) -> ForgetResponse:
        """Forget specific memories or all for a user/project."""
        body: Dict[str, Any] = {"reason": reason}
        if memory_ids:
            body["memory_ids"] = memory_ids
        if user_id:
            body["user_id"] = user_id
        if project_id:
            body["project_id"] = project_id
        return _parse_forget(self._post("/memory/forget", json=body))

    # -- Memory: Consolidation ----------------------------------------------

    def consolidate(
        self,
        user_id: str,
        project_id: Optional[str] = None,
        *,
        force: bool = False,
    ) -> ConsolidateResponse:
        """Run memory consolidation for a user/project scope."""
        pid = project_id or self.project_id
        return _parse_consolidate(
            self._post(
                f"/memory/consolidate?user_id={user_id}&project_id={pid}&force={force}"
            )
        )

    # -- Memory: Procedural -------------------------------------------------

    def mine_procedures(
        self,
        user_id: str,
        project_id: Optional[str] = None,
    ) -> Dict[str, Any]:
        """Mine procedural patterns from episodic memories."""
        pid = project_id or self.project_id
        return self._post(f"/memory/procedures/mine?user_id={user_id}&project_id={pid}")

    def match_procedures(
        self,
        user_id: str,
        context: str,
        *,
        project_id: Optional[str] = None,
        limit: int = 5,
    ) -> ProcedureMatchResponse:
        """Find procedural patterns matching the current context."""
        pid = project_id or self.project_id
        ctx = quote(context)
        return _parse_procedure_match(
            self._post(
                f"/memory/procedures/match?user_id={user_id}"
                f"&project_id={pid}&context={ctx}&limit={limit}"
            )
        )

    # -- Billing ------------------------------------------------------------

    def get_balance(self, organization_id: Optional[str] = None) -> BalanceResponse:
        """Get current balance and today's usage."""
        oid = organization_id or self.organization_id
        if not oid:
            raise EpisodialError("organization_id is required for billing")
        return _parse_balance(self._get(f"/billing/balance?organization_id={oid}"))

    def get_pricing(self) -> PricingResponse:
        """Get per-operation pricing and available top-up packs."""
        return _parse_pricing(self._get("/billing/pricing"))

    def topup(
        self,
        pack: str,
        organization_id: Optional[str] = None,
    ) -> TopupResponse:
        """Purchase a top-up pack."""
        oid = organization_id or self.organization_id
        if not oid:
            raise EpisodialError("organization_id is required for billing")
        return _parse_topup(
            self._post("/billing/topup", json={"organization_id": oid, "pack": pack})
        )

    # -- Migration ----------------------------------------------------------

    def migrate_import(
        self,
        *,
        user_id: str,
        items: List[Dict[str, Any]],
        project_id: Optional[str] = None,
        dry_run: bool = False,
        skip_duplicates: bool = True,
    ) -> ImportResult:
        """Bulk import memories in native format."""
        return _parse_import(
            self._post(
                "/migrate/import",
                json={
                    "user_id": user_id,
                    "project_id": project_id or self.project_id,
                    "items": items,
                    "dry_run": dry_run,
                    "skip_duplicates": skip_duplicates,
                },
            )
        )

    def migrate_import_conversations(
        self,
        *,
        user_id: str,
        conversations: List[Dict[str, Any]],
        project_id: Optional[str] = None,
        dry_run: bool = False,
    ) -> ImportResult:
        """Import from OpenAI-style conversations."""
        return _parse_import(
            self._post(
                "/migrate/import/conversations",
                json={
                    "user_id": user_id,
                    "project_id": project_id or self.project_id,
                    "conversations": conversations,
                    "dry_run": dry_run,
                },
            )
        )

    def migrate_import_mem0(
        self,
        *,
        user_id: str,
        memories: List[Dict[str, Any]],
        project_id: Optional[str] = None,
        dry_run: bool = False,
    ) -> ImportResult:
        """Import from Mem0 export format."""
        return _parse_import(
            self._post(
                "/migrate/import/mem0",
                json={
                    "user_id": user_id,
                    "project_id": project_id or self.project_id,
                    "memories": memories,
                    "dry_run": dry_run,
                },
            )
        )

    def migrate_import_zep(
        self,
        *,
        user_id: str,
        messages: Optional[List[Dict[str, Any]]] = None,
        summaries: Optional[List[Dict[str, Any]]] = None,
        project_id: Optional[str] = None,
        dry_run: bool = False,
    ) -> ImportResult:
        """Import from Zep export format."""
        return _parse_import(
            self._post(
                "/migrate/import/zep",
                json={
                    "user_id": user_id,
                    "project_id": project_id or self.project_id,
                    "messages": messages or [],
                    "summaries": summaries or [],
                    "dry_run": dry_run,
                },
            )
        )

    def migrate_export(
        self,
        user_id: str,
        project_id: Optional[str] = None,
        *,
        include_forgotten: bool = False,
    ) -> ExportResponse:
        """Export all memories for a user/project."""
        params = f"user_id={user_id}"
        if project_id:
            params += f"&project_id={project_id}"
        if include_forgotten:
            params += "&include_forgotten=true"
        return _parse_export(self._get(f"/migrate/export?{params}"))

    def migrate_formats(self) -> Dict[str, Any]:
        """List supported import/export formats."""
        return self._get("/migrate/formats")


# =========================================================================
# Async Client
# =========================================================================

class AsyncEpisodial:
    """Async client for the Episodial Memory Core API."""

    def __init__(
        self,
        *,
        base_url: str = "http://localhost:8000",
        api_key: Optional[str] = None,
        organization_id: Optional[str] = None,
        project_id: Optional[str] = None,
        timeout: float = 30.0,
    ) -> None:
        self.base_url = base_url.rstrip("/")
        self.api_key = api_key
        self.organization_id = organization_id
        self.project_id = project_id or "default"
        self._client = httpx.AsyncClient(
            base_url=self.base_url,
            headers=_build_headers(api_key, organization_id),
            timeout=timeout,
        )

    async def close(self) -> None:
        await self._client.aclose()

    async def __aenter__(self) -> "AsyncEpisodial":
        return self

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

    # -- HTTP helpers -------------------------------------------------------

    async def _get(self, path: str) -> Any:
        r = await self._client.get(path)
        _raise_for_status(r)
        return r.json()

    async def _post(self, path: str, json: Any = None) -> Any:
        r = await self._client.post(path, json=json)
        _raise_for_status(r)
        return r.json()

    async def _delete(self, path: str) -> Any:
        r = await self._client.delete(path)
        _raise_for_status(r)
        return r.json()

    # -- Health -------------------------------------------------------------

    async def health(self) -> HealthResponse:
        return _parse_health(await self._get("/health"))

    # -- Memory: Ingest -----------------------------------------------------

    async def ingest(
        self,
        *,
        user_id: str,
        project_id: Optional[str] = None,
        chat_id: Optional[str] = None,
        event_type: str = "llm_interaction",
        payload: Dict[str, Any],
        initial_salience: float = 0.5,
    ) -> IngestResponse:
        return _parse_ingest(
            await self._post(
                "/memory/ingest",
                json={
                    "event": {
                        "user_id": user_id,
                        "project_id": project_id or self.project_id,
                        "chat_id": chat_id,
                        "event_type": event_type,
                        "payload": payload,
                        "initial_salience": initial_salience,
                    }
                },
            )
        )

    async def remember(
        self,
        *,
        user_id: str,
        user_message: str,
        assistant_message: str,
        project_id: Optional[str] = None,
        chat_id: Optional[str] = None,
        salience: float = 0.5,
    ) -> IngestResponse:
        return await self.ingest(
            user_id=user_id,
            project_id=project_id,
            chat_id=chat_id,
            event_type="llm_interaction",
            payload={
                "user_message": user_message,
                "assistant_message": assistant_message,
            },
            initial_salience=salience,
        )

    # -- Memory: Retrieve ---------------------------------------------------

    async def retrieve(
        self,
        *,
        user_id: str,
        task_goal: str,
        project_id: Optional[str] = None,
        chat_id: Optional[str] = None,
        include_global: bool = True,
        chat_only: bool = False,
        token_budget: int = 2000,
    ) -> RetrievalResponse:
        return _parse_retrieval(
            await self._post(
                "/memory/retrieve",
                json={
                    "user_id": user_id,
                    "project_id": project_id or self.project_id,
                    "chat_id": chat_id,
                    "include_global": include_global,
                    "chat_only": chat_only,
                    "task_goal": task_goal,
                    "token_budget": token_budget,
                },
            )
        )

    async def recall(
        self,
        user_id: str,
        query: str,
        *,
        project_id: Optional[str] = None,
        chat_id: Optional[str] = None,
        token_budget: int = 2000,
    ) -> RetrievalResponse:
        return await self.retrieve(
            user_id=user_id,
            task_goal=query,
            project_id=project_id,
            chat_id=chat_id,
            token_budget=token_budget,
        )

    # -- Memory: Feedback ---------------------------------------------------

    async def feedback(
        self,
        *,
        memory_id: str,
        feedback_type: str,
        value: float = 1.0,
        context: Optional[str] = None,
    ) -> FeedbackResponse:
        body: Dict[str, Any] = {
            "memory_id": memory_id,
            "feedback_type": feedback_type,
            "value": value,
        }
        if context:
            body["context"] = context
        return _parse_feedback(await self._post("/memory/feedback", json=body))

    async def helpful(self, memory_id: str) -> FeedbackResponse:
        return await self.feedback(memory_id=memory_id, feedback_type="positive", value=1.0)

    async def unhelpful(self, memory_id: str) -> FeedbackResponse:
        return await self.feedback(memory_id=memory_id, feedback_type="negative", value=1.0)

    # -- Memory: Forget -----------------------------------------------------

    async def forget(
        self,
        *,
        reason: str,
        memory_ids: Optional[List[str]] = None,
        user_id: Optional[str] = None,
        project_id: Optional[str] = None,
    ) -> ForgetResponse:
        body: Dict[str, Any] = {"reason": reason}
        if memory_ids:
            body["memory_ids"] = memory_ids
        if user_id:
            body["user_id"] = user_id
        if project_id:
            body["project_id"] = project_id
        return _parse_forget(await self._post("/memory/forget", json=body))

    # -- Memory: Consolidation ----------------------------------------------

    async def consolidate(
        self,
        user_id: str,
        project_id: Optional[str] = None,
        *,
        force: bool = False,
    ) -> ConsolidateResponse:
        pid = project_id or self.project_id
        return _parse_consolidate(
            await self._post(
                f"/memory/consolidate?user_id={user_id}&project_id={pid}&force={force}"
            )
        )

    # -- Memory: Procedural -------------------------------------------------

    async def mine_procedures(
        self,
        user_id: str,
        project_id: Optional[str] = None,
    ) -> Dict[str, Any]:
        pid = project_id or self.project_id
        return await self._post(f"/memory/procedures/mine?user_id={user_id}&project_id={pid}")

    async def match_procedures(
        self,
        user_id: str,
        context: str,
        *,
        project_id: Optional[str] = None,
        limit: int = 5,
    ) -> ProcedureMatchResponse:
        pid = project_id or self.project_id
        ctx = quote(context)
        return _parse_procedure_match(
            await self._post(
                f"/memory/procedures/match?user_id={user_id}"
                f"&project_id={pid}&context={ctx}&limit={limit}"
            )
        )

    # -- Billing ------------------------------------------------------------

    async def get_balance(self, organization_id: Optional[str] = None) -> BalanceResponse:
        oid = organization_id or self.organization_id
        if not oid:
            raise EpisodialError("organization_id is required for billing")
        return _parse_balance(await self._get(f"/billing/balance?organization_id={oid}"))

    async def get_pricing(self) -> PricingResponse:
        return _parse_pricing(await self._get("/billing/pricing"))

    async def topup(
        self,
        pack: str,
        organization_id: Optional[str] = None,
    ) -> TopupResponse:
        oid = organization_id or self.organization_id
        if not oid:
            raise EpisodialError("organization_id is required for billing")
        return _parse_topup(
            await self._post("/billing/topup", json={"organization_id": oid, "pack": pack})
        )

    # -- Migration ----------------------------------------------------------

    async def migrate_import(
        self,
        *,
        user_id: str,
        items: List[Dict[str, Any]],
        project_id: Optional[str] = None,
        dry_run: bool = False,
        skip_duplicates: bool = True,
    ) -> ImportResult:
        return _parse_import(
            await self._post(
                "/migrate/import",
                json={
                    "user_id": user_id,
                    "project_id": project_id or self.project_id,
                    "items": items,
                    "dry_run": dry_run,
                    "skip_duplicates": skip_duplicates,
                },
            )
        )

    async def migrate_import_conversations(
        self,
        *,
        user_id: str,
        conversations: List[Dict[str, Any]],
        project_id: Optional[str] = None,
        dry_run: bool = False,
    ) -> ImportResult:
        return _parse_import(
            await self._post(
                "/migrate/import/conversations",
                json={
                    "user_id": user_id,
                    "project_id": project_id or self.project_id,
                    "conversations": conversations,
                    "dry_run": dry_run,
                },
            )
        )

    async def migrate_import_mem0(
        self,
        *,
        user_id: str,
        memories: List[Dict[str, Any]],
        project_id: Optional[str] = None,
        dry_run: bool = False,
    ) -> ImportResult:
        return _parse_import(
            await self._post(
                "/migrate/import/mem0",
                json={
                    "user_id": user_id,
                    "project_id": project_id or self.project_id,
                    "memories": memories,
                    "dry_run": dry_run,
                },
            )
        )

    async def migrate_import_zep(
        self,
        *,
        user_id: str,
        messages: Optional[List[Dict[str, Any]]] = None,
        summaries: Optional[List[Dict[str, Any]]] = None,
        project_id: Optional[str] = None,
        dry_run: bool = False,
    ) -> ImportResult:
        return _parse_import(
            await self._post(
                "/migrate/import/zep",
                json={
                    "user_id": user_id,
                    "project_id": project_id or self.project_id,
                    "messages": messages or [],
                    "summaries": summaries or [],
                    "dry_run": dry_run,
                },
            )
        )

    async def migrate_export(
        self,
        user_id: str,
        project_id: Optional[str] = None,
        *,
        include_forgotten: bool = False,
    ) -> ExportResponse:
        params = f"user_id={user_id}"
        if project_id:
            params += f"&project_id={project_id}"
        if include_forgotten:
            params += "&include_forgotten=true"
        return _parse_export(await self._get(f"/migrate/export?{params}"))

    async def migrate_formats(self) -> Dict[str, Any]:
        return await self._get("/migrate/formats")
