"""
One-shot migration: switch SREBOT storage to use clan_id (numeric squadron UID)
as the canonical identifier across DB tables and preference JSON files.

Goals
-----
1. Add `clan_id` to historical tables (player_games_hist, match_summary).
2. Rebuild points.db tables (profile_member_points, profile_totals,
   game_cache) so their PKs include `clan_id` instead of squadron text.
3. Rebuild wl.db tables to use clan_id (currently empty so trivial).
4. Add `squadron_name_history` table to squadrons.db (for old-name → clan_id
   redirects when a squadron renames). Seed with current squadrons_data.
5. Re-key every PREFERENCES/<guild_id>-preferences.json so squadron entries
   are keyed by `str(clan_id)` instead of long_name. Special wildcard keys
   ("Global", "everything", "all", "*") are preserved.
6. Backups: write a tarball of STORAGE/ before any change. Each preferences
   file gets copied to PREFERENCES_BACKUP_<timestamp>/.

Run with `--dry-run` first. The script prints exactly what it would do.

Usage
-----
    source ../SHARED/.venv/bin/activate
    python scripts/migrate_clan_id.py --dry-run
    python scripts/migrate_clan_id.py --apply
"""

from __future__ import annotations

import argparse
import json
import logging
import os
import shutil
import sqlite3
import sys
import tarfile
import time
from pathlib import Path
from typing import Any, Optional

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
)
log = logging.getLogger("migrate_clan_id")


# Auto-load the repo's .env so STORAGE_VOL_PATH and friends resolve when
# the script is run directly (PM2/services already get them from .env).
try:
    from dotenv import load_dotenv

    _here = Path(__file__).resolve()
    for candidate in (_here.parent / ".env", _here.parent.parent / ".env"):
        if candidate.exists():
            load_dotenv(candidate)
            break
except ImportError:
    pass

_storage_env = os.environ.get("STORAGE_VOL_PATH", "").strip()
if not _storage_env:
    raise RuntimeError("STORAGE_VOL_PATH must be set")
STORAGE_DIR = Path(_storage_env)

SQ_BATTLES_DB = STORAGE_DIR / "sq_battles.db"
SQUADRONS_DB = STORAGE_DIR / "squadrons.db"
POINTS_DB = STORAGE_DIR / "points.db"
WL_DB = STORAGE_DIR / "wl.db"
PREFS_DIR = STORAGE_DIR / "PREFERENCES"

WILDCARD_KEYS = {"global", "everything", "all", "*"}
PGH_BATCH = 50_000


def backup_storage() -> Path:
    ts = time.strftime("%Y%m%d-%H%M%S")
    backup_path = STORAGE_DIR.parent / f"STORAGE_BACKUP_{ts}.tar.gz"
    log.info("Creating tarball backup at %s (this may take a while)...", backup_path)
    with tarfile.open(backup_path, "w:gz") as tar:
        for db in (SQ_BATTLES_DB, SQUADRONS_DB, POINTS_DB, WL_DB):
            if db.exists():
                tar.add(db, arcname=db.name)
        if PREFS_DIR.exists():
            tar.add(PREFS_DIR, arcname=PREFS_DIR.name)
    log.info("Backup complete: %s", backup_path)
    return backup_path


def load_squadron_index() -> tuple[dict[str, int], dict[str, int], dict[int, dict[str, Any]]]:
    """Return (long_name_lower -> clan_id, short_name_lower -> clan_id, clan_id -> row)."""
    long_to_id: dict[str, int] = {}
    short_to_id: dict[str, int] = {}
    by_id: dict[int, dict[str, Any]] = {}
    con = sqlite3.connect(SQUADRONS_DB)
    try:
        con.row_factory = sqlite3.Row
        for row in con.execute(
            "SELECT clan_id, long_name, short_name, tag_name FROM squadrons_data"
        ):
            cid = int(row["clan_id"])
            by_id[cid] = dict(row)
            if row["long_name"]:
                long_to_id[row["long_name"].lower()] = cid
            if row["short_name"]:
                short_to_id[row["short_name"].lower()] = cid
    finally:
        con.close()
    log.info("Loaded %d squadrons from squadrons_data", len(by_id))
    return long_to_id, short_to_id, by_id


