"""Phase 2: migrate loyalty module to merchant-based architecture

Revision ID: loyalty_003_phase2
Revises: 0fb5d6d6ff97
Create Date: 2026-02-06 20:30:00.000000

Phase 2 changes:
- loyalty_programs: store_id -> merchant_id (one program per merchant)
- loyalty_cards: add merchant_id, rename store_id -> enrolled_at_store_id
- loyalty_transactions: add merchant_id, add related_transaction_id, store_id nullable
- staff_pins: add merchant_id
- NEW TABLE: merchant_loyalty_settings
- NEW COLUMNS on loyalty_programs: points_expiration_days, welcome_bonus_points,
  minimum_redemption_points, minimum_purchase_cents, tier_config
- NEW COLUMN on loyalty_cards: last_activity_at
"""

from collections.abc import Sequence

import sqlalchemy as sa
from sqlalchemy import text

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "loyalty_003_phase2"
down_revision: str | None = "0fb5d6d6ff97"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
    # =========================================================================
    # 1. Create merchant_loyalty_settings table
    # =========================================================================
    op.create_table(
        "merchant_loyalty_settings",
        sa.Column("id", sa.Integer(), nullable=False),
        sa.Column("merchant_id", sa.Integer(), nullable=False),
        sa.Column(
            "staff_pin_policy",
            sa.String(length=20),
            nullable=False,
            server_default="required",
        ),
        sa.Column(
            "staff_pin_lockout_attempts",
            sa.Integer(),
            nullable=False,
            server_default="5",
        ),
        sa.Column(
            "staff_pin_lockout_minutes",
            sa.Integer(),
            nullable=False,
            server_default="30",
        ),
        sa.Column(
            "allow_self_enrollment",
            sa.Boolean(),
            nullable=False,
            server_default=sa.text("true"),
        ),
        sa.Column(
            "allow_void_transactions",
            sa.Boolean(),
            nullable=False,
            server_default=sa.text("true"),
        ),
        sa.Column(
            "allow_cross_location_redemption",
            sa.Boolean(),
            nullable=False,
            server_default=sa.text("true"),
        ),
        sa.Column(
            "require_order_reference",
            sa.Boolean(),
            nullable=False,
            server_default=sa.text("false"),
        ),
        sa.Column(
            "log_ip_addresses",
            sa.Boolean(),
            nullable=False,
            server_default=sa.text("true"),
        ),
        sa.Column("created_at", sa.DateTime(), nullable=False),
        sa.Column("updated_at", sa.DateTime(), nullable=False),
        sa.ForeignKeyConstraint(
            ["merchant_id"], ["merchants.id"], ondelete="CASCADE"
        ),
        sa.PrimaryKeyConstraint("id"),
    )
    op.create_index(
        op.f("ix_merchant_loyalty_settings_id"),
        "merchant_loyalty_settings",
        ["id"],
        unique=False,
    )
    op.create_index(
        op.f("ix_merchant_loyalty_settings_merchant_id"),
        "merchant_loyalty_settings",
        ["merchant_id"],
        unique=True,
    )

    # =========================================================================
    # 2. Modify loyalty_programs: store_id -> merchant_id + new columns
    # =========================================================================

    # Add merchant_id (nullable first for data migration)
    op.add_column(
        "loyalty_programs", sa.Column("merchant_id", sa.Integer(), nullable=True)
    )

    # Migrate existing data: derive merchant_id from store_id
    op.execute(
        """
        UPDATE loyalty_programs lp
        SET merchant_id = v.merchant_id
        FROM stores v
        WHERE v.id = lp.store_id
        """
    )

    # Make merchant_id non-nullable
    op.alter_column("loyalty_programs", "merchant_id", nullable=False)

    # Add FK and indexes
    op.create_foreign_key(
        "fk_loyalty_programs_merchant_id",
        "loyalty_programs",
        "merchants",
        ["merchant_id"],
        ["id"],
        ondelete="CASCADE",
    )
    op.create_index(
        op.f("ix_loyalty_programs_merchant_id"),
        "loyalty_programs",
        ["merchant_id"],
        unique=True,
    )
    op.create_index(
        "idx_loyalty_program_merchant_active",
        "loyalty_programs",
        ["merchant_id", "is_active"],
    )

    # Add new Phase 2 columns
    op.add_column(
        "loyalty_programs",
        sa.Column("points_expiration_days", sa.Integer(), nullable=True),
    )
    op.add_column(
        "loyalty_programs",
        sa.Column(
            "welcome_bonus_points",
            sa.Integer(),
            nullable=False,
            server_default="0",
        ),
    )
    op.add_column(
        "loyalty_programs",
        sa.Column(
            "minimum_redemption_points",
            sa.Integer(),
            nullable=False,
            server_default="100",
        ),
    )
    op.add_column(
        "loyalty_programs",
        sa.Column(
            "minimum_purchase_cents",
            sa.Integer(),
            nullable=False,
            server_default="0",
        ),
    )
    op.add_column(
        "loyalty_programs",
        sa.Column("tier_config", sa.JSON(), nullable=True),
    )

    # Drop old store_id column and indexes
    op.drop_index("idx_loyalty_program_store_active", table_name="loyalty_programs")
    op.drop_index(
        op.f("ix_loyalty_programs_store_id"), table_name="loyalty_programs"
    )
    op.drop_constraint(
        "loyalty_programs_store_id_fkey", "loyalty_programs", type_="foreignkey"
    )
    op.drop_column("loyalty_programs", "store_id")

    # =========================================================================
    # 3. Modify loyalty_cards: add merchant_id, rename store_id
    # =========================================================================

    # Add merchant_id
    op.add_column(
        "loyalty_cards", sa.Column("merchant_id", sa.Integer(), nullable=True)
    )

    # Migrate data
    op.execute(
        """
        UPDATE loyalty_cards lc
        SET merchant_id = v.merchant_id
        FROM stores v
        WHERE v.id = lc.store_id
        """
    )

    op.alter_column("loyalty_cards", "merchant_id", nullable=False)

    op.create_foreign_key(
        "fk_loyalty_cards_merchant_id",
        "loyalty_cards",
        "merchants",
        ["merchant_id"],
        ["id"],
        ondelete="CASCADE",
    )
    op.create_index(
        op.f("ix_loyalty_cards_merchant_id"),
        "loyalty_cards",
        ["merchant_id"],
        unique=False,
    )
    op.create_index(
        "idx_loyalty_card_merchant_active",
        "loyalty_cards",
        ["merchant_id", "is_active"],
    )
    op.create_index(
        "idx_loyalty_card_merchant_customer",
        "loyalty_cards",
        ["merchant_id", "customer_id"],
        unique=True,
    )

    # Rename store_id -> enrolled_at_store_id, make nullable, change FK
    op.drop_index("idx_loyalty_card_store_active", table_name="loyalty_cards")
    op.drop_index(op.f("ix_loyalty_cards_store_id"), table_name="loyalty_cards")
    op.drop_constraint(
        "loyalty_cards_store_id_fkey", "loyalty_cards", type_="foreignkey"
    )
    op.alter_column(
        "loyalty_cards",
        "store_id",
        new_column_name="enrolled_at_store_id",
        nullable=True,
    )
    op.create_foreign_key(
        "fk_loyalty_cards_enrolled_store",
        "loyalty_cards",
        "stores",
        ["enrolled_at_store_id"],
        ["id"],
        ondelete="SET NULL",
    )
    op.create_index(
        op.f("ix_loyalty_cards_enrolled_at_store_id"),
        "loyalty_cards",
        ["enrolled_at_store_id"],
        unique=False,
    )

    # Add last_activity_at
    op.add_column(
        "loyalty_cards",
        sa.Column("last_activity_at", sa.DateTime(timezone=True), nullable=True),
    )

    # =========================================================================
    # 4. Modify loyalty_transactions: add merchant_id, related_transaction_id
    # =========================================================================

    # Add merchant_id
    op.add_column(
        "loyalty_transactions",
        sa.Column("merchant_id", sa.Integer(), nullable=True),
    )

    # Migrate data (from card's merchant)
    op.execute(
        """
        UPDATE loyalty_transactions lt
        SET merchant_id = lc.merchant_id
        FROM loyalty_cards lc
        WHERE lc.id = lt.card_id
        """
    )

    op.alter_column("loyalty_transactions", "merchant_id", nullable=False)

    op.create_foreign_key(
        "fk_loyalty_transactions_merchant_id",
        "loyalty_transactions",
        "merchants",
        ["merchant_id"],
        ["id"],
        ondelete="CASCADE",
    )
    op.create_index(
        op.f("ix_loyalty_transactions_merchant_id"),
        "loyalty_transactions",
        ["merchant_id"],
        unique=False,
    )
    op.create_index(
        "idx_loyalty_tx_merchant_date",
        "loyalty_transactions",
        ["merchant_id", "transaction_at"],
    )
    op.create_index(
        "idx_loyalty_tx_merchant_store",
        "loyalty_transactions",
        ["merchant_id", "store_id"],
    )

    # Make store_id nullable and change FK to SET NULL
    op.drop_constraint(
        "loyalty_transactions_store_id_fkey",
        "loyalty_transactions",
        type_="foreignkey",
    )
    op.alter_column("loyalty_transactions", "store_id", nullable=True)
    op.create_foreign_key(
        "fk_loyalty_transactions_store_id",
        "loyalty_transactions",
        "stores",
        ["store_id"],
        ["id"],
        ondelete="SET NULL",
    )

    # Add related_transaction_id (for void linkage)
    op.add_column(
        "loyalty_transactions",
        sa.Column("related_transaction_id", sa.Integer(), nullable=True),
    )
    op.create_foreign_key(
        "fk_loyalty_tx_related",
        "loyalty_transactions",
        "loyalty_transactions",
        ["related_transaction_id"],
        ["id"],
        ondelete="SET NULL",
    )
    op.create_index(
        op.f("ix_loyalty_transactions_related_transaction_id"),
        "loyalty_transactions",
        ["related_transaction_id"],
        unique=False,
    )

    # =========================================================================
    # 5. Modify staff_pins: add merchant_id
    # =========================================================================

    op.add_column(
        "staff_pins", sa.Column("merchant_id", sa.Integer(), nullable=True)
    )

    # Migrate data (from store's merchant)
    op.execute(
        """
        UPDATE staff_pins sp
        SET merchant_id = v.merchant_id
        FROM stores v
        WHERE v.id = sp.store_id
        """
    )

    op.alter_column("staff_pins", "merchant_id", nullable=False)

    op.create_foreign_key(
        "fk_staff_pins_merchant_id",
        "staff_pins",
        "merchants",
        ["merchant_id"],
        ["id"],
        ondelete="CASCADE",
    )
    op.create_index(
        op.f("ix_staff_pins_merchant_id"),
        "staff_pins",
        ["merchant_id"],
        unique=False,
    )
    op.create_index(
        "idx_staff_pin_merchant_active",
        "staff_pins",
        ["merchant_id", "is_active"],
    )


