"""tenant-scope Hermes profiles, reports, and scheduled work

Revision ID: 20260717_0028
Revises: 20260717_0027
Create Date: 2026-07-17
"""

from __future__ import annotations

from collections.abc import Sequence

import sqlalchemy as sa

from alembic import op

revision: str = "20260717_0028"
down_revision: str | None = "20260717_0027"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def _require_postgresql() -> None:
    dialect_name = op.get_bind().dialect.name
    if dialect_name != "postgresql":
        raise RuntimeError(
            "20260717_0028 only supports PostgreSQL; "
            f"refusing to mutate {dialect_name} without transactional constraint DDL"
        )


def _inspector() -> sa.Inspector:
    return sa.inspect(op.get_bind())


def _has_table(table_name: str) -> bool:
    return _inspector().has_table(table_name)


def _has_column(table_name: str, column_name: str) -> bool:
    if not _has_table(table_name):
        return False
    return column_name in {str(item["name"]) for item in _inspector().get_columns(table_name)}


def _constraint_exists(table_name: str, constraint_name: str) -> bool:
    if not _has_table(table_name):
        return False
    inspector = _inspector()
    names = {
        str(item.get("name") or "")
        for item in (
            *inspector.get_unique_constraints(table_name),
            *inspector.get_foreign_keys(table_name),
            *inspector.get_check_constraints(table_name),
        )
    }
    return constraint_name in names


def _index_exists(table_name: str, index_name: str) -> bool:
    if not _has_table(table_name):
        return False
    return index_name in {
        str(item.get("name") or "") for item in _inspector().get_indexes(table_name)
    }


def _drop_foreign_keys_for_columns(
    table_name: str,
    columns: tuple[str, ...],
) -> None:
    if not _has_table(table_name):
        return
    for item in _inspector().get_foreign_keys(table_name):
        if tuple(item.get("constrained_columns") or ()) != columns:
            continue
        name = str(item.get("name") or "")
        if name:
            op.drop_constraint(name, table_name, type_="foreignkey")


def upgrade() -> None:
    _require_postgresql()
    _scope_profile_snapshots()
    _scope_hermes_task_tables()
    _scope_hermes_risk_reports()
    _create_finance_report_tables()


def downgrade() -> None:
    _require_postgresql()
    _assert_safe_downgrade()
    if _has_table("tenant_finance_report_runs"):
        op.drop_table("tenant_finance_report_runs")
    if _has_table("tenant_finance_report_configs"):
        op.drop_table("tenant_finance_report_configs")

    _downgrade_risk_reports()
    _downgrade_task_tables()
    _downgrade_profile_snapshots()


def _scope_profile_snapshots() -> None:
    table = "employee_behavior_profile_snapshots"
    if not _has_table(table):
        return
    op.add_column(
        table,
        sa.Column("tenant_id", sa.String(length=64), nullable=False, server_default="default"),
    )
    op.create_foreign_key(
        "fk_employee_behavior_profiles_tenant",
        table,
        "tenants",
        ["tenant_id"],
        ["tenant_id"],
        ondelete="RESTRICT",
    )
    op.create_unique_constraint(
        "uq_employee_behavior_profiles_tenant_id",
        table,
        ["tenant_id", "id"],
    )
    if _has_table("employees"):
        op.create_foreign_key(
            "fk_employee_behavior_profiles_tenant_employee",
            table,
            "employees",
            ["tenant_id", "subject_id"],
            ["tenant_id", "id"],
            ondelete="CASCADE",
        )
    if _index_exists(table, "ix_employee_behavior_profile_latest"):
        op.drop_index("ix_employee_behavior_profile_latest", table_name=table)
    op.create_index(
        "ix_employee_behavior_profile_latest",
        table,
        [
            "tenant_id",
            "subject_id",
            "profile_type",
            "window_days",
            "expense_type_scope",
            "calculated_at",
        ],
    )
    op.alter_column(table, "tenant_id", server_default=None)


def _scope_hermes_task_tables() -> None:
    config_table = "hermes_task_configs"
    log_table = "hermes_task_execution_logs"
    for table, fk_name in (
        (config_table, "fk_hermes_task_configs_tenant"),
        (log_table, "fk_hermes_task_execution_logs_tenant"),
    ):
        if not _has_table(table):
            continue
        op.add_column(
            table,
            sa.Column(
                "tenant_id",
                sa.String(length=64),
                nullable=False,
                server_default="default",
            ),
        )
        op.create_foreign_key(
            fk_name,
            table,
            "tenants",
            ["tenant_id"],
            ["tenant_id"],
            ondelete="RESTRICT",
        )
        op.alter_column(table, "tenant_id", server_default=None)

    if _has_table(config_table):
        op.create_unique_constraint(
            "uq_hermes_task_configs_tenant_id",
            config_table,
            ["tenant_id", "id"],
        )
        op.create_index(
            "ix_hermes_task_configs_tenant_enabled",
            config_table,
            ["tenant_id", "is_enabled"],
        )
    if _has_table(log_table):
        op.create_unique_constraint(
            "uq_hermes_task_execution_logs_tenant_id",
            log_table,
            ["tenant_id", "id"],
        )
        if _has_table(config_table):
            _drop_foreign_keys_for_columns(log_table, ("config_id",))
            op.create_foreign_key(
                "fk_hermes_task_logs_tenant_config",
                log_table,
                config_table,
                ["tenant_id", "config_id"],
                ["tenant_id", "id"],
                ondelete="CASCADE",
            )
        op.create_index(
            "ix_hermes_task_logs_tenant_started",
            log_table,
            ["tenant_id", "started_at"],
        )


