"""Typed public models for the KyroDB SDK."""

from __future__ import annotations

from collections.abc import Mapping, Sequence
from dataclasses import dataclass, field
from math import isfinite
from typing import Any

try:
    import numpy as _np
except Exception:  # pragma: no cover - optional dependency
    _np = None


def _validate_doc_id(doc_id: int) -> None:
    if doc_id <= 0:
        raise ValueError("doc_id must be > 0")
    if doc_id > 0xFFFFFFFFFFFFFFFF:
        raise ValueError("doc_id exceeds uint64 range")


def _validate_vector(values: Sequence[float], label: str) -> tuple[float, ...]:
    if _np is not None and isinstance(values, _np.ndarray):
        try:
            array: Any = _np.asarray(values, dtype=_np.float32)
        except Exception:
            array = None
        else:
            if array.ndim != 1:
                raise ValueError(f"{label} must be a 1D vector")
            if int(array.size) == 0:
                raise ValueError(f"{label} must be non-empty")
            if not bool(_np.isfinite(array).all()):
                raise ValueError(f"{label} contains non-finite values")
            return tuple(float(v) for v in array)

    numeric = [float(v) for v in values]
    if not numeric:
        raise ValueError(f"{label} must be non-empty")
    if any(not isfinite(v) for v in numeric):
        raise ValueError(f"{label} contains non-finite values")
    return tuple(numeric)


@dataclass(slots=True, frozen=True)
class InsertItem:
    doc_id: int
    embedding: tuple[float, ...]
    metadata: Mapping[str, str] = field(default_factory=dict)
    namespace: str = ""

    @classmethod
    def from_parts(
        cls,
        doc_id: int,
        embedding: Sequence[float],
        metadata: Mapping[str, str] | None = None,
        namespace: str = "",
    ) -> InsertItem:
        _validate_doc_id(doc_id)
        normalized = _validate_vector(embedding, "embedding")
        return cls(
            doc_id=doc_id,
            embedding=normalized,
            metadata=dict(metadata or {}),
            namespace=namespace,
        )


@dataclass(slots=True, frozen=True)
class MetadataFilter:
    _serialized: bytes

    @classmethod
    def from_proto(cls, proto: Any) -> MetadataFilter:
        from ._generated import kyrodb_pb2 as pb2

        pb2_any: Any = pb2
        if not isinstance(proto, pb2_any.MetadataFilter):
            raise TypeError("proto must be a kyrodb MetadataFilter")
        return cls(_serialized=proto.SerializeToString(deterministic=True))

    def to_proto(self) -> Any:
        from ._generated import kyrodb_pb2 as pb2

        pb2_any: Any = pb2
        parsed = pb2_any.MetadataFilter()
        parsed.ParseFromString(self._serialized)
        return parsed


@dataclass(slots=True, frozen=True)
class SearchQuery:
    query_embedding: tuple[float, ...]
    k: int
    min_score: float = 0.0
    namespace: str = ""
    include_embeddings: bool = False
    filter: MetadataFilter | None = None
    ef_search: int = 0

    def __post_init__(self) -> None:
        normalized = _validate_vector(self.query_embedding, "query_embedding")
        object.__setattr__(self, "query_embedding", normalized)
        if not isfinite(self.min_score):
            raise ValueError("min_score must be a finite number")
        if self.k <= 0 or self.k > 1000:
            raise ValueError("k must be between 1 and 1000")
        if self.ef_search < 0:
            raise ValueError("ef_search must be >= 0")
        if self.filter is not None and not isinstance(self.filter, MetadataFilter):
            raise TypeError("filter must be MetadataFilter or None")

    @classmethod
    def from_parts(
        cls,
        *,
        query_embedding: Sequence[float],
        k: int,
        min_score: float = 0.0,
        namespace: str = "",
        include_embeddings: bool = False,
        filter: MetadataFilter | None = None,
        ef_search: int = 0,
    ) -> SearchQuery:
        return cls(
            query_embedding=tuple(query_embedding),
            k=k,
            min_score=min_score,
            namespace=namespace,
            include_embeddings=include_embeddings,
            filter=filter,
            ef_search=ef_search,
        )


@dataclass(slots=True, frozen=True)
class InsertAck:
    success: bool
    inserted_at: int
    total_inserted: int
    total_failed: int
    tier: str
    error: str = ""


@dataclass(slots=True, frozen=True)
class SearchHit:
    doc_id: int
    score: float
    metadata: Mapping[str, str] = field(default_factory=dict)
    embedding: tuple[float, ...] | None = None


@dataclass(slots=True, frozen=True)
class SearchResponse:
    results: tuple[SearchHit, ...]
    total_found: int
    search_latency_ms: float
    search_path: str
    error: str = ""


@dataclass(slots=True, frozen=True)
class QueryResult:
    found: bool
    doc_id: int
    metadata: Mapping[str, str] = field(default_factory=dict)
    embedding: tuple[float, ...] | None = None
    served_from: str = "UNKNOWN"
    error: str = ""


@dataclass(slots=True, frozen=True)
class BulkLoadResult:
    success: bool
    error: str = ""
    total_loaded: int = 0
    total_failed: int = 0
    load_duration_ms: float = 0.0
    avg_insert_rate: float = 0.0
    peak_memory_bytes: int = 0


@dataclass(slots=True, frozen=True)
class DeleteResult:
    success: bool
    error: str = ""
    existed: bool = False


@dataclass(slots=True, frozen=True)
class UpdateMetadataResult:
    success: bool
    error: str = ""
    existed: bool = False


@dataclass(slots=True, frozen=True)
class BulkQueryResult:
    results: tuple[QueryResult, ...]
    total_found: int
    total_requested: int
    error: str = ""


@dataclass(slots=True, frozen=True)
class BatchDeleteResult:
    success: bool
    deleted_count: int
    error: str = ""


@dataclass(slots=True, frozen=True)
class HealthResult:
    status: str
    version: str
    components: Mapping[str, str] = field(default_factory=dict)
    uptime_seconds: int = 0
    git_commit: str = ""


@dataclass(slots=True, frozen=True)
class MetricsResult:
    cache_hits: int
    cache_misses: int
    cache_hit_rate: float
    cache_size: int
    hot_tier_hits: int
    hot_tier_misses: int
    hot_tier_hit_rate: float
    hot_tier_size: int
    hot_tier_flushes: int
    cold_tier_searches: int
    cold_tier_size: int
    p50_latency_ms: float
    p95_latency_ms: float
    p99_latency_ms: float
    total_queries: int
    total_inserts: int
    queries_per_second: float
    inserts_per_second: float
    memory_usage_bytes: int
    disk_usage_bytes: int
    cpu_usage_percent: float
    overall_hit_rate: float
    collected_at: int


@dataclass(slots=True, frozen=True)
class FlushResult:
    success: bool
    error: str = ""
    documents_flushed: int = 0
    flush_duration_ms: float = 0.0


@dataclass(slots=True, frozen=True)
class SnapshotResult:
    success: bool
    error: str = ""
    snapshot_path: str = ""
    documents_snapshotted: int = 0
    snapshot_size_bytes: int = 0


@dataclass(slots=True, frozen=True)
class ConfigResult:
    hot_tier_max_size: int
    hot_tier_max_age_seconds: int
    hnsw_max_elements: int
    data_dir: str
    fsync_policy: str
    snapshot_interval: int
    flush_interval_seconds: int
    embedding_dimension: int
    version: str
    hnsw_m: int
    hnsw_ef_construction: int
    hnsw_ef_search: int
    hnsw_distance: str
    hnsw_disable_normalization_check: bool