def ensure_squadron_name_history(apply: bool, by_id: dict[int, dict[str, Any]]) -> None:
    """Create squadron_name_history table and seed from current squadrons_data.

    Schema:
      clan_id     INTEGER NOT NULL
      long_name   TEXT NOT NULL
      first_seen  INTEGER NOT NULL  (unix)
      last_seen   INTEGER NOT NULL  (unix)
      PRIMARY KEY (clan_id, long_name)
    """
    con = sqlite3.connect(SQUADRONS_DB)
    try:
        existing = con.execute(
            "SELECT name FROM sqlite_master WHERE type='table' AND name='squadron_name_history'"
        ).fetchone()
        if existing:
            log.info("squadron_name_history already exists; skipping create")
        else:
            log.info("Creating squadron_name_history table")
            if apply:
                con.executescript(
                    """
                    CREATE TABLE squadron_name_history (
                        clan_id    INTEGER NOT NULL,
                        long_name  TEXT NOT NULL,
                        first_seen INTEGER NOT NULL,
                        last_seen  INTEGER NOT NULL,
                        PRIMARY KEY (clan_id, long_name)
                    );
                    CREATE INDEX idx_snh_long_name ON squadron_name_history(long_name COLLATE NOCASE);
                    CREATE INDEX idx_snh_clan_id ON squadron_name_history(clan_id);
                    """
                )

        now = int(time.time())
        rows = [
            (cid, row["long_name"], now, now)
            for cid, row in by_id.items()
            if row.get("long_name")
        ]
        log.info("Seeding squadron_name_history with %d (clan_id, long_name) pairs", len(rows))
        if apply and rows:
            con.executemany(
                """
                INSERT INTO squadron_name_history (clan_id, long_name, first_seen, last_seen)
                VALUES (?, ?, ?, ?)
                ON CONFLICT(clan_id, long_name) DO UPDATE SET last_seen = excluded.last_seen
                """,
                rows,
            )
            con.commit()
    finally:
        con.close()


def add_squadrons_points_index(apply: bool) -> None:
    con = sqlite3.connect(SQUADRONS_DB)
    try:
        log.info("Adding clan_id index on squadrons_points")
        if apply:
            con.executescript(
                """
                CREATE INDEX IF NOT EXISTS idx_squadrons_points_clanid_time
                  ON squadrons_points(clan_id, unix_time);
                """
            )
            con.commit()
    finally:
        con.close()


