"""Data connectors for CSV, JSON, and in-memory dict ingest.

Each connector reads data into the universal list[dict] format,
optionally validates against a SchemaSpec, and returns a Dataset.
"""

from __future__ import annotations

import csv
import io
import json
import time
from dataclasses import dataclass, field

from orxaq.fabric.schema import SchemaSpec


@dataclass(frozen=True)
class Dataset:
    """A dataset loaded by a connector."""

    data: tuple[dict, ...]  # Immutable tuple of row dicts
    schema: SchemaSpec | None = None
    source: str = "memory"
    row_count: int = 0
    ingested_at: str = ""
    connector_type: str = "dict"
    metadata: dict = field(default_factory=dict)

    @property
    def columns(self) -> list[str]:
        """Column names from schema or first row."""
        if self.schema:
            return [c.name for c in self.schema.columns]
        if self.data:
            return list(self.data[0].keys())
        return []

    def to_list(self) -> list[dict]:
        """Return data as a mutable list[dict]."""
        return [dict(row) for row in self.data]


def _iso_now() -> str:
    """Return current UTC time as ISO string."""
    return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())


def _cast_row(row: dict, schema: SchemaSpec) -> dict:
    """Cast row values to schema types."""
    result: dict[str, object] = {}
    col_map = {c.name: c for c in schema.columns}
    for key, value in row.items():
        col = col_map.get(key)
        if col and value is not None and value != "":
            try:
                if col.dtype == "float":
                    result[key] = float(value)
                elif col.dtype == "int":
                    result[key] = int(value)
                elif col.dtype == "bool":
                    result[key] = str(value).lower() in ("true", "1", "yes")
                else:
                    result[key] = str(value)
            except (ValueError, TypeError):
                result[key] = value
        elif value == "" and col and col.nullable:
            result[key] = None
        else:
            result[key] = value
    return result


class CSVConnector:
    """Read CSV files into Dataset."""

    def read(self, filepath: str, schema: SchemaSpec | None = None) -> Dataset:
        """Read a CSV file and return a Dataset."""
        with open(filepath, newline="") as f:
            content = f.read()

        # Auto-detect delimiter
        sniffer = csv.Sniffer()
        try:
            dialect = sniffer.sniff(content[:2048])
        except csv.Error:
            dialect = csv.excel  # type: ignore[assignment]

        reader = csv.DictReader(io.StringIO(content), dialect=dialect)
        rows = list(reader)

        if schema:
            rows = [_cast_row(row, schema) for row in rows]

        return Dataset(
            data=tuple(rows),
            schema=schema,
            source=filepath,
            row_count=len(rows),
            ingested_at=_iso_now(),
            connector_type="csv",
        )


class JSONConnector:
    """Read JSON files (list-of-dicts) into Dataset."""

    def read(self, filepath: str, schema: SchemaSpec | None = None) -> Dataset:
        """Read a JSON file and return a Dataset."""
        with open(filepath) as f:
            raw = json.load(f)

        if not isinstance(raw, list):
            msg = "JSON file must contain a list of objects"
            raise ValueError(msg)

        rows = raw
        if schema:
            rows = [_cast_row(row, schema) for row in rows]

        return Dataset(
            data=tuple(rows),
            schema=schema,
            source=filepath,
            row_count=len(rows),
            ingested_at=_iso_now(),
            connector_type="json",
        )


class DictConnector:
    """Wrap in-memory list[dict] into Dataset."""

    def read(self, data: list[dict], schema: SchemaSpec | None = None) -> Dataset:
        """Wrap data into a Dataset."""
        return Dataset(
            data=tuple(data),
            schema=schema,
            source="memory",
            row_count=len(data),
            ingested_at=_iso_now(),
            connector_type="dict",
        )
