from __future__ import annotations

from dataclasses import dataclass, field

from sqlalchemy import MetaData, bindparam, inspect, text
from sqlalchemy.engine import Engine

HIDDEN_PRODUCT_CLIENTS = (
    "Bird Grits",
    "Chaff",
    "Hay & Straw",
    "Hunter Premium Produce",
    "Straight Grain",
    "Uncategorized",
    "Uncategorised",
)


TENANT_TABLES = {
    "client_users": None,
    "client_feature_access": None,
    "client_user_module_permissions": None,
    "client_access_audit_events": None,
    "raw_materials": None,
    "raw_material_price_versions": None,
    "mixes": None,
    "mix_ingredients": None,
    "product_ingredients": None,
    "mix_calculator_sessions": None,
    "mix_calculator_session_lines": None,
    "products": None,
    "product_cost_items": None,
    "product_cost_base_inputs": None,
    "product_cost_process_inputs": None,
    "product_cost_client_inputs": None,
    "product_cost_bag_inputs": None,
    "product_cost_freight_inputs": None,
    "scenarios": None,
    "costing_results": None,
    "editor_change_events": None,
    "process_cost_rules": None,
    "packaging_cost_rules": None,
    "freight_cost_rules": None,
    "throughput_products": None,
    "production_throughput_entries": None,
    # B2B ordering portal
    "product_categories": None,
    "catalogue_products": None,
    "customer_product_visibility": None,
    "price_lists": None,
    "price_list_items": None,
    "customer_price_assignments": None,
    "customer_product_prices": None,
    "price_tiers": None,
    "orders": None,
    "order_lines": None,
    "order_status_history": None,
    "order_attachments": None,
    "notification_settings": None,
    "xero_sync_log": None,
}


@dataclass(frozen=True)
class MigrationReport:
    created_tables: tuple[str, ...] = ()
    added_columns: tuple[str, ...] = ()
    synced_tenant_rows: dict[str, int] = field(default_factory=dict)
    resynced_sequences: tuple[str, ...] = ()

    def has_changes(self) -> bool:
        return bool(
            self.created_tables
            or self.added_columns
            or self.synced_tenant_rows
            or self.resynced_sequences
        )

    def summary(self) -> str:
        parts: list[str] = []
        if self.created_tables:
            parts.append(f"created tables: {', '.join(self.created_tables)}")
        if self.added_columns:
            parts.append(f"patched columns: {', '.join(self.added_columns)}")
        if self.synced_tenant_rows:
            counts = ", ".join(f"{table}={count}" for table, count in sorted(self.synced_tenant_rows.items()))
            parts.append(f"synced tenant rows: {counts}")
        if self.resynced_sequences:
            parts.append(f"resynced sequences: {', '.join(self.resynced_sequences)}")
        return "; ".join(parts) if parts else "schema already up to date"


def _has_column(engine: Engine, table_name: str, column_name: str) -> bool:
    inspector = inspect(engine)
    try:
        columns = inspector.get_columns(table_name)
    except Exception:
        return False
    return any(column["name"] == column_name for column in columns)


def _add_tenant_column(engine: Engine, table_name: str) -> None:
    if _has_column(engine, table_name, "tenant_id"):
        return
    with engine.begin() as connection:
        connection.execute(text(f"ALTER TABLE {table_name} ADD COLUMN tenant_id VARCHAR(64)"))


def _table_exists(engine: Engine, table_name: str) -> bool:
    return inspect(engine).has_table(table_name)


def ensure_metadata_tables(engine: Engine, metadata: MetaData) -> tuple[str, ...]:
    missing_tables = tuple(table.name for table in metadata.sorted_tables if not _table_exists(engine, table.name))
    if missing_tables:
        metadata.create_all(bind=engine)
    return missing_tables