def _constraint_exists(table: str, constraint: str) -> bool:
    """Check if a constraint exists on a table."""
    conn = op.get_bind()
    result = conn.execute(text(
        "SELECT 1 FROM pg_constraint c "
        "JOIN pg_class r ON c.conrelid = r.oid "
        "JOIN pg_namespace n ON r.relnamespace = n.oid "
        "WHERE n.nspname = 'public' AND r.relname = :t AND c.conname = :c"
    ), {"t": table, "c": constraint})
    return result.fetchone() is not None


def _col_exists(table: str, col: str) -> bool:
    """Check if column exists."""
    conn = op.get_bind()
    result = conn.execute(text(
        "SELECT 1 FROM information_schema.columns "
        "WHERE table_schema='public' AND table_name=:t AND column_name=:c"
    ), {"t": table, "c": col})
    return result.fetchone() is not None


def _index_exists(index: str) -> bool:
    """Check if an index exists."""
    conn = op.get_bind()
    result = conn.execute(text(
        "SELECT 1 FROM pg_indexes WHERE schemaname='public' AND indexname=:i"
    ), {"i": index})
    return result.fetchone() is not None


def _table_exists(table: str) -> bool:
    """Check if table exists."""
    conn = op.get_bind()
    result = conn.execute(text(
        "SELECT 1 FROM information_schema.tables "
        "WHERE table_schema='public' AND table_name=:t"
    ), {"t": table})
    return result.fetchone() is not None


