"""
SAYTHON Framework - ORM
Zero-config SQLite by default. Switch to PostgreSQL/MySQL via DATABASE_URL.

Usage:
    from saython.orm import Model, Field

    class Book(Model):
        title  = Field(str,  required=True)
        author = Field(str,  required=True)
        year   = Field(int,  default=2024)
        active = Field(bool, default=True)

    # Table created automatically on first use.
    book = Book.create(title="Dune", author="Frank Herbert", year=1965)
    book = Book.get(1)
    books = Book.all()
    books = Book.filter(author="Frank Herbert")
    book.title = "Dune Messiah"
    book.save()
    book.delete()
"""
import sqlite3
import threading
from datetime import datetime, date
from typing import Any, Optional, Type, TypeVar, List, Dict

T = TypeVar("T", bound="Model")

# ------------------------------------------------------------------ #
#  Field descriptor
# ------------------------------------------------------------------ #
_PYTHON_TO_SQL = {
    str:      "TEXT",
    int:      "INTEGER",
    float:    "REAL",
    bool:     "INTEGER",   # SQLite stores bools as 0/1
    bytes:    "BLOB",
    datetime: "TEXT",
    date:     "TEXT",
}


class Field:
    """
    Declares a column on a Model.

    Args:
        type_       : Python type (str, int, float, bool, bytes, datetime, date)
        required    : If True, value must be provided (NOT NULL)
        default     : Default value or callable (e.g. datetime.utcnow)
        unique      : Add UNIQUE constraint
        primary_key : Mark as PRIMARY KEY (mostly internal)

    Example:
        name  = Field(str, required=True)
        score = Field(float, default=0.0)
        ts    = Field(datetime, default=datetime.utcnow)
    """

    _creation_counter = 0   # keeps column ordering stable

    def __init__(
        self,
        type_: type = str,
        *,
        required: bool = False,
        default: Any = None,
        unique: bool = False,
        primary_key: bool = False,
    ):
        self.type_ = type_
        self.required = required
        self.default = default
        self.unique = unique
        self.primary_key = primary_key

        Field._creation_counter += 1
        self._order = Field._creation_counter

    def sql_type(self) -> str:
        return _PYTHON_TO_SQL.get(self.type_, "TEXT")

    def column_def(self, name: str) -> str:
        parts = [name, self.sql_type()]
        if self.primary_key:
            parts.append("PRIMARY KEY AUTOINCREMENT")
        else:
            if self.required:
                parts.append("NOT NULL")
            if self.unique:
                parts.append("UNIQUE")
        return " ".join(parts)

    def coerce(self, value: Any) -> Any:
        """Cast a raw DB value to the Python type."""
        if value is None:
            return None
        if self.type_ == bool:
            return bool(int(value))
        if self.type_ == datetime and isinstance(value, str):
            try:
                return datetime.fromisoformat(value)
            except ValueError:
                return value
        if self.type_ == date and isinstance(value, str):
            try:
                return date.fromisoformat(value)
            except ValueError:
                return value
        try:
            return self.type_(value)
        except (TypeError, ValueError):
            return value


# ------------------------------------------------------------------ #
#  Database connection pool (thread-local for SQLite)
# ------------------------------------------------------------------ #
_local = threading.local()
_db_url: str = "saython.db"          # default SQLite file
_engine: str = "sqlite"


def configure_db(url: str):
    """
    Set the database URL before the app starts.

    Examples:
        configure_db("myapp.db")                          # SQLite file
        configure_db("sqlite:///:memory:")                # in-memory SQLite
        configure_db("postgresql://user:pass@host/db")    # PostgreSQL (needs psycopg2)
        configure_db("mysql://user:pass@host/db")         # MySQL (needs mysql-connector-python)
    """
    global _db_url, _engine
    _db_url = url
    if url.startswith("postgresql") or url.startswith("postgres"):
        _engine = "postgresql"
    elif url.startswith("mysql"):
        _engine = "mysql"
    else:
        _engine = "sqlite"


def _get_connection():
    """Return a thread-local DB connection."""
    conn = getattr(_local, "connection", None)
    if conn is None:
        conn = _create_connection()
        _local.connection = conn
    return conn


def _create_connection():
    if _engine == "sqlite":
        db_path = _db_url.replace("sqlite:///", "").replace("sqlite://", "")
        conn = sqlite3.connect(db_path, check_same_thread=False)
        conn.row_factory = sqlite3.Row
        conn.execute("PRAGMA journal_mode=WAL")
        conn.execute("PRAGMA foreign_keys=ON")
        return conn
    elif _engine == "postgresql":
        try:
            import psycopg2
            import psycopg2.extras
            conn = psycopg2.connect(_db_url)
            return conn
        except ImportError:
            raise ImportError("Install psycopg2: pip install psycopg2-binary")
    elif _engine == "mysql":
        try:
            import mysql.connector
            # parse URL: mysql://user:pass@host/db
            from urllib.parse import urlparse
            p = urlparse(_db_url)
            conn = mysql.connector.connect(
                host=p.hostname, user=p.username,
                password=p.password, database=p.path.lstrip("/")
            )
            return conn
        except ImportError:
            raise ImportError("Install mysql-connector-python: pip install mysql-connector-python")
    raise ValueError(f"Unsupported DB engine: {_engine}")


