"""Memories API namespace — domain-specific logic on top of the generated base."""

from __future__ import annotations

import mimetypes
from pathlib import Path
from typing import Any, Literal, Optional, overload

from pydantic import Field, StrictStr

from goodmem._generated.api.memories_base import AsyncMemoriesAPIBase, MemoriesAPIBase
from goodmem._generated.models.memory import Memory
from goodmem._generated.models.memory_creation_request import (
    MemoryCreationRequest as _GeneratedMemoryCreationRequest,
)
from goodmem._generated.models.retrieve_memory_event import RetrieveMemoryEvent
from goodmem._generated.models.retrieve_memory_request import RetrieveMemoryRequest
from goodmem.pagination import AsyncPaginator, Paginator
from goodmem.streaming import AsyncRetrieveMemoryStream, RetrieveMemoryStream
from goodmem.registries.convenience import _transform_list_kwargs, apply_convenience_transforms


class MemoryCreationRequest(_GeneratedMemoryCreationRequest):
    """Convenience version — ``content_type`` is optional (auto-inferred for text).

    Identical to the generated ``MemoryCreationRequest`` except ``content_type``
    defaults to ``None``.  When passed to ``batch_create``, the SDK infers
    ``"text/plain"`` when ``original_content`` is a string and no explicit
    ``content_type`` is provided.
    """

    content_type: Optional[StrictStr] = Field(
        default=None,
        description="MIME type of the content. Auto-inferred as 'text/plain' when original_content is a string.",
        alias="contentType",
    )


def _build_retrieve_body(message: str, kwargs: dict[str, Any]) -> dict:
    """Build RetrieveMemoryRequest dict, handling convenience params."""
    apply_convenience_transforms(("memories", "retrieve"), kwargs)

    # Extract raw post_processor (may have been assembled by the transform)
    raw_post_processor = kwargs.pop("post_processor", None)

    kwargs["message"] = message
    body = RetrieveMemoryRequest.model_validate(kwargs)
    result = body.model_dump(by_alias=True, exclude_unset=True)

    # Inject post_processor directly into serialized body — bypasses pydantic
    # validation because PostProcessor.config is typed as Dict[str, Dict[str, Any]]
    # in the spec, but the server accepts flat scalar values (strings, floats, etc.).
    # TODO(#847): Once the server defines a typed ChatPostProcessorConfig schema,
    # remove this bypass and validate through the pydantic model.  The corresponding
    # assembly logic lives in convenience.py:_transform_retrieve().
    if raw_post_processor is not None:
        result["postProcessor"] = raw_post_processor

    return result


def _infer_content_type(file_path: str | Path) -> str:
    """Infer MIME type from file extension."""
    mime_type, _ = mimetypes.guess_type(str(file_path))
    return mime_type or "application/octet-stream"