def _scope_hermes_risk_reports() -> None:
    table = "hermes_risk_reports"
    if not _has_table(table):
        return
    op.add_column(
        table,
        sa.Column("tenant_id", sa.String(length=64), nullable=False, server_default="default"),
    )
    op.create_foreign_key(
        "fk_hermes_risk_reports_tenant",
        table,
        "tenants",
        ["tenant_id"],
        ["tenant_id"],
        ondelete="RESTRICT",
    )
    op.create_unique_constraint(
        "uq_hermes_risk_reports_tenant_id",
        table,
        ["tenant_id", "id"],
    )
    if _has_table("expense_claims"):
        _drop_foreign_keys_for_columns(table, ("claim_id",))
        op.create_foreign_key(
            "fk_hermes_risk_reports_tenant_claim",
            table,
            "expense_claims",
            ["tenant_id", "claim_id"],
            ["tenant_id", "id"],
            ondelete="CASCADE",
        )
    if _has_table("hermes_task_execution_logs"):
        _drop_foreign_keys_for_columns(table, ("execution_log_id",))
        op.create_foreign_key(
            "fk_hermes_risk_reports_tenant_log",
            table,
            "hermes_task_execution_logs",
            ["tenant_id", "execution_log_id"],
            ["tenant_id", "id"],
            ondelete="CASCADE",
        )
    op.create_index(
        "ix_hermes_risk_reports_tenant_status",
        table,
        ["tenant_id", "status"],
    )
    op.alter_column(table, "tenant_id", server_default=None)


def _create_finance_report_tables() -> None:
    if not _has_table("tenant_finance_report_configs"):
        op.create_table(
            "tenant_finance_report_configs",
            sa.Column("tenant_id", sa.String(length=64), nullable=False),
            sa.Column("status", sa.String(length=20), nullable=False, server_default="disabled"),
            sa.Column("delivery_enabled", sa.Boolean(), nullable=False, server_default=sa.false()),
            sa.Column("recipients_json", sa.JSON(), nullable=False, server_default="[]"),
            sa.Column("updated_by", sa.String(length=100), nullable=False, server_default=""),
            sa.Column(
                "created_at",
                sa.DateTime(timezone=True),
                nullable=False,
                server_default=sa.func.now(),
            ),
            sa.Column(
                "updated_at",
                sa.DateTime(timezone=True),
                nullable=False,
                server_default=sa.func.now(),
            ),
            sa.CheckConstraint(
                "status IN ('active', 'disabled')",
                name="ck_tenant_finance_report_configs_status",
            ),
            sa.ForeignKeyConstraint(
                ["tenant_id"],
                ["tenants.tenant_id"],
                ondelete="CASCADE",
            ),
            sa.PrimaryKeyConstraint("tenant_id"),
        )
    if not _has_table("tenant_finance_report_runs"):
        op.create_table(
            "tenant_finance_report_runs",
            sa.Column("id", sa.String(length=36), nullable=False),
            sa.Column("tenant_id", sa.String(length=64), nullable=False),
            sa.Column("report_type", sa.String(length=20), nullable=False),
            sa.Column("period_start", sa.Date(), nullable=False),
            sa.Column("period_end", sa.Date(), nullable=False),
            sa.Column("idempotency_key", sa.String(length=180), nullable=False),
            sa.Column("status", sa.String(length=20), nullable=False, server_default="running"),
            sa.Column("agent_run_id", sa.String(length=50), nullable=True),
            sa.Column("storage_key", sa.String(length=512), nullable=False, server_default=""),
            sa.Column("result_json", sa.JSON(), nullable=False, server_default="{}"),
            sa.Column("error_message", sa.Text(), nullable=True),
            sa.Column(
                "started_at",
                sa.DateTime(timezone=True),
                nullable=False,
                server_default=sa.func.now(),
            ),
            sa.Column("finished_at", sa.DateTime(timezone=True), nullable=True),
            sa.CheckConstraint(
                "report_type IN ('weekly', 'quarterly', 'annual')",
                name="ck_tenant_finance_report_runs_type",
            ),
            sa.CheckConstraint(
                "status IN ('running', 'succeeded', 'failed')",
                name="ck_tenant_finance_report_runs_status",
            ),
            sa.ForeignKeyConstraint(
                ["tenant_id"],
                ["tenants.tenant_id"],
                ondelete="CASCADE",
            ),
            sa.PrimaryKeyConstraint("id"),
            sa.UniqueConstraint(
                "tenant_id",
                "idempotency_key",
                name="uq_tenant_finance_report_runs_idempotency",
            ),
        )
        op.create_index(
            "ix_tenant_finance_report_runs_period",
            "tenant_finance_report_runs",
            ["tenant_id", "report_type", "period_start", "period_end"],
        )
        op.create_index(
            "ix_tenant_finance_report_runs_agent_run_id",
            "tenant_finance_report_runs",
            ["agent_run_id"],
        )


