"""zuzu.record() — Define records with inferred types, backed by SQLite."""

from __future__ import annotations

import sqlite3
from pathlib import Path
from typing import Any, Callable

from zuzu.field import (
    FieldProxy, Condition, CompoundCondition,
    _DateType, _ImageType,
    Choice, Currency, Percent, Rating,
)
from zuzu.query import Query


# ── Type inference ───────────────────────────────────────────────────────

def _infer_sqlite_type(value: Any) -> str:
    """Infer SQLite column type from an example value."""
    if isinstance(value, bool):
        return "INTEGER"  # SQLite stores bools as 0/1
    if isinstance(value, int):
        return "INTEGER"
    if isinstance(value, float):
        return "REAL"
    if isinstance(value, str):
        return "TEXT"
    if isinstance(value, list):
        return "TEXT"  # Choice field — stored as text
    if isinstance(value, Choice):
        return "TEXT"  # Choice field — stored as text
    if isinstance(value, Currency):
        return "REAL"
    if isinstance(value, Percent):
        return "REAL"
    if isinstance(value, Rating):
        return "INTEGER"
    if isinstance(value, (_DateType, _ImageType)):
        return "TEXT"
    if isinstance(value, Record):
        return "TEXT"  # Foreign key — stores the linked record's ID
    return "TEXT"


def _infer_field_type(value: Any) -> str:
    """Infer a human-readable field type for the spec."""
    if isinstance(value, bool):
        return "boolean"
    if isinstance(value, int):
        return "number"
    if isinstance(value, float):
        return "number"
    if isinstance(value, str):
        return "text"
    if isinstance(value, list):
        return "choice"
    if isinstance(value, Choice):
        return "choice"
    if isinstance(value, Currency):
        return "number"
    if isinstance(value, Percent):
        return "number"
    if isinstance(value, Rating):
        return "number"
    if isinstance(value, _DateType):
        return "date"
    if isinstance(value, _ImageType):
        return "image"
    if isinstance(value, Record):
        return "link"
    return "text"


# ── Database connection (one per app) ────────────────────────────────────

_db_path: Path | None = None
_conn: sqlite3.Connection | None = None


def _get_db() -> sqlite3.Connection:
    """Get or create the SQLite connection."""
    global _conn, _db_path
    if _conn is None:
        if _db_path is None:
            _db_path = Path("./db.sqlite")
        _conn = sqlite3.connect(str(_db_path))
        _conn.row_factory = sqlite3.Row
        _conn.execute("PRAGMA journal_mode=WAL")
        _conn.execute("PRAGMA foreign_keys=ON")
    return _conn


def set_db_path(path: str | Path) -> None:
    """Set the database path. Must be called before any record operations."""
    global _db_path, _conn
    _db_path = Path(path)
    _conn = None


# ── Record registry ──────────────────────────────────────────────────────

_registry: dict[str, Record] = {}


# ── Record class ─────────────────────────────────────────────────────────