def migrate_player_games_hist(
    apply: bool, long_to_id: dict[str, int]
) -> None:
    """Add clan_id INTEGER column + backfill via long_name lookup. Add index."""
    con = sqlite3.connect(SQ_BATTLES_DB)
    try:
        con.execute("PRAGMA journal_mode=WAL")
        cols = [r[1] for r in con.execute("PRAGMA table_info(player_games_hist)").fetchall()]
        if "clan_id" not in cols:
            log.info("Adding clan_id column to player_games_hist")
            if apply:
                con.execute("ALTER TABLE player_games_hist ADD COLUMN clan_id INTEGER")
                con.commit()
        else:
            log.info("player_games_hist.clan_id already present")

        # Refresh after the conditional ALTER.
        cols = [r[1] for r in con.execute("PRAGMA table_info(player_games_hist)").fetchall()]
        total = con.execute("SELECT COUNT(*) FROM player_games_hist").fetchone()[0]
        if "clan_id" in cols:
            unbackfilled = con.execute(
                "SELECT COUNT(*) FROM player_games_hist WHERE clan_id IS NULL"
            ).fetchone()[0]
        else:
            unbackfilled = total
        log.info(
            "player_games_hist: %d total rows, %d need backfill",
            total,
            unbackfilled,
        )

        if apply and unbackfilled:
            # Faster path: pre-build a name → clan_id map in Python and run one
            # indexed UPDATE per distinct squadron_name. Each UPDATE hits the
            # idx_pgh_squadron_name index. ~1500 small ops vs one giant
            # correlated subquery. squadron_name is the WT short_name (after
            # alphanum strip), but very rarely a long_name leaks through, so
            # we accept matches against either.
            log.info("Building name → clan_id map in Python...")
            sq_con = sqlite3.connect(f"file:{SQUADRONS_DB}?mode=ro", uri=True)
            try:
                sq_con.row_factory = sqlite3.Row
                name_to_id: dict[str, int] = {}
                for row in sq_con.execute(
                    "SELECT clan_id, long_name, short_name FROM squadrons_data"
                ):
                    cid = int(row["clan_id"])
                    if row["long_name"]:
                        name_to_id.setdefault(row["long_name"].lower(), cid)
                    if row["short_name"]:
                        name_to_id.setdefault(row["short_name"].lower(), cid)
            finally:
                sq_con.close()

            log.info("Fetching distinct squadron_name values from player_games_hist...")
            distinct_names = [
                r[0] for r in con.execute(
                    "SELECT DISTINCT squadron_name FROM player_games_hist WHERE clan_id IS NULL"
                ).fetchall()
                if r[0]
            ]
            log.info("Distinct unbackfilled squadron_names: %d", len(distinct_names))

            updates = [
                (name_to_id[n.lower()], n)
                for n in distinct_names
                if n.lower() in name_to_id
            ]
            log.info("Will UPDATE %d distinct names that resolve to clan_ids", len(updates))
            if updates:
                cur = con.executemany(
                    "UPDATE player_games_hist SET clan_id = ? "
                    "WHERE squadron_name = ? AND clan_id IS NULL",
                    updates,
                )
                con.commit()
                log.info("Backfill: %s row-updates committed", cur.rowcount)

        cols = [r[1] for r in con.execute("PRAGMA table_info(player_games_hist)").fetchall()]
        if "clan_id" in cols:
            still_null = con.execute(
                "SELECT COUNT(*) FROM player_games_hist WHERE clan_id IS NULL"
            ).fetchone()[0]
            log.info("player_games_hist orphans (clan_id NULL after backfill): %d", still_null)
        else:
            log.info("player_games_hist orphans: n/a (column absent in dry-run)")

        log.info("Ensuring index on player_games_hist(clan_id, endtime_unix)")
        if apply:
            con.execute(
                "CREATE INDEX IF NOT EXISTS idx_pgh_clanid_endtime "
                "ON player_games_hist(clan_id, endtime_unix)"
            )
            con.commit()
    finally:
        con.close()


