"""
RBSP Index Schema — field definitions and document validation.

Provides an Elasticsearch-style schema system for declaring field types,
controlling which fields are indexed/stored/analyzed, and validating
documents before they enter the index.
"""

from __future__ import annotations

import json
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import Any, Dict, List, Optional


# ---------------------------------------------------------------------------
# Field types
# ---------------------------------------------------------------------------

class FieldType(Enum):
    """Supported field types for index schema definitions."""

    TEXT = "text"
    KEYWORD = "keyword"
    INTEGER = "integer"
    FLOAT = "float"
    BOOLEAN = "boolean"
    DATE = "date"
    BINARY = "binary"


# Python type expectations per FieldType (used for validation)
_TYPE_CHECKS: Dict[FieldType, tuple] = {
    FieldType.TEXT: (str,),
    FieldType.KEYWORD: (str,),
    FieldType.INTEGER: (int,),
    FieldType.FLOAT: (int, float),
    FieldType.BOOLEAN: (bool,),
    FieldType.DATE: (str, int, float),
    FieldType.BINARY: (bytes, str),
}


# ---------------------------------------------------------------------------
# Field configuration
# ---------------------------------------------------------------------------

@dataclass
class FieldConfig:
    """Configuration for a single schema field."""

    name: str
    field_type: FieldType
    indexed: bool = True
    stored: bool = True
    analyzed: bool = True
    boost: float = 1.0
    doc_values: bool = False

    def to_dict(self) -> Dict[str, Any]:
        """Serialize to a plain dictionary."""
        return {
            "name": self.name,
            "field_type": self.field_type.value,
            "indexed": self.indexed,
            "stored": self.stored,
            "analyzed": self.analyzed,
            "boost": self.boost,
            "doc_values": self.doc_values,
        }

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> FieldConfig:
        """Deserialize from a plain dictionary."""
        return cls(
            name=data["name"],
            field_type=FieldType(data["field_type"]),
            indexed=data.get("indexed", True),
            stored=data.get("stored", True),
            analyzed=data.get("analyzed", True),
            boost=data.get("boost", 1.0),
            doc_values=data.get("doc_values", False),
        )


# ---------------------------------------------------------------------------
# Schema
# ---------------------------------------------------------------------------

class Schema:
    """Index schema defining fields and their mappings.

    Usage::

        schema = Schema("my_index")
        schema.add_field(FieldConfig("title", FieldType.TEXT, boost=2.0))
        schema.add_field(FieldConfig("tag", FieldType.KEYWORD))
        errors = schema.validate_document({"title": "hello", "tag": "py"})
    """

    def __init__(self, name: str = "default") -> None:
        self.name = name
        self._fields: Dict[str, FieldConfig] = {}

    # -- field management ---------------------------------------------------

    def add_field(self, config: FieldConfig) -> None:
        """Add a field definition to the schema.

        Raises :class:`ValueError` if a field with the same name already exists.
        """
        if config.name in self._fields:
            raise ValueError(f"Field already exists: {config.name!r}")
        self._fields[config.name] = config

    def get_field(self, name: str) -> Optional[FieldConfig]:
        """Return the :class:`FieldConfig` for *name*, or ``None``."""
        return self._fields.get(name)

    @property
    def fields(self) -> List[FieldConfig]:
        """All field configs in insertion order."""
        return list(self._fields.values())

    # -- filtered views -----------------------------------------------------

    def text_fields(self) -> List[FieldConfig]:
        """Return fields of type TEXT."""
        return [f for f in self._fields.values() if f.field_type is FieldType.TEXT]

    def keyword_fields(self) -> List[FieldConfig]:
        """Return fields of type KEYWORD."""
        return [f for f in self._fields.values() if f.field_type is FieldType.KEYWORD]

    def indexed_fields(self) -> List[FieldConfig]:
        """Return fields where ``indexed`` is ``True``."""
        return [f for f in self._fields.values() if f.indexed]

    def stored_fields(self) -> List[FieldConfig]:
        """Return fields where ``stored`` is ``True``."""
        return [f for f in self._fields.values() if f.stored]

    # -- validation ---------------------------------------------------------

    def validate_document(self, doc: Dict[str, Any]) -> List[str]:
        """Validate *doc* against this schema and return a list of errors.

        An empty list means the document is valid.
        """
        errors: List[str] = []

        for field_name, config in self._fields.items():
            if field_name not in doc:
                continue
            value = doc[field_name]
            if value is None:
                continue
            expected = _TYPE_CHECKS.get(config.field_type)
            if expected and not isinstance(value, expected):
                errors.append(
                    f"Field {field_name!r}: expected {config.field_type.value}, "
                    f"got {type(value).__name__}"
                )

        # Warn about unknown fields
        known = set(self._fields)
        for key in doc:
            if key not in known:
                errors.append(f"Unknown field: {key!r}")

        return errors

    # -- serialization ------------------------------------------------------

    def to_dict(self) -> Dict[str, Any]:
        """Serialize the schema to a plain dictionary."""
        return {
            "name": self.name,
            "fields": [f.to_dict() for f in self._fields.values()],
        }

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> Schema:
        """Deserialize a schema from a plain dictionary."""
        schema = cls(name=data.get("name", "default"))
        for fd in data.get("fields", []):
            schema.add_field(FieldConfig.from_dict(fd))
        return schema

    def save_json(self, path: str | Path) -> Path:
        """Persist the schema to a JSON file. Returns the path written."""
        p = Path(path)
        p.parent.mkdir(parents=True, exist_ok=True)
        with p.open("w", encoding="utf-8") as fh:
            json.dump(self.to_dict(), fh, indent=2)
            fh.write("\n")
        return p

    @classmethod
    def load_json(cls, path: str | Path) -> Schema:
        """Load a schema from a JSON file."""
        p = Path(path)
        with p.open("r", encoding="utf-8") as fh:
            data = json.load(fh)
        return cls.from_dict(data)

    def __repr__(self) -> str:
        return f"Schema({self.name!r}, fields={len(self._fields)})"


# ---------------------------------------------------------------------------
# Pre-built schemas
# ---------------------------------------------------------------------------

def default_code_schema() -> Schema:
    """Return a pre-built schema suitable for indexing source code files.

    Fields:
        path      -- full file path (KEYWORD, not analyzed)
        filename  -- base file name (KEYWORD, not analyzed)
        content   -- file content (TEXT, analyzed, boosted)
        language  -- programming language (KEYWORD, not analyzed)
        size      -- file size in bytes (INTEGER)
        modified  -- last modification timestamp (DATE)
    """
    schema = Schema("code")
    schema.add_field(FieldConfig(
        "path", FieldType.KEYWORD, indexed=True, stored=True, analyzed=False,
    ))
    schema.add_field(FieldConfig(
        "filename", FieldType.KEYWORD, indexed=True, stored=True, analyzed=False,
    ))
    schema.add_field(FieldConfig(
        "content", FieldType.TEXT, indexed=True, stored=True, analyzed=True,
        boost=2.0,
    ))
    schema.add_field(FieldConfig(
        "language", FieldType.KEYWORD, indexed=True, stored=True, analyzed=False,
    ))
    schema.add_field(FieldConfig(
        "size", FieldType.INTEGER, indexed=False, stored=True, analyzed=False,
    ))
    schema.add_field(FieldConfig(
        "modified", FieldType.DATE, indexed=True, stored=True, analyzed=False,
        doc_values=True,
    ))
    return schema