def _execute(sql: str, params: tuple = (), fetch: str = None):
    conn = _get_connection()
    cur = conn.cursor()
    try:
        cur.execute(sql, params)
        conn.commit()
        if fetch == "one":
            return cur.fetchone()
        if fetch == "all":
            return cur.fetchall()
        return cur
    except Exception:
        conn.rollback()
        raise


# ------------------------------------------------------------------ #
#  ModelMeta – collects Field declarations
# ------------------------------------------------------------------ #
class ModelMeta(type):
    def __new__(mcs, name, bases, namespace):
        fields: Dict[str, Field] = {}

        # Inherit fields from parent models
        for base in bases:
            if hasattr(base, "_fields"):
                fields.update(base._fields)

        # Collect Field objects defined in this class
        for attr, value in list(namespace.items()):
            if isinstance(value, Field):
                fields[attr] = value
                namespace.pop(attr)   # remove from class dict (use __getattr__)

        namespace["_fields"] = fields
        # Table name: snake_case of class name (pluralised)
        if "Meta" in namespace and hasattr(namespace["Meta"], "table_name"):
            namespace["_table"] = namespace["Meta"].table_name
        else:
            raw = name.lower()
            namespace["_table"] = raw + ("s" if not raw.endswith("s") else "")

        cls = super().__new__(mcs, name, bases, namespace)
        return cls