def ensure_tenant_columns(engine: Engine) -> tuple[str, ...]:
    added_columns: list[str] = []
    for table_name in TENANT_TABLES:
        if _table_exists(engine, table_name):
            if not _has_column(engine, table_name, "tenant_id"):
                _add_tenant_column(engine, table_name)
                added_columns.append(f"{table_name}.tenant_id")
    return tuple(added_columns)


# Ad-hoc column additions for tables that pre-existed before a column was
# introduced on the model. Each entry is (table, column, DDL fragment).
_LEGACY_COLUMN_PATCHES: tuple[tuple[str, str, str], ...] = (
    ("users", "password_hash", "VARCHAR(255)"),
    ("products", "visible", "BOOLEAN NOT NULL DEFAULT TRUE"),
    ("throughput_products", "is_stock_item", "BOOLEAN NOT NULL DEFAULT TRUE"),
    ("throughput_products", "client_name", "VARCHAR(255)"),
    ("production_throughput_entries", "for_order", "BOOLEAN NOT NULL DEFAULT FALSE"),
    ("production_throughput_entries", "for_stock", "BOOLEAN NOT NULL DEFAULT FALSE"),
    ("production_throughput_entries", "job_number", "VARCHAR(64)"),
    ("production_throughput_entries", "stock_quantity", "FLOAT"),
    ("raw_materials", "rounding_decimals", "INTEGER NOT NULL DEFAULT 2"),
    ("mix_calculator_session_lines", "rounding_decimals", "INTEGER NOT NULL DEFAULT 2"),
)


def ensure_legacy_columns(engine: Engine) -> tuple[str, ...]:
    added: list[str] = []
    for table_name, column_name, ddl in _LEGACY_COLUMN_PATCHES:
        if not _table_exists(engine, table_name):
            continue
        if _has_column(engine, table_name, column_name):
            continue
        with engine.begin() as connection:
            connection.execute(
                text(f"ALTER TABLE {table_name} ADD COLUMN {column_name} {ddl}")
            )
        added.append(f"{table_name}.{column_name}")
    return tuple(added)