def _safe_drop_index(name: str, table: str) -> None:
    if _index_exists(name):
        op.drop_index(name, table_name=table)


def _safe_drop_constraint(name: str, table: str, type_: str = "foreignkey") -> None:
    if _constraint_exists(table, name):
        op.drop_constraint(name, table, type_=type_)


def _safe_drop_column(table: str, col: str) -> None:
    if _col_exists(table, col):
        op.drop_column(table, col)


def downgrade() -> None:
    # t001 may have renamed stores→vendors and merchants→companies before us.
    # Detect the correct table name for FK references.
    stores_ref = "stores" if _table_exists("stores") else "vendors"

    # =========================================================================
    # 5. Revert staff_pins
    # =========================================================================
    _safe_drop_index("idx_staff_pin_merchant_active", "staff_pins")
    _safe_drop_index("ix_staff_pins_merchant_id", "staff_pins")
    _safe_drop_constraint("fk_staff_pins_merchant_id", "staff_pins")
    _safe_drop_column("staff_pins", "merchant_id")

    # =========================================================================
    # 4. Revert loyalty_transactions
    # =========================================================================
    _safe_drop_index("ix_loyalty_transactions_related_transaction_id", "loyalty_transactions")
    _safe_drop_constraint("fk_loyalty_tx_related", "loyalty_transactions")
    _safe_drop_column("loyalty_transactions", "related_transaction_id")

    _safe_drop_constraint("fk_loyalty_transactions_store_id", "loyalty_transactions")
    if _col_exists("loyalty_transactions", "store_id"):
        op.alter_column("loyalty_transactions", "store_id", nullable=False)
        op.create_foreign_key(
            "loyalty_transactions_store_id_fkey",
            "loyalty_transactions",
            stores_ref,
            ["store_id"],
            ["id"],
            ondelete="CASCADE",
        )

    _safe_drop_index("idx_loyalty_tx_merchant_store", "loyalty_transactions")
    _safe_drop_index("idx_loyalty_tx_merchant_date", "loyalty_transactions")
    _safe_drop_index("ix_loyalty_transactions_merchant_id", "loyalty_transactions")
    _safe_drop_constraint("fk_loyalty_transactions_merchant_id", "loyalty_transactions")
    _safe_drop_column("loyalty_transactions", "merchant_id")

    # =========================================================================
    # 3. Revert loyalty_cards
    # =========================================================================
    _safe_drop_column("loyalty_cards", "last_activity_at")

    # Rename enrolled_at_store_id back to store_id
    enrolled_col = "enrolled_at_store_id"
    if _col_exists("loyalty_cards", enrolled_col):
        _safe_drop_index("ix_loyalty_cards_enrolled_at_store_id", "loyalty_cards")
        _safe_drop_constraint("fk_loyalty_cards_enrolled_store", "loyalty_cards")
        op.alter_column(
            "loyalty_cards",
            enrolled_col,
            new_column_name="store_id",
            nullable=False,
        )
        op.create_foreign_key(
            "loyalty_cards_store_id_fkey",
            "loyalty_cards",
            stores_ref,
            ["store_id"],
            ["id"],
            ondelete="CASCADE",
        )
        op.create_index(
            op.f("ix_loyalty_cards_store_id"),
            "loyalty_cards",
            ["store_id"],
            unique=False,
        )
        op.create_index(
            "idx_loyalty_card_store_active",
            "loyalty_cards",
            ["store_id", "is_active"],
        )

    _safe_drop_index("idx_loyalty_card_merchant_customer", "loyalty_cards")
    _safe_drop_index("idx_loyalty_card_merchant_active", "loyalty_cards")
    _safe_drop_index("ix_loyalty_cards_merchant_id", "loyalty_cards")
    _safe_drop_constraint("fk_loyalty_cards_merchant_id", "loyalty_cards")
    _safe_drop_column("loyalty_cards", "merchant_id")

    # =========================================================================
    # 2. Revert loyalty_programs
    # =========================================================================
    if not _col_exists("loyalty_programs", "store_id"):
        op.add_column(
            "loyalty_programs",
            sa.Column("store_id", sa.Integer(), nullable=True),
        )
        op.create_foreign_key(
            "loyalty_programs_store_id_fkey",
            "loyalty_programs",
            stores_ref,
            ["store_id"],
            ["id"],
            ondelete="CASCADE",
        )
        op.create_index(
            op.f("ix_loyalty_programs_store_id"),
            "loyalty_programs",
            ["store_id"],
            unique=True,
        )
        op.create_index(
            "idx_loyalty_program_store_active",
            "loyalty_programs",
            ["store_id", "is_active"],
        )

    _safe_drop_column("loyalty_programs", "tier_config")
    _safe_drop_column("loyalty_programs", "minimum_purchase_cents")
    _safe_drop_column("loyalty_programs", "minimum_redemption_points")
    _safe_drop_column("loyalty_programs", "welcome_bonus_points")
    _safe_drop_column("loyalty_programs", "points_expiration_days")

    _safe_drop_index("idx_loyalty_program_merchant_active", "loyalty_programs")
    _safe_drop_index("ix_loyalty_programs_merchant_id", "loyalty_programs")
    _safe_drop_constraint("fk_loyalty_programs_merchant_id", "loyalty_programs")
    _safe_drop_column("loyalty_programs", "merchant_id")

    # =========================================================================
    # 1. Drop merchant_loyalty_settings table
    # =========================================================================
    # t001 may have renamed this to company_loyalty_settings
    settings_table = "merchant_loyalty_settings" if _table_exists("merchant_loyalty_settings") else "company_loyalty_settings"
    if _table_exists(settings_table):
        _safe_drop_index("ix_merchant_loyalty_settings_merchant_id", settings_table)
        _safe_drop_index("ix_merchant_loyalty_settings_id", settings_table)
        # Also check for indexes under the renamed table name
        _safe_drop_index("ix_company_loyalty_settings_merchant_id", settings_table)
        _safe_drop_index("ix_company_loyalty_settings_id", settings_table)
        op.drop_table(settings_table)
