from __future__ import annotations

import csv
import io
import re
from collections.abc import Mapping, Sequence
from dataclasses import dataclass, field
from datetime import datetime
from typing import Protocol, runtime_checkable


CAPABILITY_CONNECTORS_TABULAR_SOURCES = "connectors.tabularSources"
CAPABILITY_CONNECTORS_TABULAR_SNAPSHOT_WRITER = "connectors.tabularSnapshotWriter"


class TabularSourceError(ValueError):
    """Stable base error for provider-neutral tabular source operations."""


class TabularSourceNotFoundError(TabularSourceError):
    pass


class TabularSourceAccessError(TabularSourceError):
    pass


class TabularSourceValidationError(TabularSourceError):
    pass


def parse_tabular_csv(
    csv_text: str,
    *,
    delimiter: str = ",",
    max_rows: int = 10_000,
) -> tuple[Mapping[str, object], ...]:
    """Parse a bounded CSV document into JSON-compatible tabular rows."""

    if len(delimiter) != 1:
        raise TabularSourceValidationError("CSV delimiter must be one character.")
    try:
        reader = csv.DictReader(io.StringIO(csv_text), delimiter=delimiter)
        original_headers, normalized_headers = _csv_headers(reader.fieldnames)
        rows: list[dict[str, object]] = []
        for row in reader:
            _validate_csv_row_shape(row)
            if _csv_row_is_empty(row, original_headers):
                continue
            if len(rows) >= max_rows:
                raise TabularSourceValidationError(
                    f"CSV snapshots are limited to {max_rows:,} rows."
                )
            rows.append(_csv_row(row, original_headers, normalized_headers))
        return tuple(rows)
    except csv.Error as exc:
        raise TabularSourceValidationError(f"CSV input could not be parsed: {exc}") from exc


def _csv_headers(
    fieldnames: Sequence[str | None] | None,
) -> tuple[tuple[str, ...], tuple[str, ...]]:
    if not fieldnames:
        raise TabularSourceValidationError("CSV input requires a non-empty header row.")
    original = tuple(str(name or "") for name in fieldnames)
    normalized = tuple(
        name.strip().lstrip("\ufeff") if position == 0 else name.strip()
        for position, name in enumerate(original)
    )
    if any(not name for name in normalized):
        raise TabularSourceValidationError("CSV input requires a non-empty header row.")
    if len(set(normalized)) != len(normalized):
        raise TabularSourceValidationError("CSV column names must be unique.")
    return original, normalized


def _validate_csv_row_shape(row: Mapping[str | None, object]) -> None:
    extras = row.get(None)
    if isinstance(extras, list) and any(str(value or "").strip() for value in extras):
        raise TabularSourceValidationError(
            "A CSV row contains more values than the header defines."
        )


def _csv_row_is_empty(
    row: Mapping[str | None, str | list[str] | None],
    headers: Sequence[str],
) -> bool:
    return all(
        value is None or isinstance(value, list) or not value.strip()
        for value in (row.get(header) for header in headers)
    )


def _csv_row(
    row: Mapping[str | None, str | list[str] | None],
    original_headers: Sequence[str],
    normalized_headers: Sequence[str],
) -> dict[str, object]:
    return {
        normalized: _csv_scalar(value if isinstance(value, str) else None)
        for original, normalized in zip(
            original_headers,
            normalized_headers,
            strict=True,
        )
        for value in (row.get(original),)
    }


def _csv_scalar(value: str | None) -> object:
    if value is None:
        return None
    text = value.strip()
    if not text:
        return None
    lowered = text.casefold()
    if lowered in {"true", "false"}:
        return lowered == "true"
    if re.fullmatch(r"-?(?:0|[1-9][0-9]*)", text):
        return int(text)
    if re.fullmatch(r"-?(?:0|[1-9][0-9]*)\.[0-9]+", text):
        return float(text)
    return text


@dataclass(frozen=True, slots=True)
class TabularColumn:
    name: str
    data_type: str
    nullable: bool = True


@dataclass(frozen=True, slots=True)
class TabularSource:
    """Opaque, policy-filtered source reference exposed to consuming modules."""

    ref: str
    provider: str
    source_name: str
    name: str
    description: str | None = None
    schema: tuple[TabularColumn, ...] = ()
    schema_version: str = "1"
    fingerprint: str = ""
    row_count: int | None = None
    byte_count: int | None = None
    updated_at: datetime | None = None
    capabilities: tuple[str, ...] = ("read",)
    metadata: Mapping[str, object] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class TabularReadRequest:
    source_ref: str
    limit: int = 250
    offset: int = 0
    columns: tuple[str, ...] = ()
    expected_fingerprint: str | None = None


@dataclass(frozen=True, slots=True)
class TabularReadResult:
    source: TabularSource
    rows: tuple[Mapping[str, object], ...]
    total_rows: int
    truncated: bool


@dataclass(frozen=True, slots=True)
class TabularSnapshotInput:
    name: str
    source_name: str
    rows: tuple[Mapping[str, object], ...]
    description: str | None = None
    metadata: Mapping[str, object] = field(default_factory=dict)


@runtime_checkable
class TabularSourceProvider(Protocol):
    """Principal-aware catalogue and bounded reader for tabular sources."""

    def list_sources(
        self,
        session: object,
        principal: object,
        *,
        query: str = "",
        limit: int = 100,
    ) -> Sequence[TabularSource]:
        ...

    def get_source(
        self,
        session: object,
        principal: object,
        *,
        source_ref: str,
    ) -> TabularSource | None:
        ...

    def read_source(
        self,
        session: object,
        principal: object,
        *,
        request: TabularReadRequest,
    ) -> TabularReadResult:
        ...


@runtime_checkable
class TabularSnapshotWriter(Protocol):
    """Optional capability for importing bounded static tabular snapshots."""

    def create_snapshot(
        self,
        session: object,
        principal: object,
        *,
        snapshot: TabularSnapshotInput,
    ) -> TabularSource:
        ...


def tabular_source_provider(registry: object | None) -> TabularSourceProvider | None:
    capability = _capability(registry, CAPABILITY_CONNECTORS_TABULAR_SOURCES)
    return capability if isinstance(capability, TabularSourceProvider) else None


def tabular_snapshot_writer(registry: object | None) -> TabularSnapshotWriter | None:
    capability = _capability(registry, CAPABILITY_CONNECTORS_TABULAR_SNAPSHOT_WRITER)
    return capability if isinstance(capability, TabularSnapshotWriter) else None


def _capability(registry: object | None, name: str) -> object | None:
    if (
        registry is None
        or not hasattr(registry, "has_capability")
        or not hasattr(registry, "capability")
        or not registry.has_capability(name)
    ):
        return None
    return registry.capability(name)


__all__ = [
    "CAPABILITY_CONNECTORS_TABULAR_SNAPSHOT_WRITER",
    "CAPABILITY_CONNECTORS_TABULAR_SOURCES",
    "TabularColumn",
    "TabularReadRequest",
    "TabularReadResult",
    "TabularSnapshotInput",
    "TabularSnapshotWriter",
    "TabularSource",
    "TabularSourceAccessError",
    "TabularSourceError",
    "TabularSourceNotFoundError",
    "TabularSourceProvider",
    "TabularSourceValidationError",
    "parse_tabular_csv",
    "tabular_snapshot_writer",
    "tabular_source_provider",
]