def sync_tenant_ids(engine: Engine) -> dict[str, int]:
    existing_tables = set(inspect(engine).get_table_names())

    if "client_accounts" not in existing_tables:
        return {}

    synced_rows: dict[str, int] = {}
    with engine.begin() as connection:
        default_tenant = connection.execute(
            text("SELECT tenant_id FROM client_accounts ORDER BY id LIMIT 1")
        ).scalar_one_or_none()
        if not default_tenant:
            return {}

        statements = [
            (
                "client_users",
                text(
                """
                UPDATE client_users
                SET tenant_id = (
                    SELECT client_accounts.tenant_id
                    FROM client_accounts
                    WHERE client_accounts.id = client_users.client_account_id
                )
                WHERE tenant_id IS NULL OR tenant_id = '' OR tenant_id = 'default'
                """
                ),
            ),
            (
                "client_feature_access",
                text(
                """
                UPDATE client_feature_access
                SET tenant_id = (
                    SELECT client_accounts.tenant_id
                    FROM client_accounts
                    WHERE client_accounts.id = client_feature_access.client_account_id
                )
                WHERE tenant_id IS NULL OR tenant_id = '' OR tenant_id = 'default'
                """
                ),
            ),
            (
                "client_user_module_permissions",
                text(
                """
                UPDATE client_user_module_permissions
                SET tenant_id = (
                    SELECT client_users.tenant_id
                    FROM client_users
                    WHERE client_users.id = client_user_module_permissions.client_user_id
                )
                WHERE tenant_id IS NULL OR tenant_id = '' OR tenant_id = 'default'
                """
                ),
            ),
            (
                "client_access_audit_events",
                text(
                """
                UPDATE client_access_audit_events
                SET tenant_id = (
                    SELECT client_accounts.tenant_id
                    FROM client_accounts
                    WHERE client_accounts.id = client_access_audit_events.client_account_id
                )
                WHERE tenant_id IS NULL OR tenant_id = '' OR tenant_id = 'default'
                """
                ),
            ),
            (
                "raw_materials",
                text(
                """
                UPDATE raw_materials
                SET tenant_id = :default_tenant
                WHERE tenant_id IS NULL OR tenant_id = '' OR tenant_id = 'default'
                """
                ),
            ),
            (
                "raw_material_price_versions",
                text(
                """
                UPDATE raw_material_price_versions
                SET tenant_id = (
                    SELECT raw_materials.tenant_id
                    FROM raw_materials
                    WHERE raw_materials.id = raw_material_price_versions.raw_material_id
                )
                WHERE tenant_id IS NULL OR tenant_id = '' OR tenant_id = 'default'
                """
                ),
            ),
            (
                "mixes",
                text(
                """
                UPDATE mixes
                SET tenant_id = COALESCE(
                    (
                        SELECT client_accounts.tenant_id
                        FROM client_accounts
                        WHERE client_accounts.name = mixes.client_name
                    ),
                    :default_tenant
                )
                WHERE tenant_id IS NULL OR tenant_id = '' OR tenant_id = 'default'
                """
                ),
            ),
            (
                "mix_ingredients",
                text(
                """
                UPDATE mix_ingredients
                SET tenant_id = (
                    SELECT mixes.tenant_id
                    FROM mixes
                    WHERE mixes.id = mix_ingredients.mix_id
                )
                WHERE tenant_id IS NULL OR tenant_id = '' OR tenant_id = 'default'
                """
                ),
            ),
            (
                "product_ingredients",
                text(
                """
                UPDATE product_ingredients
                SET tenant_id = (
                    SELECT products.tenant_id
                    FROM products
                    WHERE products.id = product_ingredients.product_id
                )
                WHERE tenant_id IS NULL OR tenant_id = '' OR tenant_id = 'default'
                """
                ),
            ),
            (
                "products",
                text(
                """
                UPDATE products
                SET tenant_id = COALESCE(
                    (
                        SELECT client_accounts.tenant_id
                        FROM client_accounts
                        WHERE client_accounts.name = products.client_name
                    ),
                    (
                        SELECT mixes.tenant_id
                        FROM mixes
                        WHERE mixes.id = products.mix_id
                    ),
                    :default_tenant
                )
                WHERE tenant_id IS NULL OR tenant_id = '' OR tenant_id = 'default'
                """
                ),
            ),
            (
                "mix_calculator_sessions",
                text(
                """
                UPDATE mix_calculator_sessions
                SET tenant_id = COALESCE(
                    (
                        SELECT products.tenant_id
                        FROM products
                        WHERE products.id = mix_calculator_sessions.product_id
                    ),
                    :default_tenant
                )
                WHERE tenant_id IS NULL OR tenant_id = '' OR tenant_id = 'default'
                """
                ),
            ),
            (
                "mix_calculator_session_lines",
                text(
                """
                UPDATE mix_calculator_session_lines
                SET tenant_id = (
                    SELECT mix_calculator_sessions.tenant_id
                    FROM mix_calculator_sessions
                    WHERE mix_calculator_sessions.id = mix_calculator_session_lines.session_id
                )
                WHERE tenant_id IS NULL OR tenant_id = '' OR tenant_id = 'default'
                """
                ),
            ),
            (
                "scenarios",
                text(
                """
                UPDATE scenarios
                SET tenant_id = :default_tenant
                WHERE tenant_id IS NULL OR tenant_id = '' OR tenant_id = 'default'
                """
                ),
            ),
            (
                "costing_results",
                text(
                """
                UPDATE costing_results
                SET tenant_id = COALESCE(
                    (
                        SELECT products.tenant_id
                        FROM products
                        WHERE products.id = costing_results.product_id
                    ),
                    (
                        SELECT scenarios.tenant_id
                        FROM scenarios
                        WHERE scenarios.id = costing_results.scenario_id
                    ),
                    :default_tenant
                )
                WHERE tenant_id IS NULL OR tenant_id = '' OR tenant_id = 'default'
                """
                ),
            ),
            (
                "process_cost_rules",
                text(
                """
                UPDATE process_cost_rules
                SET tenant_id = :default_tenant
                WHERE tenant_id IS NULL OR tenant_id = '' OR tenant_id = 'default'
                """
                ),
            ),
            (
                "packaging_cost_rules",
                text(
                """
                UPDATE packaging_cost_rules
                SET tenant_id = :default_tenant
                WHERE tenant_id IS NULL OR tenant_id = '' OR tenant_id = 'default'
                """
                ),
            ),
            (
                "freight_cost_rules",
                text(
                """
                UPDATE freight_cost_rules
                SET tenant_id = :default_tenant
                WHERE tenant_id IS NULL OR tenant_id = '' OR tenant_id = 'default'
                """
                ),
            ),
        ]

        for table_name, statement in statements:
            if table_name not in existing_tables:
                continue
            result = connection.execute(statement, {"default_tenant": default_tenant})
            if result.rowcount and result.rowcount > 0:
                synced_rows[table_name] = result.rowcount

    return synced_rows