def migrate_match_summary(apply: bool) -> None:
    """Add winning_clan_id + losing_clan_id, backfill via squadrons_data."""
    con = sqlite3.connect(SQ_BATTLES_DB)
    try:
        con.execute("PRAGMA journal_mode=WAL")
        cols = [r[1] for r in con.execute("PRAGMA table_info(match_summary)").fetchall()]
        for new_col in ("winning_clan_id", "losing_clan_id"):
            if new_col not in cols:
                log.info("Adding %s column to match_summary", new_col)
                if apply:
                    con.execute(f"ALTER TABLE match_summary ADD COLUMN {new_col} INTEGER")
                    con.commit()
            else:
                log.info("match_summary.%s already present", new_col)

        # Refresh after potential ALTERs.
        cols = [r[1] for r in con.execute("PRAGMA table_info(match_summary)").fetchall()]
        total = con.execute("SELECT COUNT(*) FROM match_summary").fetchone()[0]
        if "winning_clan_id" in cols and "losing_clan_id" in cols:
            unbackfilled = con.execute(
                "SELECT COUNT(*) FROM match_summary "
                "WHERE winning_clan_id IS NULL OR losing_clan_id IS NULL"
            ).fetchone()[0]
        else:
            # Dry-run path: columns don't exist yet, so every row is "to be backfilled".
            unbackfilled = total
        log.info(
            "match_summary: %d total rows, %d need backfill",
            total,
            unbackfilled,
        )

        if apply and unbackfilled:
            con.execute(f"ATTACH DATABASE '{SQUADRONS_DB}' AS sq")
            try:
                # winning_sq / losing_sq can be either short_name or tag_name
                # depending on replay metadata. Try short_name first since that's
                # what the autologger writes most often.
                for col_in, col_out in (
                    ("winning_sq", "winning_clan_id"),
                    ("losing_sq", "losing_clan_id"),
                ):
                    log.info("Backfilling %s ← %s via short_name then long_name", col_out, col_in)
                    con.execute(
                        f"""
                        UPDATE match_summary
                        SET {col_out} = (
                            SELECT clan_id FROM sq.squadrons_data
                            WHERE LOWER(squadrons_data.short_name) = LOWER(match_summary.{col_in})
                            LIMIT 1
                        )
                        WHERE {col_out} IS NULL AND {col_in} IS NOT NULL
                        """
                    )
                    con.execute(
                        f"""
                        UPDATE match_summary
                        SET {col_out} = (
                            SELECT clan_id FROM sq.squadrons_data
                            WHERE LOWER(squadrons_data.long_name) = LOWER(match_summary.{col_in})
                            LIMIT 1
                        )
                        WHERE {col_out} IS NULL AND {col_in} IS NOT NULL
                        """
                    )
                con.commit()
            finally:
                con.execute("DETACH DATABASE sq")

        cols = [r[1] for r in con.execute("PRAGMA table_info(match_summary)").fetchall()]
        for col, label in (("winning_clan_id", "winning"), ("losing_clan_id", "losing")):
            if col not in cols:
                log.info("match_summary orphans (%s NULL): n/a (column absent in dry-run)", label)
                continue
            n = con.execute(
                f"SELECT COUNT(*) FROM match_summary WHERE {col} IS NULL"
            ).fetchone()[0]
            log.info("match_summary orphans (%s NULL): %d", label, n)

        log.info("Ensuring indexes on match_summary clan_id columns")
        if apply:
            con.execute(
                "CREATE INDEX IF NOT EXISTS idx_ms_winning_clanid ON match_summary(winning_clan_id)"
            )
            con.execute(
                "CREATE INDEX IF NOT EXISTS idx_ms_losing_clanid ON match_summary(losing_clan_id)"
            )
            con.commit()
    finally:
        con.close()


def _resolve_squadron_to_clan_id(name: Optional[str], name_to_id: dict[str, int]) -> int:
    if not name:
        return -1
    return name_to_id.get(name.lower(), -1)