class Record:
    """A named collection of data backed by a SQLite table.

    Usage:
        clients = zuzu.record("Clients",
            name="",
            email="",
            status=["lead", "active", "churned"],
            revenue=0.0,
        )

        clients.add(name="Alice", email="alice@co.com", revenue=50000)
        clients.where(clients.revenue > 10000).sort("name")
    """

    def __init__(self, _name: str, **fields: Any) -> None:
        self._name = _name
        self._table_name = _name.lower().replace(" ", "_")

        # Separate computed fields (callables) from stored fields
        self._computed: dict[str, Callable] = {}
        stored_fields: dict[str, Any] = {}
        for k, v in fields.items():
            if callable(v) and not isinstance(v, (Choice, Currency, Percent, Rating, _DateType, _ImageType, Record)):
                self._computed[k] = v
            else:
                stored_fields[k] = v

        self._fields: dict[str, Any] = stored_fields
        self._field_types: dict[str, str] = {k: _infer_sqlite_type(v) for k, v in stored_fields.items()}
        self._field_labels: dict[str, str] = {k: _infer_field_type(v) for k, v in stored_fields.items()}

        # Choices: support both plain lists and Choice instances
        self._choices: dict[str, list[str]] = {}
        self._choice_colors: dict[str, dict[str, str]] = {}
        for k, v in stored_fields.items():
            if isinstance(v, list):
                self._choices[k] = v
            elif isinstance(v, Choice):
                self._choices[k] = v.options
                if v.colors:
                    self._choice_colors[k] = v.colors

        # Format metadata for special number types
        self._field_formats: dict[str, dict[str, Any]] = {}
        for k, v in stored_fields.items():
            if isinstance(v, Currency):
                self._field_formats[k] = {"format": "currency", "symbol": v.symbol}
            elif isinstance(v, Percent):
                self._field_formats[k] = {"format": "percent"}
            elif isinstance(v, Rating):
                self._field_formats[k] = {"format": "rating", "max": v.max}

        self._links: dict[str, Record] = {
            k: v for k, v in stored_fields.items() if isinstance(v, Record)
        }
        self._table_created = False

        # Required fields
        self._required: list[str] = []

        # Display field: first text field that's not an FK
        self._display_field: str | None = None
        for k, v in stored_fields.items():
            if k not in self._links and self._field_types.get(k) == "TEXT" and not isinstance(v, (_DateType, _ImageType)):
                self._display_field = k
                break

        # Register
        _registry[self._table_name] = self

    def display(self, field: str) -> Record:
        """Set which field represents this record visually (used in FK chips)."""
        self._display_field = field
        return self

    def require(self, *fields: str) -> Record:
        """Mark fields as required (enforced in forms).

        Usage:
            clients = zuzu.record("Clients", name="", email="").require("name", "email")
        """
        self._required = list(fields)
        return self

    # ── Computed fields ─────────────────────────────────────────────────

    def _apply_computed(self, row: dict[str, Any]) -> dict[str, Any]:
        """Apply computed fields to a row dict."""
        for name, fn in self._computed.items():
            try:
                row[name] = fn(row)
            except Exception:
                row[name] = None
        return row

    # ── Field proxy access (for operators) ───────────────────────────────

    def __getattr__(self, name: str) -> FieldProxy:
        if name.startswith("_"):
            raise AttributeError(name)
        if name in self._fields or name in self._computed:
            return FieldProxy(name)
        raise AttributeError(f"Record '{self._name}' has no field '{name}'")

    # ── Table creation ───────────────────────────────────────────────────

    def _ensure_table(self) -> None:
        """Create the SQLite table if it doesn't exist."""
        if self._table_created:
            return

        db = _get_db()
        cols = ["id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(4))) || '-' || lower(hex(randomblob(2))) || '-4' || substr(lower(hex(randomblob(2))),2) || '-' || substr('89ab',abs(random()) % 4 + 1,1) || substr(lower(hex(randomblob(2))),2) || '-' || lower(hex(randomblob(6))))"]
        for field_name, sqlite_type in self._field_types.items():
            if field_name in self._links:
                linked = self._links[field_name]
                cols.append(f"{field_name}_id {sqlite_type} REFERENCES {linked._table_name}(id)")
            else:
                cols.append(f"{field_name} {sqlite_type}")
        cols.append("created_at TEXT DEFAULT (datetime('now'))")
        cols.append("updated_at TEXT DEFAULT (datetime('now'))")

        sql = f"CREATE TABLE IF NOT EXISTS {self._table_name} ({', '.join(cols)})"
        db.execute(sql)
        db.commit()
        self._table_created = True

    # ── CRUD ─────────────────────────────────────────────────────────────

    def add(self, **data: Any) -> dict[str, Any]:
        """Add a new record. Returns the inserted row as a dict."""
        self._ensure_table()
        db = _get_db()

        # Map link fields to _id columns
        insert_data: dict[str, Any] = {}
        for k, v in data.items():
            if k in self._links:
                insert_data[f"{k}_id"] = v if isinstance(v, (int, str)) else v.get("id")
            else:
                insert_data[k] = v

        columns = ", ".join(insert_data.keys())
        placeholders = ", ".join("?" for _ in insert_data)
        values = list(insert_data.values())

        # Insert and get the generated UUID back
        row = db.execute(
            f"INSERT INTO {self._table_name} ({columns}) VALUES ({placeholders}) RETURNING *",
            values,
        ).fetchone()
        db.commit()

        result = dict(row) if row else {}
        if result and self._computed:
            self._apply_computed(result)
        return result

    def get(self, **kwargs: Any) -> dict[str, Any] | None:
        """Get a single record by field values."""
        self._ensure_table()
        db = _get_db()

        clauses = [f"{k} = ?" for k in kwargs]
        values = list(kwargs.values())

        row = db.execute(
            f"SELECT * FROM {self._table_name} WHERE {' AND '.join(clauses)} LIMIT 1",
            values,
        ).fetchone()

        if row is None:
            return None
        result = dict(row)
        if self._computed:
            self._apply_computed(result)
        return result

    def update(self, id: int | str, **data: Any) -> dict[str, Any] | None:
        """Update a record by ID."""
        self._ensure_table()
        db = _get_db()

        update_data = {**data, "updated_at": "datetime('now')"}
        sets = ", ".join(
            f"{k} = datetime('now')" if k == "updated_at" else f"{k} = ?"
            for k in update_data
        )
        values = [v for k, v in update_data.items() if k != "updated_at"]
        values.append(id)

        db.execute(f"UPDATE {self._table_name} SET {sets} WHERE id = ?", values)
        db.commit()
        return self.get(id=id)

    def remove(self, id: int | str) -> bool:
        """Remove a record by ID."""
        self._ensure_table()
        db = _get_db()
        cursor = db.execute(f"DELETE FROM {self._table_name} WHERE id = ?", [id])
        db.commit()
        return cursor.rowcount > 0

    # ── CSV import / export ─────────────────────────────────────────────

    def import_csv(self, path: str) -> int:
        """Import records from a CSV file. Returns count of imported rows."""
        import csv

        self._ensure_table()
        count = 0
        with open(path) as f:
            reader = csv.DictReader(f)
            for row in reader:
                data: dict[str, Any] = {}
                for key in row:
                    if key in self._fields:
                        val = row[key]
                        # Type coerce based on field type
                        field_type = self._field_types.get(key)
                        if field_type == "REAL":
                            try:
                                val = float(val) if val else 0.0
                            except (ValueError, TypeError):
                                val = 0.0
                        elif field_type == "INTEGER":
                            try:
                                val = int(val) if val else 0
                            except (ValueError, TypeError):
                                val = 0
                        data[key] = val
                if data:
                    self.add(**data)
                    count += 1
        return count

    def export_csv(self, path: str) -> int:
        """Export all records to CSV. Returns count of exported rows."""
        import csv

        rows = self.all()
        if not rows:
            return 0
        # Exclude timestamps and computed fields from export
        exclude = {"created_at", "updated_at"} | set(self._computed.keys())
        fieldnames = [k for k in rows[0] if k not in exclude]
        with open(path, "w", newline="") as f:
            writer = csv.DictWriter(f, fieldnames=fieldnames)
            writer.writeheader()
            writer.writerows([{k: v for k, v in r.items() if k not in exclude} for r in rows])
        return len(rows)

    # ── Querying ─────────────────────────────────────────────────────────

    def where(self, *conditions: Condition | CompoundCondition, **kwargs: Any) -> RecordQuery:
        """Filter records. Supports operator syntax and keyword syntax.

        Usage:
            clients.where(clients.revenue > 100)
            clients.where(status="active")
            clients.where((clients.revenue >= 50) & (clients.status != "churned"))
        """
        q = Query(table=self._table_name)
        for cond in conditions:
            q = q.add_condition(cond)
        if kwargs:
            q = q.add_keywords(**kwargs)
        return RecordQuery(record=self, query=q)

    def sort(self, field: str, descending: bool = False) -> RecordQuery:
        """Sort all records by a field."""
        q = Query(table=self._table_name).with_sort(field, descending)
        return RecordQuery(record=self, query=q)

    def first(self, n: int) -> RecordQuery:
        """Get the first N records."""
        q = Query(table=self._table_name).with_limit(n)
        return RecordQuery(record=self, query=q)

    def all(self) -> list[dict[str, Any]]:
        """Get all records."""
        self._ensure_table()
        db = _get_db()
        rows = db.execute(f"SELECT * FROM {self._table_name}").fetchall()
        results = [dict(r) for r in rows]
        if self._computed:
            for row in results:
                self._apply_computed(row)
        return results

    # ── Aggregates ───────────────────────────────────────────────────────

    def count(self) -> int:
        """Count all records."""
        self._ensure_table()
        db = _get_db()
        row = db.execute(f"SELECT COUNT(*) FROM {self._table_name}").fetchone()
        return row[0] if row else 0

    def total(self, field: str) -> float:
        """Sum a numeric field across all records."""
        self._ensure_table()
        db = _get_db()
        row = db.execute(f"SELECT COALESCE(SUM({field}), 0) FROM {self._table_name}").fetchone()
        return row[0] if row else 0.0

    def average(self, field: str) -> float:
        """Average a numeric field across all records."""
        self._ensure_table()
        db = _get_db()
        row = db.execute(f"SELECT COALESCE(AVG({field}), 0) FROM {self._table_name}").fetchone()
        return row[0] if row else 0.0

    # ── Spec generation ──────────────────────────────────────────────────

    def _to_spec(self) -> dict[str, Any]:
        """Generate the record spec for the renderer."""
        fields_spec: dict[str, Any] = {}
        for name in self._fields:
            field_info: dict[str, Any] = {
                "type": self._field_labels[name],
                "choices": self._choices.get(name),
                "link": self._links[name]._table_name if name in self._links else None,
            }
            # Include choice colors
            if name in self._choice_colors:
                field_info["colors"] = self._choice_colors[name]
            # Include format metadata for special number types
            if name in self._field_formats:
                field_info.update(self._field_formats[name])
            fields_spec[name] = field_info

        # Include computed fields in the spec (type: computed)
        for name in self._computed:
            fields_spec[name] = {"type": "computed"}

        return {
            "name": self._name,
            "table": self._table_name,
            "display": self._display_field,
            "required": self._required,
            "fields": fields_spec,
        }

    def __repr__(self) -> str:
        return f"Record({self._name!r}, fields={list(self._fields.keys())})"


