# Generated by Django 4.2.27 on 2026-02-25 00:00

from django.conf import settings
from django.core.management.color import no_style
from django.db import migrations

EVE_SDE_REGION_MODEL = getattr(
    settings, "AACAMPAIGN_SDE_REGION_MODEL", "eve_sde.Region"
)
EVE_SDE_CONSTELLATION_MODEL = getattr(
    settings, "AACAMPAIGN_SDE_CONSTELLATION_MODEL", "eve_sde.Constellation"
)
EVE_SDE_SOLAR_SYSTEM_MODEL = getattr(
    settings, "AACAMPAIGN_SDE_SOLAR_SYSTEM_MODEL", "eve_sde.SolarSystem"
)


def _get_model(apps, label):
    app_label, model_name = label.split(".")
    return apps.get_model(app_label, model_name)


def _quote(schema_editor, name):
    return schema_editor.quote_name(name)


def _table_exists(schema_editor, table_name):
    connection = schema_editor.connection
    with connection.cursor() as cursor:
        return table_name in connection.introspection.table_names(cursor)


def _table_columns(schema_editor, table_name):
    connection = schema_editor.connection
    with connection.cursor() as cursor:
        description = connection.introspection.get_table_description(cursor, table_name)
    return [col.name for col in description]


def _table_constraints(schema_editor, table_name):
    connection = schema_editor.connection
    with connection.cursor() as cursor:
        return connection.introspection.get_constraints(cursor, table_name)


def _has_fk_to_table(schema_editor, table_name, column_name, target_table):
    constraints = _table_constraints(schema_editor, table_name)
    for details in constraints.values():
        if details.get("columns") != [column_name]:
            continue
        foreign_key = details.get("foreign_key")
        if foreign_key and foreign_key[0] == target_table:
            return True
    return False


def _backup_table_name(table_name):
    return f"{table_name}_legacy"


def _ensure_backup_table_available(schema_editor, table_name):
    backup_name = _backup_table_name(table_name)
    if _table_exists(schema_editor, backup_name):
        raise RuntimeError(
            f"Refusing to migrate because temporary table '{backup_name}' already exists. "
            "Remove or rename it and rerun the migration."
        )
    return backup_name


def _reset_sequences(schema_editor, model):
    connection = schema_editor.connection
    for sql in connection.ops.sequence_reset_sql(no_style(), [model]):
        schema_editor.execute(sql)


def _copy_rows(schema_editor, source_table, target_table, column_mapping):
    if not column_mapping:
        return
    target_columns = ", ".join(_quote(schema_editor, target) for target, _ in column_mapping)
    source_columns = ", ".join(_quote(schema_editor, source) for _, source in column_mapping)
    schema_editor.execute(
        "INSERT INTO {target} ({target_columns}) SELECT {source_columns} FROM {source}".format(
            target=_quote(schema_editor, target_table),
            target_columns=target_columns,
            source_columns=source_columns,
            source=_quote(schema_editor, source_table),
        )
    )


def _drop_table(schema_editor, table_name):
    schema_editor.execute(f"DROP TABLE {_quote(schema_editor, table_name)}")


def _find_missing_target_id(schema_editor, source_table, source_column, target_model):
    target_table = target_model._meta.db_table
    target_pk = target_model._meta.pk.column
    sql = """
        SELECT source.{source_column}
        FROM {source_table} source
        LEFT JOIN {target_table} target
            ON source.{source_column} = target.{target_pk}
        WHERE source.{source_column} IS NOT NULL
          AND target.{target_pk} IS NULL
        LIMIT 1
    """.format(
        source_column=_quote(schema_editor, source_column),
        source_table=_quote(schema_editor, source_table),
        target_table=_quote(schema_editor, target_table),
        target_pk=_quote(schema_editor, target_pk),
    )
    with schema_editor.connection.cursor() as cursor:
        cursor.execute(sql)
        row = cursor.fetchone()
    return row[0] if row else None


