"""Schema DDL generation — pure SQL string builders, no I/O.

Provides :class:`SchemaBuilder` for generating ``CREATE TABLE``, ``CREATE INDEX``,
``DROP TABLE``, and ``ALTER TABLE`` statements from structured table/index/column
definitions.  All methods are pure functions that return SQL strings — they never
connect to a database.
"""

from __future__ import annotations

from dataclasses import dataclass, field


# ---------------------------------------------------------------------------
# Dataclass definitions — immutable value objects describing schema elements
# ---------------------------------------------------------------------------


@dataclass(frozen=True)
class ColumnDef:
    """Definition of a single table column.

    Args:
        name: Column name.
        col_type: SQL type string, e.g. ``"SERIAL"``, ``"UUID"``, ``"TEXT"``,
            ``"INTEGER"``, ``"BIGINT"``, ``"BOOLEAN"``, ``"TIMESTAMPTZ"``,
            ``"JSONB"``, ``"VARCHAR(255)"``.
        nullable: If ``False``, emits ``NOT NULL``.
        default: Raw SQL expression for ``DEFAULT``, e.g. ``"NOW()"``,
            ``"gen_random_uuid()"``, ``"'{}'::jsonb"``, ``"TRUE"``.
            Omitted if ``None``.
        primary_key: If ``True``, emits ``PRIMARY KEY`` on this column.
    """

    name: str
    col_type: str
    nullable: bool = True
    default: str | None = None
    primary_key: bool = False


@dataclass(frozen=True)
class IndexDef:
    """Definition of a table index.

    Args:
        columns: Columns the index covers.
        unique: If ``True``, creates a ``UNIQUE INDEX``.
        using: Index method — ``"btree"`` (default), ``"gin"``, ``"gist"``,
            ``"bloom"``, etc.
        name: Explicit index name.  Auto-generated from ``columns`` and
            ``table`` name when ``None``.
        with_params: Optional ``WITH`` clause parameters, e.g.
            ``{"length": 80}`` for a bloom index signature length.
            Omitted when ``None`` or empty.
    """

    columns: list[str]
    unique: bool = False
    using: str = "btree"
    name: str | None = None
    with_params: dict[str, object] | None = None


@dataclass(frozen=True)
class ForeignKeyDef:
    """Definition of a foreign key constraint.

    Args:
        column: Local column name.
        ref_table: Referenced table name.
        ref_column: Referenced column (default ``"id"``).
        on_delete: Referential action on delete (``"CASCADE"``, ``"SET NULL"``,
            ``"RESTRICT"``, etc.).
    """

    column: str
    ref_table: str
    ref_column: str = "id"
    on_delete: str = "CASCADE"


@dataclass(frozen=True)
class TableDef:
    """Complete description of a database table.

    Args:
        name: Table name.
        columns: Column definitions in order.
        indexes: Indexes to create on the table (separate ``CREATE INDEX``
            statements — not part of ``CREATE TABLE``).
        foreign_keys: Foreign key constraints.
        if_not_exists: When ``True`` (default), emits ``IF NOT EXISTS``.
    """

    name: str
    columns: list[ColumnDef]
    indexes: list[IndexDef] = field(default_factory=list)
    foreign_keys: list[ForeignKeyDef] = field(default_factory=list)
    if_not_exists: bool = True


# ---------------------------------------------------------------------------
# SchemaBuilder — pure SQL generation functions
# ---------------------------------------------------------------------------