def _assert_safe_downgrade() -> None:
    connection = op.get_bind()
    for table in (
        "employee_behavior_profile_snapshots",
        "hermes_task_configs",
        "hermes_task_execution_logs",
        "hermes_risk_reports",
    ):
        if not _has_column(table, "tenant_id"):
            continue
        count = connection.execute(
            sa.text(f"SELECT COUNT(*) FROM {table} WHERE tenant_id <> 'default'")
        ).scalar_one()
        if int(count or 0) > 0:
            raise RuntimeError(f"Refusing downgrade: {table} contains non-default tenant rows.")
    for table in ("tenant_finance_report_configs", "tenant_finance_report_runs"):
        if not _has_table(table):
            continue
        count = connection.execute(sa.text(f"SELECT COUNT(*) FROM {table}")).scalar_one()
        if int(count or 0) > 0:
            raise RuntimeError(f"Refusing downgrade: {table} contains data.")


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


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


def _downgrade_risk_reports() -> None:
    table = "hermes_risk_reports"
    if not _has_column(table, "tenant_id"):
        return
    _drop_index_if_exists(table, "ix_hermes_risk_reports_tenant_status")
    _drop_constraint_if_exists(table, "fk_hermes_risk_reports_tenant_log", "foreignkey")
    _drop_constraint_if_exists(table, "fk_hermes_risk_reports_tenant_claim", "foreignkey")
    _drop_constraint_if_exists(table, "uq_hermes_risk_reports_tenant_id", "unique")
    _drop_constraint_if_exists(table, "fk_hermes_risk_reports_tenant", "foreignkey")
    if _has_table("expense_claims"):
        op.create_foreign_key(
            "hermes_risk_reports_claim_id_fkey",
            table,
            "expense_claims",
            ["claim_id"],
            ["id"],
            ondelete="CASCADE",
        )
    if _has_table("hermes_task_execution_logs"):
        op.create_foreign_key(
            "hermes_risk_reports_execution_log_id_fkey",
            table,
            "hermes_task_execution_logs",
            ["execution_log_id"],
            ["id"],
            ondelete="CASCADE",
        )
    op.drop_column(table, "tenant_id")


def _downgrade_task_tables() -> None:
    config_table = "hermes_task_configs"
    log_table = "hermes_task_execution_logs"
    if _has_column(log_table, "tenant_id"):
        _drop_index_if_exists(log_table, "ix_hermes_task_logs_tenant_started")
        _drop_constraint_if_exists(
            log_table,
            "fk_hermes_task_logs_tenant_config",
            "foreignkey",
        )
        _drop_constraint_if_exists(
            log_table,
            "uq_hermes_task_execution_logs_tenant_id",
            "unique",
        )
        _drop_constraint_if_exists(
            log_table,
            "fk_hermes_task_execution_logs_tenant",
            "foreignkey",
        )
        if _has_table(config_table):
            op.create_foreign_key(
                "hermes_task_execution_logs_config_id_fkey",
                log_table,
                config_table,
                ["config_id"],
                ["id"],
            )
        op.drop_column(log_table, "tenant_id")
    if _has_column(config_table, "tenant_id"):
        _drop_index_if_exists(config_table, "ix_hermes_task_configs_tenant_enabled")
        _drop_constraint_if_exists(
            config_table,
            "uq_hermes_task_configs_tenant_id",
            "unique",
        )
        _drop_constraint_if_exists(
            config_table,
            "fk_hermes_task_configs_tenant",
            "foreignkey",
        )
        op.drop_column(config_table, "tenant_id")


def _downgrade_profile_snapshots() -> None:
    table = "employee_behavior_profile_snapshots"
    if not _has_column(table, "tenant_id"):
        return
    _drop_index_if_exists(table, "ix_employee_behavior_profile_latest")
    _drop_constraint_if_exists(
        table,
        "fk_employee_behavior_profiles_tenant_employee",
        "foreignkey",
    )
    _drop_constraint_if_exists(
        table,
        "uq_employee_behavior_profiles_tenant_id",
        "unique",
    )
    _drop_constraint_if_exists(
        table,
        "fk_employee_behavior_profiles_tenant",
        "foreignkey",
    )
    op.drop_column(table, "tenant_id")
    op.create_index(
        "ix_employee_behavior_profile_latest",
        table,
        [
            "subject_id",
            "profile_type",
            "window_days",
            "expense_type_scope",
            "calculated_at",
        ],
    )