def sync_product_visibility(engine: Engine) -> int:
    if not _table_exists(engine, "products") or not _has_column(engine, "products", "visible"):
        return 0

    with engine.begin() as connection:
        result = connection.execute(
            text(
                """
                UPDATE products
                SET visible = FALSE
                WHERE client_name IN :hidden_clients
                  AND (visible IS NULL OR visible != FALSE)
                """
            ).bindparams(bindparam("hidden_clients", value=HIDDEN_PRODUCT_CLIENTS, expanding=True))
        )
    return result.rowcount or 0


def resync_identity_sequences(engine: Engine) -> tuple[str, ...]:
    """Realign Postgres identity sequences with each table's current MAX(id).

    After a bulk import that carries original primary keys across (the SQLite →
    Postgres migration inserts rows with their existing ids), every table's
    sequence still points at its starting value. The next INSERT then reuses an
    id that already exists and fails with ``duplicate key value violates unique
    constraint`` — which is why creating a new mix/ingredient/product saved fine
    on SQLite but not on production Postgres.

    This advances each ``id`` sequence to MAX(id) so the next INSERT continues
    cleanly. It is a no-op on SQLite and idempotent on Postgres, so it is safe to
    run on every startup. A per-table failure is skipped rather than aborting the
    whole boot.
    """
    if engine.dialect.name != "postgresql":
        return ()

    resynced: list[str] = []
    inspector = inspect(engine)
    with engine.begin() as connection:
        for table_name in inspector.get_table_names():
            if not any(column["name"] == "id" for column in inspector.get_columns(table_name)):
                continue
            try:
                sequence = connection.execute(
                    text("SELECT pg_get_serial_sequence(:table, 'id')"),
                    {"table": table_name},
                ).scalar()
                if not sequence:
                    continue
                max_id = connection.execute(text(f'SELECT MAX(id) FROM "{table_name}"')).scalar()
                if max_id is None:
                    continue
                connection.execute(
                    text("SELECT setval(:sequence, :value, true)"),
                    {"sequence": sequence, "value": int(max_id)},
                )
                resynced.append(table_name)
            except Exception:
                # A single problematic table must not block startup; the others
                # still get realigned.
                continue
    return tuple(resynced)


def bootstrap_schema(engine: Engine, metadata: MetaData) -> MigrationReport:
    created_tables = ensure_metadata_tables(engine, metadata)
    added_columns = ensure_tenant_columns(engine) + ensure_legacy_columns(engine)
    resynced_sequences = resync_identity_sequences(engine)
    return MigrationReport(
        created_tables=created_tables,
        added_columns=added_columns,
        resynced_sequences=resynced_sequences,
    )