class SchemaBuilder:
    """Generate DDL SQL strings from structured schema definitions.

    All methods are ``@staticmethod`` and return SQL strings only — they
    perform no I/O and never connect to a database.  Use
    :meth:`PostgresClient.execute_raw` to execute the generated SQL.

    Usage::

        from db_core.schema import SchemaBuilder, TableDef, ColumnDef, IndexDef

        table = TableDef(
            name="users",
            columns=[
                ColumnDef("id", "SERIAL", primary_key=True),
                ColumnDef("email", "VARCHAR(255)", nullable=False),
                ColumnDef("created_at", "TIMESTAMPTZ", default="NOW()"),
            ],
            indexes=[IndexDef(["email"], unique=True)],
        )
        sql = SchemaBuilder.create_table(table)
        # "CREATE TABLE IF NOT EXISTS users (\\n  id SERIAL PRIMARY KEY, ...)"
    """

    # ------------------------------------------------------------------
    # Table-level DDL
    # ------------------------------------------------------------------

    @staticmethod
    def create_table(table: TableDef) -> str:
        """Generate ``CREATE TABLE ...`` SQL from a :class:`TableDef`."""
        lines: list[str] = []

        for col in table.columns:
            parts = [col.name, col.col_type]
            if col.primary_key:
                parts.append("PRIMARY KEY")
            if not col.nullable:
                parts.append("NOT NULL")
            if col.default is not None:
                parts.append(f"DEFAULT {col.default}")
            lines.append(" ".join(parts))

        # Foreign keys as inline constraints
        for fk in table.foreign_keys:
            constraint_name = f"fk_{table.name}_{fk.column}_{fk.ref_table}"
            lines.append(
                f"CONSTRAINT {constraint_name} "
                f"FOREIGN KEY ({fk.column}) "
                f"REFERENCES {fk.ref_table}({fk.ref_column})"
                f" ON DELETE {fk.on_delete}"
            )

        if_not_exists = " IF NOT EXISTS" if table.if_not_exists else ""
        sql = f"CREATE TABLE{if_not_exists} {table.name} (\n  "
        sql += ",\n  ".join(lines)
        sql += "\n)"
        return sql

    @staticmethod
    def drop_table(table_name: str, if_exists: bool = True) -> str:
        """Generate ``DROP TABLE ...`` SQL."""
        ie = " IF EXISTS" if if_exists else ""
        return f"DROP TABLE{ie} {table_name}"

    # ------------------------------------------------------------------
    # Index DDL
    # ------------------------------------------------------------------

    @staticmethod
    def create_index(table_name: str, index: IndexDef) -> str:
        """Generate ``CREATE INDEX ...`` SQL from an :class:`IndexDef`."""
        if index.name is not None:
            name = index.name
        else:
            prefix = "uq" if index.unique else "ix"
            cols_part = "_".join(index.columns)
            name = f"{prefix}_{table_name}_{cols_part}"

        unique = "UNIQUE " if index.unique else ""
        cols = ", ".join(index.columns)
        using = f" USING {index.using}" if index.using != "btree" else ""

        with_clause = ""
        if index.with_params:
            params = ", ".join(
                f"{k} = {v}" if not isinstance(v, str) else f"{k} = '{v}'"
                for k, v in index.with_params.items()
            )
            with_clause = f" WITH ({params})"

        return f"CREATE {unique}INDEX IF NOT EXISTS {name} ON {table_name}{using} ({cols}){with_clause}"

    @staticmethod
    def drop_index(index_name: str, if_exists: bool = True) -> str:
        """Generate ``DROP INDEX ...`` SQL."""
        ie = " IF EXISTS" if if_exists else ""
        return f"DROP INDEX{ie} {index_name}"

    # ------------------------------------------------------------------
    # Column-level ALTER
    # ------------------------------------------------------------------

    @staticmethod
    def add_column(table_name: str, col: ColumnDef) -> str:
        """Generate ``ALTER TABLE ... ADD COLUMN ...`` SQL."""
        parts = [f"ADD COLUMN {col.name}", col.col_type]
        if not col.nullable:
            parts.append("NOT NULL")
        if col.default is not None:
            parts.append(f"DEFAULT {col.default}")
        return f"ALTER TABLE {table_name} {' '.join(parts)}"

    @staticmethod
    def drop_column(table_name: str, col_name: str, if_exists: bool = True) -> str:
        """Generate ``ALTER TABLE ... DROP COLUMN ...`` SQL."""
        ie = " IF EXISTS" if if_exists else ""
        return f"ALTER TABLE {table_name} DROP COLUMN{ie} {col_name}"

    @staticmethod
    def rename_table(old_name: str, new_name: str) -> str:
        """Generate ``ALTER TABLE ... RENAME TO ...`` SQL."""
        return f"ALTER TABLE {old_name} RENAME TO {new_name}"