# ------------------------------------------------------------------ #
#  Model base class
# ------------------------------------------------------------------ #
class Model(metaclass=ModelMeta):
    """
    Base class for all database models.

    Subclass this and declare fields:

        class Post(Model):
            title   = Field(str, required=True)
            content = Field(str, required=True)
            views   = Field(int, default=0)

    The table is created automatically.  Every model gets an `id`
    (INTEGER PRIMARY KEY AUTOINCREMENT) and `created_at` / `updated_at`.
    """

    # Built-in fields added to EVERY model
    _builtin_fields: Dict[str, Field] = {
        "id":         Field(int, primary_key=True),
        "created_at": Field(datetime),
        "updated_at": Field(datetime),
    }

    def __init__(self, **kwargs):
        # id / timestamps – set by DB or ORM
        object.__setattr__(self, "_data", {"id": None, "created_at": None, "updated_at": None})
        object.__setattr__(self, "_dirty", set())

        all_fields = {**self._builtin_fields, **self.__class__._fields}
        for fname, fobj in all_fields.items():
            if fname in kwargs:
                self._data[fname] = fobj.coerce(kwargs[fname])
            elif fname not in self._data:
                # Apply default
                default = fobj.default
                if callable(default):
                    self._data[fname] = default()
                else:
                    self._data[fname] = default

    def __getattr__(self, name):
        data = object.__getattribute__(self, "_data")
        if name in data:
            return data[name]
        raise AttributeError(f"'{type(self).__name__}' has no attribute '{name}'")

    def __setattr__(self, name, value):
        all_fields = {**self._builtin_fields, **self.__class__._fields}
        if name in all_fields:
            self._data[name] = all_fields[name].coerce(value)
            self._dirty.add(name)
        else:
            object.__setattr__(self, name, value)

    # ------------------------------------------------------------------ #
    #  Table management
    # ------------------------------------------------------------------ #
    @classmethod
    def _create_table(cls):
        all_fields = {**cls._builtin_fields, **cls._fields}
        # Sort: id first, then user fields, then timestamps
        ordered = []
        ordered.append(cls._builtin_fields["id"].column_def("id"))
        for fname, fobj in sorted(cls._fields.items(), key=lambda x: x[1]._order):
            ordered.append(fobj.column_def(fname))
        ordered.append("created_at TEXT")
        ordered.append("updated_at TEXT")

        sql = f"CREATE TABLE IF NOT EXISTS {cls._table} ({', '.join(ordered)})"
        _execute(sql)

    # ------------------------------------------------------------------ #
    #  CRUD class methods
    # ------------------------------------------------------------------ #
    @classmethod
    def create(cls: Type[T], **kwargs) -> T:
        """Insert a new row and return the instance."""
        cls._create_table()
        now = datetime.utcnow().isoformat()
        kwargs.setdefault("created_at", now)
        kwargs.setdefault("updated_at", now)

        # Apply field defaults
        for fname, fobj in cls._fields.items():
            if fname not in kwargs:
                default = fobj.default
                if callable(default):
                    kwargs[fname] = default()
                else:
                    kwargs[fname] = default

        cols = [k for k in kwargs if k != "id"]
        placeholders = ", ".join("?" for _ in cols)
        col_names = ", ".join(cols)
        values = tuple(
            v.isoformat() if isinstance(v, (datetime, date)) else (1 if v is True else 0 if v is False else v)
            for v in (kwargs[c] for c in cols)
        )

        sql = f"INSERT INTO {cls._table} ({col_names}) VALUES ({placeholders})"
        cur = _execute(sql, values)
        kwargs["id"] = cur.lastrowid
        return cls(**kwargs)

    @classmethod
    def get(cls: Type[T], id: int) -> T:
        """Fetch a single row by primary key. Raises NotFound if missing."""
        from saython.exceptions import NotFound
        cls._create_table()
        row = _execute(f"SELECT * FROM {cls._table} WHERE id = ?", (id,), fetch="one")
        if row is None:
            raise NotFound(f"{cls.__name__} with id={id} not found")
        return cls._from_row(row)

    @classmethod
    def get_or_none(cls: Type[T], id: int) -> Optional[T]:
        """Fetch by pk, return None if not found."""
        try:
            return cls.get(id)
        except Exception:
            return None

    @classmethod
    def all(cls: Type[T]) -> List[T]:
        """Fetch all rows."""
        cls._create_table()
        rows = _execute(f"SELECT * FROM {cls._table}", fetch="all")
        return [cls._from_row(r) for r in rows]

    @classmethod
    def all_as_dicts(cls: Type[T], exclude: list = None) -> list:
        """Fetch all rows as a list of dicts (ready for JSON responses)."""
        return [item.to_dict(exclude=exclude) for item in cls.all()]

    @classmethod
    def filter(cls: Type[T], **kwargs) -> List[T]:
        """
        Simple equality filter.
            Book.filter(author="Tolkien")
            Book.filter(author="Tolkien", year=1954)
        """
        cls._create_table()
        if not kwargs:
            return cls.all()
        conditions = " AND ".join(f"{k} = ?" for k in kwargs)
        values = tuple(
            v.isoformat() if isinstance(v, (datetime, date)) else (1 if v is True else 0 if v is False else v)
            for v in kwargs.values()
        )
        rows = _execute(f"SELECT * FROM {cls._table} WHERE {conditions}", values, fetch="all")
        return [cls._from_row(r) for r in rows]

    @classmethod
    def first(cls: Type[T], **kwargs) -> Optional[T]:
        """Return first matching row or None."""
        results = cls.filter(**kwargs) if kwargs else cls.all()
        return results[0] if results else None

    @classmethod
    def count(cls) -> int:
        """Count total rows."""
        cls._create_table()
        row = _execute(f"SELECT COUNT(*) FROM {cls._table}", fetch="one")
        return row[0] if row else 0

    @classmethod
    def exists(cls, **kwargs) -> bool:
        """Check if any row matches the filter."""
        return bool(cls.filter(**kwargs))

    @classmethod
    def paginate(cls: Type[T], page: int = 1, limit: int = 20) -> dict:
        """
        Paginated fetch.
        Returns: {"items": [...], "page": 1, "limit": 20, "total": 100}
        """
        cls._create_table()
        offset = (page - 1) * limit
        rows = _execute(
            f"SELECT * FROM {cls._table} LIMIT ? OFFSET ?", (limit, offset), fetch="all"
        )
        total = cls.count()
        return {
            "items": [cls._from_row(r) for r in rows],
            "page": page,
            "limit": limit,
            "total": total,
        }

    def save(self) -> "Model":
        """Update the row in the database."""
        self.updated_at = datetime.utcnow().isoformat()
        all_fields = {**self._builtin_fields, **self.__class__._fields}
        cols = [k for k in all_fields if k != "id"]
        set_clause = ", ".join(f"{c} = ?" for c in cols)
        values = tuple(
            v.isoformat() if isinstance(v, (datetime, date)) else (1 if v is True else 0 if v is False else v)
            for v in (self._data.get(c) for c in cols)
        )
        _execute(
            f"UPDATE {self.__class__._table} SET {set_clause} WHERE id = ?",
            values + (self.id,)
        )
        self._dirty.clear()
        return self

    def delete(self):
        """Delete this row from the database."""
        _execute(f"DELETE FROM {self.__class__._table} WHERE id = ?", (self.id,))
        self._data["id"] = None

    # ------------------------------------------------------------------ #
    #  Serialisation
    # ------------------------------------------------------------------ #
    def to_dict(self, exclude: list = None) -> dict:
        """Convert to plain dict (for JSON responses)."""
        exclude = exclude or []
        result = {}
        all_fields = {**self._builtin_fields, **self.__class__._fields}
        for fname in all_fields:
            if fname in exclude:
                continue
            val = self._data.get(fname)
            if isinstance(val, (datetime, date)):
                val = val.isoformat()
            elif isinstance(val, bool):
                pass  # keep as bool
            result[fname] = val
        return result

    @classmethod
    def _from_row(cls: Type[T], row) -> T:
        """Build an instance from a DB row (sqlite3.Row or tuple)."""
        try:
            data = dict(row)
        except TypeError:
            # sqlite3.Row -> dict
            data = {row.keys()[i]: row[i] for i in range(len(row.keys()))}
        return cls(**data)

    def __repr__(self):
        return f"<{type(self).__name__} id={self.id}>"