class MemoriesAPI(MemoriesAPIBase):
    """Memories API with GoodMem convenience features.

    Extends ``MemoriesAPIBase`` with:

    - **File upload**: ``file_path="doc.pdf"`` sends a multipart upload with MIME type
      auto-detected from the file extension.
    - **Content type inference**: ``content_type`` defaults to ``"text/plain"`` when
      ``original_content`` is a string and no ``content_type`` is provided.
    - **space_ids shorthand**: ``space_ids=["uuid", ...]`` converts to the ``space_keys``
      structure expected by the retrieve endpoint. Mutually exclusive with ``space_keys``.
    - **space_keys passthrough**: ``space_keys=[{"space_id": "...", "embedder_weights": [...], "filter": "..."}]``
      for advanced per-space embedder weight overrides and filters. Mutually exclusive with ``space_ids``.
    - **Post-processor flattening**: flat kwargs like ``llm_id``, ``reranker_id``,
      ``relevance_threshold``, ``prompt``, etc. are assembled into the nested
      ``PostProcessor`` structure automatically.
    """

    def create(
        self,
        *,
        space_id: str | None = None,
        file_path: str | None = None,
        **kwargs: Any,
    ) -> Memory:
        if space_id is not None:
            kwargs["space_id"] = space_id

        # Validate mutually exclusive content sources
        sources = []
        if file_path is not None:
            sources.append("file_path")
        if "original_content" in kwargs:
            sources.append("original_content")
        if "original_content_b64" in kwargs:
            sources.append("original_content_b64")
        if len(sources) > 1:
            raise ValueError(
                f"Content must be provided via exactly one of file_path, "
                f"original_content, or original_content_b64. "
                f"Got: {', '.join(sources)}"
            )

        if file_path is not None:
            return self._create_with_file(file_path, kwargs)

        # Auto-infer content_type for text
        if "content_type" not in kwargs and "original_content" in kwargs:
            if isinstance(kwargs["original_content"], str):
                kwargs["content_type"] = "text/plain"

        return super().create(**kwargs)

    def _create_with_file(self, file_path: str, kwargs: dict) -> Memory:
        """Handle multipart file upload."""
        path = Path(file_path)
        if not path.exists():
            raise FileNotFoundError(f"File not found: {path}")

        kwargs.setdefault("content_type", _infer_content_type(path))

        request_data = self._create_model.model_validate(kwargs)
        with open(path, "rb") as f:
            return super().create(
                request=request_data,
                file=(path.name, f, kwargs["content_type"]),
            )

    @overload
    def retrieve(self, *, message: str, stream: Literal[False], **kwargs: Any) -> list[RetrieveMemoryEvent]: ...
    @overload
    def retrieve(self, *, message: str, stream: Literal[True] = ..., **kwargs: Any) -> RetrieveMemoryStream: ...

    def retrieve(
        self,
        *,
        message: str,
        stream: bool = True,
        **kwargs: Any,
    ) -> RetrieveMemoryStream | list[RetrieveMemoryEvent]:
        body_dict = _build_retrieve_body(message, kwargs)
        return self._retrieve_with_body(body_dict, stream=stream, format="ndjson")

    def list(self, *, space_id: str, **kwargs: Any) -> Paginator[Memory]:
        max_items = _transform_list_kwargs(kwargs)
        paginator = super().list(space_id=space_id, **kwargs)
        paginator._max_items = max_items
        return paginator

    def batch_create(self, *, requests: list[MemoryCreationRequest]) -> Any:
        processed = []
        for req in requests:
            d = req.model_dump(by_alias=False, exclude_unset=True)
            if req.content_type is None and req.original_content is not None:
                if isinstance(req.original_content, str):
                    d["content_type"] = "text/plain"
            processed.append(d)
        return super().batch_create(requests=processed)


class AsyncMemoriesAPI(AsyncMemoriesAPIBase):
    """Async Memories API with GoodMem convenience features. See ``MemoriesAPI``."""

    async def create(
        self,
        *,
        space_id: str | None = None,
        file_path: str | None = None,
        **kwargs: Any,
    ) -> Memory:
        if space_id is not None:
            kwargs["space_id"] = space_id

        # Validate mutually exclusive content sources
        sources = []
        if file_path is not None:
            sources.append("file_path")
        if "original_content" in kwargs:
            sources.append("original_content")
        if "original_content_b64" in kwargs:
            sources.append("original_content_b64")
        if len(sources) > 1:
            raise ValueError(
                f"Content must be provided via exactly one of file_path, "
                f"original_content, or original_content_b64. "
                f"Got: {', '.join(sources)}"
            )

        if file_path is not None:
            return await self._create_with_file(file_path, kwargs)

        if "content_type" not in kwargs and "original_content" in kwargs:
            if isinstance(kwargs["original_content"], str):
                kwargs["content_type"] = "text/plain"

        return await super().create(**kwargs)

    async def _create_with_file(self, file_path: str, kwargs: dict) -> Memory:
        path = Path(file_path)
        if not path.exists():
            raise FileNotFoundError(f"File not found: {path}")

        kwargs.setdefault("content_type", _infer_content_type(path))
        request_data = self._create_model.model_validate(kwargs)
        with open(path, "rb") as f:
            return await super().create(
                request=request_data,
                file=(path.name, f, kwargs["content_type"]),
            )

    async def retrieve(
        self,
        *,
        message: str,
        stream: bool = True,
        **kwargs: Any,
    ) -> AsyncRetrieveMemoryStream | list[RetrieveMemoryEvent]:
        body_dict = _build_retrieve_body(message, kwargs)
        return await self._retrieve_with_body(body_dict, stream=stream, format="ndjson")

    def list(self, *, space_id: str, **kwargs: Any) -> AsyncPaginator[Memory]:
        max_items = _transform_list_kwargs(kwargs)
        paginator = super().list(space_id=space_id, **kwargs)
        paginator._max_items = max_items
        return paginator

    async def batch_create(self, *, requests: list[MemoryCreationRequest]) -> Any:
        processed = []
        for req in requests:
            d = req.model_dump(by_alias=False, exclude_unset=True)
            if req.content_type is None and req.original_content is not None:
                if isinstance(req.original_content, str):
                    d["content_type"] = "text/plain"
            processed.append(d)
        return await super().batch_create(requests=processed)
