"""Elasticsearch mapping generation — pure dict builders, no I/O.

Provides :class:`ESMappingBuilder` for generating ES index mappings, settings,
and create-index bodies from structured :class:`ESFieldDef` /
:class:`ESIndexDef` definitions.  All methods are pure functions that return
dicts — they never connect to Elasticsearch.

Mirrors the pattern of :mod:`db_core.schema.builder` (frozen dataclasses +
static-method builders).

Nested fields are first-class: an ``ESFieldDef`` with ``field_type="nested"``
(or ``"object"``) carries its sub-fields in ``properties``, which is a
recursive ``list[ESFieldDef]``.

Usage::

    from db_core.schema.es_builder import ESFieldDef, ESIndexDef, ESMappingBuilder

    index = ESIndexDef(
        name="messages",
        properties=[
            ESFieldDef("id", "keyword"),
            ESFieldDef("message", "text",
                       fields={"keyword": {"type": "keyword"}}),
            ESFieldDef("replies", "nested",
                       properties=[
                           ESFieldDef("user_id", "keyword"),
                           ESFieldDef("text", "text"),
                       ]),
        ],
        settings={"number_of_shards": 1},
    )
    body = ESMappingBuilder.build_create_body(index)
    # → {"settings": {...}, "mappings": {"properties": {...}}}
"""

from __future__ import annotations

from dataclasses import dataclass, field


# ---------------------------------------------------------------------------
# Dataclass definitions — immutable value objects
# ---------------------------------------------------------------------------


@dataclass(frozen=True)
class ESFieldDef:
    """Definition of a single Elasticsearch field.

    Args:
        name: Field name (e.g. ``"message"``, ``"user_id"``).
        field_type: ES field type — ``"text"``, ``"keyword"``, ``"long"``,
            ``"double"``, ``"boolean"``, ``"date"``, ``"nested"``,
            ``"object"``, ``"geo_point"``, etc.
        properties: Sub-fields for ``"nested"`` and ``"object"`` types.
            This recursion is the core mechanism for modeling nested
            documents.
        fields: Multi-field definitions, e.g.
            ``{"keyword": {"type": "keyword", "ignore_above": 256}}``.
        analyzer: Text analyzer name (e.g. ``"standard"``, ``"english"``).
        format_: Date format string (e.g. ``"epoch_millis"``,
            ``"strict_date_optional_time||epoch_millis"``).
        enabled: For ``"object"`` fields — when ``False``, the field is
            stored but not indexed.
        copy_to: Target field(s) to copy this field's value into.
        doc_values: Whether to store doc values (default ``True`` for most
            types, but ``False`` for ``"text"``).
        index: Whether the field is indexed (default ``True``).
    """

    name: str
    field_type: str
    properties: list[ESFieldDef] | None = None
    fields: dict | None = None
    analyzer: str | None = None
    format_: str | None = None
    enabled: bool | None = None
    copy_to: str | None = None
    doc_values: bool | None = None
    index: bool | None = None

    def to_dict(self) -> dict:
        """Render this field definition to an ES mapping fragment."""
        d: dict = {"type": self.field_type}

        if self.properties is not None:
            d["properties"] = {
                p.name: p.to_dict() for p in self.properties
            }

        if self.fields is not None:
            d["fields"] = self.fields

        if self.analyzer is not None:
            d["analyzer"] = self.analyzer

        if self.format_ is not None:
            d["format"] = self.format_

        if self.enabled is not None:
            d["enabled"] = self.enabled

        if self.copy_to is not None:
            d["copy_to"] = self.copy_to

        if self.doc_values is not None:
            d["doc_values"] = self.doc_values

        if self.index is not None:
            d["index"] = self.index

        return d


@dataclass(frozen=True)
class ESIndexDef:
    """Complete description of an Elasticsearch index.

    Args:
        name: Index name (e.g. ``"telegram_messages"``).
        properties: Top-level field definitions.
        settings: Optional index settings dict — shards, replicas,
            analysis, lifecycle policies, etc.
            Example::

                {"number_of_shards": 1,
                 "number_of_replicas": 0,
                 "analysis": {"analyzer": {"custom_analyzer": {...}}}}
    """

    name: str
    properties: list[ESFieldDef]
    settings: dict | None = None


