"""Immutable, bounded read models for the NocoDB compatibility boundary."""

from __future__ import annotations

import hashlib
import json
import re
from dataclasses import dataclass, field
from itertools import islice
from types import MappingProxyType
from typing import Mapping

from ..domain.enums import OsmObjectType
from ..domain.models import OsmIdentity
from ..errors import ContractViolation

_SHA256 = re.compile(r"^[0-9a-f]{64}$")
_IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_ ]{0,127}$")
_MAX_COLUMNS = 512
_MAX_FIELDS = 512
_MAX_TEXT = 16384


def _text(value: object, field_name: str, *, max_length: int = 512) -> str:
    if not isinstance(value, str) or not value.strip():
        raise ContractViolation(f"{field_name} must be a nonblank string")
    result = value.strip()
    if len(result) > max_length:
        raise ContractViolation(f"{field_name} exceeds the bounded length")
    return result


def _positive(value: object, field_name: str) -> int:
    if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
        raise ContractViolation(f"{field_name} must be a positive integer")
    return value


def _immutable_fields(values: Mapping[str, object]) -> Mapping[str, object]:
    if not isinstance(values, Mapping):
        raise ContractViolation("fields must be a mapping")
    if len(values) > _MAX_FIELDS:
        raise ContractViolation("fields exceed the bounded count")
    result: dict[str, object] = {}
    for key, value in values.items():
        name = _text(key, "field name", max_length=128)
        if isinstance(value, str) and len(value) > _MAX_TEXT:
            raise ContractViolation(f"field {name} exceeds the bounded text length")
        if not isinstance(value, (str, int, float, bool, type(None))):
            raise ContractViolation(f"field {name} has an unsupported value type")
        result[name] = value
    return MappingProxyType(result)


@dataclass(frozen=True, slots=True)
class NocoColumn:
    """One physical Noco table column used in a schema fingerprint."""

    name: str
    db_type: str
    nullable: bool

    def __post_init__(self) -> None:
        name = _text(self.name, "name", max_length=128)
        if not _IDENTIFIER.fullmatch(name):
            raise ContractViolation("column name has an unsupported shape")
        object.__setattr__(self, "name", name)
        object.__setattr__(
            self, "db_type", _text(self.db_type, "db_type", max_length=128).casefold()
        )
        if not isinstance(self.nullable, bool):
            raise ContractViolation("nullable must be a boolean")


@dataclass(frozen=True, slots=True)
class NocoSchema:
    """Canonical physical schema evidence with a deterministic fingerprint."""

    table_id: str
    physical_schema: str
    physical_table: str
    columns: tuple[NocoColumn, ...]
    fingerprint: str = field(init=False)

    def __post_init__(self) -> None:
        object.__setattr__(self, "table_id", _text(self.table_id, "table_id", max_length=64))
        for field_name in ("physical_schema", "physical_table"):
            value = _text(getattr(self, field_name), field_name, max_length=128)
            if not _IDENTIFIER.fullmatch(value):
                raise ContractViolation(f"{field_name} has an unsupported shape")
            object.__setattr__(self, field_name, value)
        columns = tuple(islice(iter(self.columns), _MAX_COLUMNS + 1))
        if not columns or len(columns) > _MAX_COLUMNS:
            raise ContractViolation("columns must contain a bounded nonempty set")
        if any(not isinstance(column, NocoColumn) for column in columns):
            raise ContractViolation("columns must contain NocoColumn values")
        ordered = tuple(sorted(columns, key=lambda column: column.name.casefold()))
        names = [column.name.casefold() for column in ordered]
        if len(names) != len(set(names)):
            raise ContractViolation("column names must be unique case-insensitively")
        object.__setattr__(self, "columns", ordered)
        canonical = json.dumps(
            {
                "columns": [
                    {"db_type": column.db_type, "name": column.name, "nullable": column.nullable}
                    for column in ordered
                ],
                "physical_schema": self.physical_schema,
                "physical_table": self.physical_table,
                "table_id": self.table_id,
            },
            sort_keys=True,
            separators=(",", ":"),
        ).encode("utf-8")
        object.__setattr__(self, "fingerprint", hashlib.sha256(canonical).hexdigest())