# ── RecordQuery (chainable filtered view) ────────────────────────────────

class RecordQuery:
    """A filtered/sorted view of a Record. Chainable."""

    def __init__(self, record: Record, query: Query) -> None:
        self._record = record
        self._query = query

    def where(self, *conditions: Condition | CompoundCondition, **kwargs: Any) -> RecordQuery:
        q = self._query
        for cond in conditions:
            q = q.add_condition(cond)
        if kwargs:
            q = q.add_keywords(**kwargs)
        return RecordQuery(record=self._record, query=q)

    def sort(self, field: str, descending: bool = False) -> RecordQuery:
        return RecordQuery(record=self._record, query=self._query.with_sort(field, descending))

    def first(self, n: int) -> RecordQuery:
        return RecordQuery(record=self._record, query=self._query.with_limit(n))

    def all(self) -> list[dict[str, Any]]:
        """Execute the query and return results."""
        self._record._ensure_table()
        db = _get_db()
        sql, params = self._query.build_select()
        rows = db.execute(sql, params).fetchall()
        results = [dict(r) for r in rows]
        if self._record._computed:
            for row in results:
                self._record._apply_computed(row)
        return results

    def count(self) -> int:
        self._record._ensure_table()
        db = _get_db()
        sql, params = self._query.build_count()
        row = db.execute(sql, params).fetchone()
        return row[0] if row else 0

    def total(self, field: str) -> float:
        self._record._ensure_table()
        db = _get_db()
        sql, params = self._query.build_aggregate("SUM", field)
        row = db.execute(sql, params).fetchone()
        return row[0] if row else 0.0

    def average(self, field: str) -> float:
        self._record._ensure_table()
        db = _get_db()
        sql, params = self._query.build_aggregate("AVG", field)
        row = db.execute(sql, params).fetchone()
        return row[0] if row else 0.0

    def __repr__(self) -> str:
        sql, params = self._query.build_select()
        return f"RecordQuery({sql}, {params})"


# ── Module-level factory ─────────────────────────────────────────────────

def record(_name: str, **fields: Any) -> Record:
    """Create a new Record (data container backed by SQLite).

    Types are inferred from example values:
        "" → text
        0 or 0.0 → number
        True → boolean
        ["a", "b"] → choice
        zuzu.date → date
        other_record → link (foreign key)

    Usage:
        clients = zuzu.record("Clients",
            name="",
            email="",
            status=["lead", "active", "churned"],
            revenue=0.0,
        )
    """
    return Record(_name, **fields)