@dataclass(frozen=True)
class ESIndexTemplateDef:
    """Definition of a composable index template (ES 7.8+).

    Args:
        name: Template name.
        index_patterns: Patterns to match, e.g. ``["logs-*"]``.
        template: The :class:`ESIndexDef` to apply to matching indices.
        priority: Template priority (higher wins).  Default 0.
        version: Template version number (optional).
        composed_of: List of component template names to compose.
        data_stream: If non-``None``, the data stream config dict
            (e.g. ``{}`` to enable).
    """

    name: str
    index_patterns: list[str]
    template: ESIndexDef
    priority: int = 0
    version: int | None = None
    composed_of: list[str] = field(default_factory=list)
    data_stream: dict | None = None


# ---------------------------------------------------------------------------
# ESMappingBuilder — pure dict generation functions
# ---------------------------------------------------------------------------


class ESMappingBuilder:
    """Generate Elasticsearch mapping/settings dicts from structured definitions.

    All methods are ``@staticmethod`` and return plain ``dict`` values —
    they perform no I/O and never connect to Elasticsearch.  Use
    :meth:`ElasticsearchClient.create_index` to apply the generated
    body.

    Usage::

        index = ESIndexDef(
            name="users",
            properties=[
                ESFieldDef("id", "keyword"),
                ESFieldDef("name", "text",
                           fields={"keyword": {"type": "keyword"}}),
            ],
        )
        body = ESMappingBuilder.build_create_body(index)
        await es.create_index("users", body)
    """

    # ------------------------------------------------------------------
    # Mapping generation
    # ------------------------------------------------------------------

    @staticmethod
    def build_mapping(index: ESIndexDef) -> dict:
        """Build the ``mappings`` portion of an index body.

        Returns a dict like::

            {"properties": {"id": {"type": "keyword"}, ...}}
        """
        properties = {
            field.name: field.to_dict()
            for field in index.properties
        }
        return {"properties": properties}

    @staticmethod
    def build_settings(index: ESIndexDef) -> dict | None:
        """Build the ``settings`` portion of an index body.

        Returns ``None`` if ``index.settings`` is ``None``.
        """
        return index.settings

    @staticmethod
    def build_create_body(index: ESIndexDef) -> dict:
        """Build the full ``PUT /{index}`` request body.

        Returns a dict combining settings and mappings::

            {
                "settings": {...},
                "mappings": {"properties": {...}}
            }
        """
        body: dict = {}
        settings = ESMappingBuilder.build_settings(index)
        if settings is not None:
            body["settings"] = settings
        body["mappings"] = ESMappingBuilder.build_mapping(index)
        return body

    # ------------------------------------------------------------------
    # Composable index template (ES 7.8+)
    # ------------------------------------------------------------------

    @staticmethod
    def build_index_template(tpl: ESIndexTemplateDef) -> dict:
        """Build a composable index template body.

        Returns the body for ``PUT /_index_template/{name}``::

            {
                "index_patterns": ["logs-*"],
                "template": {"settings": {...}, "mappings": {...}},
                "priority": 100,
                "composed_of": [...],
            }
        """
        body: dict = {
            "index_patterns": tpl.index_patterns,
            "template": ESMappingBuilder.build_create_body(tpl.template),
            "priority": tpl.priority,
        }
        if tpl.version is not None:
            body["version"] = tpl.version
        if tpl.composed_of:
            body["composed_of"] = tpl.composed_of
        if tpl.data_stream is not None:
            body["data_stream"] = tpl.data_stream
        return body

    @staticmethod
    def build_component_template(name: str, index: ESIndexDef, version: int | None = None) -> dict:
        """Build a component template body.

        Returns the body for ``PUT /_component_template/{name}``::

            {
                "template": {"settings": {...}, "mappings": {...}},
                "version": 1,
            }
        """
        body: dict = {
            "template": ESMappingBuilder.build_create_body(index),
        }
        if version is not None:
            body["version"] = version
        return body