def _rebuild_campaign_m2m_table(apps, schema_editor, field_name, target_label):
    campaign_model = apps.get_model("aacampaign", "Campaign")
    campaign_field = campaign_model._meta.get_field(field_name)
    through_model = campaign_field.remote_field.through
    table_name = through_model._meta.db_table

    if not _table_exists(schema_editor, table_name):
        return

    target_model = _get_model(apps, target_label)
    target_table = target_model._meta.db_table

    through_fields = list(through_model._meta.local_concrete_fields)
    pk_field = through_model._meta.pk
    campaign_fk = next(
        field for field in through_fields if getattr(field.remote_field, "model", None) == campaign_model
    )
    target_fk = next(
        field
        for field in through_fields
        if field is not pk_field and field is not campaign_fk
    )

    columns = _table_columns(schema_editor, table_name)
    if target_fk.column in columns and _has_fk_to_table(
        schema_editor, table_name, target_fk.column, target_table
    ):
        return

    legacy_target_columns = [
        column
        for column in columns
        if column.endswith("_id")
        and column not in {campaign_fk.column, target_fk.column}
    ]
    if target_fk.column in columns:
        source_target_column = target_fk.column
    elif len(legacy_target_columns) == 1:
        source_target_column = legacy_target_columns[0]
    else:
        raise RuntimeError(
            f"Could not determine legacy target column for table '{table_name}'."
        )

    missing_id = _find_missing_target_id(
        schema_editor, table_name, source_target_column, target_model
    )
    if missing_id is not None:
        raise RuntimeError(
            f"Cannot switch '{table_name}' to {target_label}: "
            f"ID {missing_id} does not exist in '{target_table}'. "
            "Load django-eveonline-sde data before migrating."
        )

    backup_table = _ensure_backup_table_available(schema_editor, table_name)
    schema_editor.alter_db_table(through_model, table_name, backup_table)
    schema_editor.create_model(through_model)

    backup_columns = set(_table_columns(schema_editor, backup_table))
    copy_mapping = []
    if pk_field.column in backup_columns:
        copy_mapping.append((pk_field.column, pk_field.column))
    copy_mapping.append((campaign_fk.column, campaign_fk.column))
    copy_mapping.append((target_fk.column, source_target_column))
    _copy_rows(schema_editor, backup_table, table_name, copy_mapping)

    _drop_table(schema_editor, backup_table)
    _reset_sequences(schema_editor, through_model)


def _rebuild_campaignkillmail_table(apps, schema_editor):
    killmail_model = apps.get_model("aacampaign", "CampaignKillmail")
    table_name = killmail_model._meta.db_table
    if not _table_exists(schema_editor, table_name):
        return

    solar_system_field = killmail_model._meta.get_field("solar_system")
    target_model = solar_system_field.remote_field.model
    target_table = target_model._meta.db_table

    if _has_fk_to_table(schema_editor, table_name, solar_system_field.column, target_table):
        return

    missing_id = _find_missing_target_id(
        schema_editor, table_name, solar_system_field.column, target_model
    )
    if missing_id is not None:
        raise RuntimeError(
            f"Cannot switch '{table_name}' to {target_table}: "
            f"solar system ID {missing_id} is missing from django-eveonline-sde. "
            "Load django-eveonline-sde data before migrating."
        )

    attackers_field = killmail_model._meta.get_field("attackers")
    through_model = attackers_field.remote_field.through
    through_table = through_model._meta.db_table

    backup_table = _ensure_backup_table_available(schema_editor, table_name)
    backup_through_table = None
    if _table_exists(schema_editor, through_table):
        backup_through_table = _ensure_backup_table_available(schema_editor, through_table)
        schema_editor.alter_db_table(through_model, through_table, backup_through_table)

    schema_editor.alter_db_table(killmail_model, table_name, backup_table)
    schema_editor.create_model(killmail_model)

    backup_columns = set(_table_columns(schema_editor, backup_table))
    killmail_mapping = [
        (field.column, field.column)
        for field in killmail_model._meta.local_concrete_fields
        if field.column in backup_columns
    ]
    _copy_rows(schema_editor, backup_table, table_name, killmail_mapping)

    if backup_through_table:
        if not _table_exists(schema_editor, through_table):
            schema_editor.create_model(through_model)
        through_backup_columns = set(_table_columns(schema_editor, backup_through_table))
        through_mapping = [
            (field.column, field.column)
            for field in through_model._meta.local_concrete_fields
            if field.column in through_backup_columns
        ]
        _copy_rows(schema_editor, backup_through_table, through_table, through_mapping)
        _drop_table(schema_editor, backup_through_table)
        _reset_sequences(schema_editor, through_model)

    _drop_table(schema_editor, backup_table)
    _reset_sequences(schema_editor, killmail_model)


def switch_campaign_tables_to_eve_sde(apps, schema_editor):
    _rebuild_campaign_m2m_table(
        apps, schema_editor, "constellations", EVE_SDE_CONSTELLATION_MODEL
    )
    _rebuild_campaign_m2m_table(
        apps, schema_editor, "regions", EVE_SDE_REGION_MODEL
    )
    _rebuild_campaign_m2m_table(
        apps, schema_editor, "systems", EVE_SDE_SOLAR_SYSTEM_MODEL
    )
    _rebuild_campaignkillmail_table(apps, schema_editor)


class Migration(migrations.Migration):
    atomic = False

    dependencies = [
        ("aacampaign", "0005_campaign_last_run"),
        migrations.swappable_dependency(EVE_SDE_REGION_MODEL),
        migrations.swappable_dependency(EVE_SDE_CONSTELLATION_MODEL),
        migrations.swappable_dependency(EVE_SDE_SOLAR_SYSTEM_MODEL),
    ]

    operations = [
        migrations.RunPython(
            switch_campaign_tables_to_eve_sde, migrations.RunPython.noop
        ),
    ]