def rebuild_points_db(apply: bool, long_to_id: dict[str, int]) -> None:
    """Rebuild profile_member_points, profile_totals, game_cache with clan_id columns.

    The squadron text column is preserved for reference but the new tables key
    off clan_id for primary keys. Resolution happens in Python so we don't have
    to ATTACH squadrons.db (avoids the cross-db lock issue we hit earlier).
    """
    # Build name → clan_id map once (long_name AND short_name).
    sq_con = sqlite3.connect(f"file:{SQUADRONS_DB}?mode=ro", uri=True)
    try:
        sq_con.row_factory = sqlite3.Row
        name_to_id: dict[str, int] = {}
        for row in sq_con.execute(
            "SELECT clan_id, long_name, short_name FROM squadrons_data"
        ):
            cid = int(row["clan_id"])
            if row["long_name"]:
                name_to_id.setdefault(row["long_name"].lower(), cid)
            if row["short_name"]:
                name_to_id.setdefault(row["short_name"].lower(), cid)
    finally:
        sq_con.close()

    con = sqlite3.connect(POINTS_DB)
    try:
        con.execute("PRAGMA journal_mode=WAL")

        # profile_member_points -------------------------------------------------
        cols = [r[1] for r in con.execute("PRAGMA table_info(profile_member_points)").fetchall()]
        if "clan_id" in cols:
            log.info("profile_member_points already migrated; skipping rebuild")
        else:
            log.info("Rebuilding profile_member_points with clan_id PK")
            if apply:
                # Idempotency: a previous half-finished run may have left _new behind.
                con.execute("DROP TABLE IF EXISTS profile_member_points_new")
                con.execute(
                    """CREATE TABLE profile_member_points_new (
                        clan_id  INTEGER NOT NULL,
                        squadron TEXT NOT NULL,
                        uid      TEXT NOT NULL,
                        points   INTEGER NOT NULL,
                        PRIMARY KEY (clan_id, uid)
                    )"""
                )
                src = con.execute(
                    "SELECT squadron, uid, points FROM profile_member_points"
                ).fetchall()
                rows = [
                    (_resolve_squadron_to_clan_id(s, name_to_id), s, u, p)
                    for (s, u, p) in src
                ]
                con.executemany(
                    "INSERT OR IGNORE INTO profile_member_points_new "
                    "(clan_id, squadron, uid, points) VALUES (?, ?, ?, ?)",
                    rows,
                )
                con.commit()
                log.info("profile_member_points: copied %d rows", len(rows))
                con.execute("DROP TABLE profile_member_points")
                con.execute("ALTER TABLE profile_member_points_new RENAME TO profile_member_points")
                con.execute("CREATE INDEX IF NOT EXISTS idx_pmp_squadron ON profile_member_points(squadron)")
                con.commit()

        # profile_totals --------------------------------------------------------
        cols = [r[1] for r in con.execute("PRAGMA table_info(profile_totals)").fetchall()]
        if "clan_id" in cols:
            log.info("profile_totals already migrated; skipping rebuild")
        else:
            log.info("Rebuilding profile_totals with clan_id PK")
            if apply:
                con.execute("DROP TABLE IF EXISTS profile_totals_new")
                con.execute(
                    """CREATE TABLE profile_totals_new (
                        clan_id  INTEGER PRIMARY KEY,
                        squadron TEXT NOT NULL,
                        total    INTEGER NOT NULL
                    )"""
                )
                src = con.execute("SELECT squadron, total FROM profile_totals").fetchall()
                rows = [
                    (_resolve_squadron_to_clan_id(s, name_to_id), s, t)
                    for (s, t) in src
                ]
                con.executemany(
                    "INSERT OR IGNORE INTO profile_totals_new (clan_id, squadron, total) VALUES (?, ?, ?)",
                    rows,
                )
                con.commit()
                log.info("profile_totals: copied %d rows", len(rows))
                con.execute("DROP TABLE profile_totals")
                con.execute("ALTER TABLE profile_totals_new RENAME TO profile_totals")
                con.commit()

        # game_cache ------------------------------------------------------------
        cols = [r[1] for r in con.execute("PRAGMA table_info(game_cache)").fetchall()]
        if "clan_id" in cols:
            log.info("game_cache already migrated; skipping rebuild")
        else:
            log.info("Rebuilding game_cache with clan_id in PK")
            if apply:
                con.execute("DROP TABLE IF EXISTS game_cache_new")
                con.execute(
                    """CREATE TABLE game_cache_new (
                        game_id      TEXT NOT NULL,
                        clan_id      INTEGER NOT NULL,
                        squadron     TEXT NOT NULL,
                        diffs_json   TEXT NOT NULL,
                        diff_total   INTEGER NOT NULL,
                        updated_json TEXT NOT NULL,
                        created_at   REAL NOT NULL DEFAULT (strftime('%s','now')),
                        PRIMARY KEY (game_id, clan_id)
                    )"""
                )
                src = con.execute(
                    "SELECT game_id, squadron, diffs_json, diff_total, updated_json, created_at FROM game_cache"
                ).fetchall()
                rows = [
                    (
                        gid,
                        _resolve_squadron_to_clan_id(s, name_to_id),
                        s,
                        dj,
                        dt,
                        uj,
                        ca,
                    )
                    for (gid, s, dj, dt, uj, ca) in src
                ]
                con.executemany(
                    "INSERT OR IGNORE INTO game_cache_new "
                    "(game_id, clan_id, squadron, diffs_json, diff_total, updated_json, created_at) "
                    "VALUES (?, ?, ?, ?, ?, ?, ?)",
                    rows,
                )
                con.commit()
                log.info("game_cache: copied %d rows", len(rows))
                con.execute("DROP TABLE game_cache")
                con.execute("ALTER TABLE game_cache_new RENAME TO game_cache")
                con.commit()
    finally:
        con.close()