@dataclass(frozen=True, slots=True)
class NocoSourceRecord:
    """A detached Noco row containing only scalar read evidence."""

    noco_id: int
    fields: Mapping[str, object]

    def __post_init__(self) -> None:
        object.__setattr__(self, "noco_id", _positive(self.noco_id, "noco_id"))
        object.__setattr__(self, "fields", _immutable_fields(self.fields))

    def value(self, field_name: str) -> object:
        return self.fields.get(field_name)

    def text(self, field_name: str) -> str | None:
        value = self.fields.get(field_name)
        if value is None:
            return None
        if not isinstance(value, str):
            return str(value)
        normalized = value.strip()
        return normalized or None

    def osm_identity(self) -> OsmIdentity | None:
        source = (self.text("source") or "").casefold()
        osm_type = self.text("osm_type")
        osm_id = self.text("osm_id")
        if source == "osm" and osm_type and osm_id:
            try:
                return OsmIdentity(osm_type=OsmObjectType(osm_type.casefold()), osm_id=int(osm_id))
            except (ValueError, ContractViolation):
                pass
        url = self.text("osm_url")
        if url:
            match = re.fullmatch(
                r"https?://(?:www\.)?openstreetmap\.org/(node|way|relation)/([1-9][0-9]*)/?",
                url.casefold(),
            )
            if match:
                return OsmIdentity(
                    osm_type=OsmObjectType(match.group(1)), osm_id=int(match.group(2))
                )
        return None


@dataclass(frozen=True, slots=True)
class WorkflowProtectionEvidence:
    """Field-level evidence that must be preserved by future writers."""

    noco_id: int
    protected: bool
    reasons: tuple[str, ...] = ()

    def __post_init__(self) -> None:
        object.__setattr__(self, "noco_id", _positive(self.noco_id, "noco_id"))
        if not isinstance(self.protected, bool):
            raise ContractViolation("protected must be a boolean")
        raw_reasons = tuple(islice(iter(self.reasons), _MAX_FIELDS + 1))
        if len(raw_reasons) > _MAX_FIELDS:
            raise ContractViolation("reasons exceed the bounded count")
        reasons = tuple(sorted({_text(reason, "reason", max_length=256) for reason in raw_reasons}))
        if self.protected != bool(reasons):
            raise ContractViolation("protected must match whether reasons are present")
        object.__setattr__(self, "reasons", reasons)


@dataclass(frozen=True, slots=True)
class NocoReadRequest:
    """Explicit bounds for one keyset-paginated read."""

    page_size: int = 500
    max_rows: int = 100_000
    start_after_id: int = 0

    def __post_init__(self) -> None:
        if isinstance(self.page_size, bool) or not isinstance(self.page_size, int):
            raise ContractViolation("page_size must be an integer")
        if not 1 <= self.page_size <= 5_000:
            raise ContractViolation("page_size must be within [1, 5000]")
        if isinstance(self.max_rows, bool) or not isinstance(self.max_rows, int):
            raise ContractViolation("max_rows must be an integer")
        if not 1 <= self.max_rows <= 5_000_000:
            raise ContractViolation("max_rows must be within [1, 5000000]")
        if isinstance(self.start_after_id, bool) or not isinstance(self.start_after_id, int):
            raise ContractViolation("start_after_id must be an integer")
        if self.start_after_id < 0:
            raise ContractViolation("start_after_id must be nonnegative")


@dataclass(frozen=True, slots=True)
class NocoReadPage:
    """One stable ascending-id page from a read-only repository."""

    records: tuple[NocoSourceRecord, ...]
    next_after_id: int | None

    def __post_init__(self) -> None:
        records = tuple(islice(iter(self.records), 5_001))
        if len(records) > 5_000:
            raise ContractViolation("records exceed the maximum page size")
        if any(not isinstance(record, NocoSourceRecord) for record in records):
            raise ContractViolation("records must contain NocoSourceRecord values")
        ids = [record.noco_id for record in records]
        if ids != sorted(ids) or len(ids) != len(set(ids)):
            raise ContractViolation("records must have unique ascending Noco ids")
        object.__setattr__(self, "records", records)
        if self.next_after_id is not None:
            object.__setattr__(
                self, "next_after_id", _positive(self.next_after_id, "next_after_id")
            )
            if not records or self.next_after_id != records[-1].noco_id:
                raise ContractViolation("next_after_id must equal the last record id")


@dataclass(frozen=True, slots=True)
class NocoReadAudit:
    """Secret-free evidence for one completed bounded read."""

    schema_fingerprint: str
    rows_read: int
    pages_read: int
    max_noco_id: int | None
    repository_kind: str

    def __post_init__(self) -> None:
        if not _SHA256.fullmatch(self.schema_fingerprint):
            raise ContractViolation("schema_fingerprint must be lowercase SHA-256")
        for field_name in ("rows_read", "pages_read"):
            value = getattr(self, field_name)
            if isinstance(value, bool) or not isinstance(value, int) or value < 0:
                raise ContractViolation(f"{field_name} must be nonnegative")
        if self.max_noco_id is not None:
            object.__setattr__(self, "max_noco_id", _positive(self.max_noco_id, "max_noco_id"))
        object.__setattr__(
            self, "repository_kind", _text(self.repository_kind, "repository_kind", max_length=64)
        )