def rebuild_wl_db(apply: bool) -> None:
    """wl_events / wl_standings are empty - swap schemas to clan_id keyed tables."""
    con = sqlite3.connect(WL_DB)
    try:
        # wl_standings
        cols = [r[1] for r in con.execute("PRAGMA table_info(wl_standings)").fetchall()]
        if "clan_id" in cols:
            log.info("wl_standings already migrated")
        else:
            log.info("Recreating wl_standings keyed by clan_id (was empty)")
            if apply:
                con.executescript(
                    """
                    DROP TABLE IF EXISTS wl_standings;
                    CREATE TABLE wl_standings (
                        clan_id  INTEGER PRIMARY KEY,
                        squadron TEXT NOT NULL,
                        wins     INTEGER NOT NULL DEFAULT 0,
                        losses   INTEGER NOT NULL DEFAULT 0
                    );
                    """
                )
                con.commit()

        # wl_events: store winner_clan_id alongside winner text
        cols = [r[1] for r in con.execute("PRAGMA table_info(wl_events)").fetchall()]
        if "winner_clan_id" in cols:
            log.info("wl_events already migrated")
        else:
            log.info("Adding winner_clan_id column to wl_events (table is empty)")
            if apply:
                con.execute("ALTER TABLE wl_events ADD COLUMN winner_clan_id INTEGER")
                con.commit()
    finally:
        con.close()


def migrate_preferences(
    apply: bool,
    long_to_id: dict[str, int],
    short_to_id: dict[str, int],
    by_id: dict[int, dict[str, Any]],
) -> None:
    """Re-key every PREFERENCES/<guild_id>-preferences.json from long_name → str(clan_id).

    Special wildcard keys (Global, everything, all, *) are preserved as-is.
    Each migrated entry gets a `Long` field with the squadron's current
    long_name so display fallback works if squadrons_data is unavailable.
    Original files are copied to PREFERENCES_BACKUP_<timestamp>/.
    """
    if not PREFS_DIR.exists():
        log.warning("PREFERENCES dir missing at %s; skipping prefs migration", PREFS_DIR)
        return

    ts = time.strftime("%Y%m%d-%H%M%S")
    backup_dir = STORAGE_DIR / f"PREFERENCES_BACKUP_{ts}"
    if apply:
        backup_dir.mkdir(parents=True, exist_ok=True)

    pref_files = sorted(PREFS_DIR.glob("*-preferences.json"))
    log.info("Found %d preference files to migrate", len(pref_files))

    migrated_count = 0
    orphan_count = 0
    skipped_count = 0
    files_changed = 0

    for pref_file in pref_files:
        try:
            data = json.loads(pref_file.read_text(encoding="utf-8"))
        except Exception as e:
            log.warning("Could not read %s: %s", pref_file.name, e)
            continue

        if not isinstance(data, dict):
            log.warning("Skipping %s: not an object", pref_file.name)
            continue

        new_data: dict[str, Any] = {}
        any_changes = False

        for key, value in data.items():
            if not isinstance(value, dict):
                new_data[key] = value
                continue

            key_lower = str(key).lower()

            # Preserve wildcards / Global
            if key_lower in WILDCARD_KEYS:
                new_data[key] = value
                continue

            # Already a numeric clan_id (running migration twice)
            if str(key).isdigit():
                new_data[key] = value
                continue

            # Try long_name then short_name
            cid = long_to_id.get(key_lower) or short_to_id.get(key_lower)

            # Maybe entry has a "Short" hint we can use as fallback
            if cid is None and value.get("Short"):
                cid = short_to_id.get(str(value["Short"]).lower())

            if cid is None:
                log.warning(
                    "  [%s] orphan key %r — squadron not found in squadrons_data",
                    pref_file.name,
                    key,
                )
                # Keep the original key so the user doesn't lose their data;
                # a future load_guild_preferences will surface it for cleanup.
                new_data[key] = value
                orphan_count += 1
                continue

            new_key = str(cid)
            row = by_id.get(cid, {})
            merged = dict(value)
            # Preserve display fields - the bot uses them when squadrons_data is stale
            merged.setdefault("Long", row.get("long_name") or key)
            if row.get("short_name"):
                merged["Short"] = row["short_name"]

            if new_key in new_data:
                # Two prefs entries for the same squadron (rare). Merge,
                # preferring values from this iteration.
                existing = new_data[new_key]
                if isinstance(existing, dict):
                    existing.update(merged)
                    new_data[new_key] = existing
                else:
                    new_data[new_key] = merged
            else:
                new_data[new_key] = merged

            if new_key != key:
                any_changes = True
            migrated_count += 1

        if not any_changes:
            skipped_count += 1
            continue

        if apply:
            backup_path = backup_dir / pref_file.name
            shutil.copy2(pref_file, backup_path)
            tmp = pref_file.with_suffix(".json.tmp")
            tmp.write_text(json.dumps(new_data, ensure_ascii=False), encoding="utf-8")
            os.replace(tmp, pref_file)
        files_changed += 1

    log.info(
        "Preferences migration: %d files changed, %d unchanged, %d entries migrated, %d orphans",
        files_changed,
        skipped_count,
        migrated_count,
        orphan_count,
    )
    if apply:
        log.info("Preference backups saved to %s", backup_dir)


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--apply", action="store_true", help="Actually apply changes")
    parser.add_argument(
        "--dry-run",
        action="store_true",
        help="Print what would happen without writing anything (default)",
    )
    parser.add_argument(
        "--skip-backup",
        action="store_true",
        help="Skip the tarball backup step (use only if you already have one)",
    )
    args = parser.parse_args()

    if args.apply and args.dry_run:
        log.error("--apply and --dry-run are mutually exclusive")
        return 2

    apply = args.apply

    log.info("STORAGE_DIR=%s", STORAGE_DIR)
    log.info("apply=%s", apply)

    for db in (SQ_BATTLES_DB, SQUADRONS_DB, POINTS_DB, WL_DB):
        if not db.exists():
            log.error("Required DB missing: %s", db)
            return 1

    if apply and not args.skip_backup:
        backup_storage()

    long_to_id, short_to_id, by_id = load_squadron_index()

    ensure_squadron_name_history(apply, by_id)
    add_squadrons_points_index(apply)
    migrate_player_games_hist(apply, long_to_id)
    migrate_match_summary(apply)
    rebuild_points_db(apply, long_to_id)
    rebuild_wl_db(apply)
    migrate_preferences(apply, long_to_id, short_to_id, by_id)

    if not apply:
        log.info("DRY RUN complete. Re-run with --apply to actually write changes.")
    else:
        log.info("Migration applied successfully.")
    return 0


if __name__ == "__main__":
    sys.exit(main())
