"""Populate every table not covered by migrate_dimensions or migrate_sales.

Step list (each independent — failure of one does not stop the others):

  STOCK + SUPPLY
    erp_stock_current   ← stock.csv + stock_stores{,_at,_slo}.csv
    supply_master       ← supply_master.csv
    incoming_supply     ← incoming_supply.csv

  PROMO + PRICING
    erp_promo_weeks     ← erp_promo_calendar.csv  (~423K rows; COPY-loaded)
    erp_nc30            ← nc30.csv
    erp_prices          ← sku_prices.csv
    erp_costs           ← sku_costs.csv

  FORECAST + PLANNING
    backtest_results    ← backtest_fa.csv
    factor_history      ← factor_history.csv
    forecasts           ← forecast_log.csv (skipped if missing)

Each step TRUNCATEs its target before INSERT, so re-running this script
gives a clean reload. dim_* tables are NOT truncated — auto-discovered
SKUs / suppliers accumulate across runs (matches migrate_sales behavior).

Notes that came out of reading the source CSVs:

  • stock_stores{,_at,_slo}.csv each have only (sku, on_hand). The
    country/store information is implicit in the FILENAME, not the
    file contents. To preserve the FK to dim_stores we create three
    synthetic aggregate stores — STORES_HR, STORES_AT, STORES_SLO —
    and route each file to the matching synthetic id.

  • supply_master.supplier strings ("09244 ABC NUTRITIONAL LTD") are
    ERP supplier codes, which do NOT match dim_suppliers.code (built
    from proizvodjac names like "Polleo Sport"). Any new supplier in
    this CSV is auto-discovered into dim_suppliers with the full
    string as both code and name (truncated to 64 chars).

  • erp_promo_calendar.csv.is_erp_promo is 1/0 int → BOOLEAN. NaN
    treated as false (this column is a flag and should always carry
    a value; defensiveness only).

  • erp_prices.csv has extra columns (qty_retail/qty_webshop/
    qty_wholesale) not in the schema — dropped.

  • backtest_fa.csv has more columns than backtest_results — kept
    only sku, year, week, forecast, actual, model, channel_mode.

  • forecast_log.csv does not exist on disk yet — step skipped.

Usage from project root:
    python -m db.migrate_remaining
"""
from __future__ import annotations

import io
from pathlib import Path
from typing import Iterable

import pandas as pd
from psycopg2.extras import execute_values

from db.connection import get_connection

PROJECT_ROOT = Path(__file__).resolve().parent.parent
DATA_DIR = PROJECT_ROOT / "data"


# -----------------------------------------------------------------
# Shared helpers
# -----------------------------------------------------------------

def _load_lookup_maps(conn) -> dict:
    out: dict[str, dict] = {}
    with conn.cursor() as cur:
        cur.execute("SELECT id, sku FROM dim_products")
        out["product"] = {r[1]: r[0] for r in cur.fetchall()}
        cur.execute("SELECT id, unit_code FROM dim_stores")
        out["store"] = {r[1]: r[0] for r in cur.fetchall()}
        cur.execute("SELECT id, code FROM dim_suppliers")
        out["supplier"] = {r[1]: r[0] for r in cur.fetchall()}
    return out


def _autodiscover_skus(conn, skus: Iterable[str], maps: dict) -> int:
    """Insert any SKU not already in dim_products. Name is left NULL
    (these source files carry no product name). active=true."""
    unknown = sorted({s for s in skus if s and s not in maps["product"]})
    if not unknown:
        return 0
    rows = [(s, None, True) for s in unknown]
    with conn.cursor() as cur:
        result = execute_values(
            cur,
            "INSERT INTO dim_products (sku, name, active) VALUES %s "
            "RETURNING id, sku",
            rows,
            fetch=True,
        )
    for new_id, sku in result:
        maps["product"][sku] = new_id
    return len(rows)


def _autodiscover_suppliers(conn, codes: Iterable[str], maps: dict) -> int:
    """Insert any supplier string not already in dim_suppliers. Code is
    the full string truncated to 64 chars; name is the full string."""
    unknown_raw = [c for c in codes if c]
    unknown = []
    seen = set()
    for raw in unknown_raw:
        code = raw[:64]
        if code in maps["supplier"] or code in seen:
            continue
        seen.add(code)
        unknown.append((code, raw))
    if not unknown:
        return 0
    rows = [(code, name) for code, name in unknown]
    with conn.cursor() as cur:
        result = execute_values(
            cur,
            "INSERT INTO dim_suppliers (code, name) VALUES %s "
            "RETURNING id, code",
            rows,
            fetch=True,
        )
    for new_id, code in result:
        maps["supplier"][code] = new_id
    return len(rows)


def _ensure_country_stores(conn, maps: dict) -> int:
    """Create the three synthetic country-aggregate stores used by the
    stock_stores{,_at,_slo}.csv files. Idempotent — skips any already
    present."""
    synthetic = [
        ("STORES_HR",  "All HR stores (aggregate)",  "HR"),
        ("STORES_AT",  "All AT stores (aggregate)",  "AT"),
        ("STORES_SLO", "All SLO stores (aggregate)", "SI"),
    ]
    new = [(c, n, ctry, False) for c, n, ctry in synthetic
           if c not in maps["store"]]
    if not new:
        return 0
    with conn.cursor() as cur:
        result = execute_values(
            cur,
            "INSERT INTO dim_stores (unit_code, name, country, is_warehouse) "
            "VALUES %s RETURNING id, unit_code",
            new,
            fetch=True,
        )
    for new_id, code in result:
        maps["store"][code] = new_id
    return len(new)


def _truncate(conn, table: str) -> None:
    with conn.cursor() as cur:
        cur.execute(f"TRUNCATE TABLE {table} RESTART IDENTITY")


def _executemany_insert(conn, table: str, cols: list[str],
                         rows: list[tuple]) -> None:
    """INSERT in batches via psycopg2.extras.execute_values."""
    if not rows:
        return
    placeholders = "(" + ", ".join(["%s"] * len(cols)) + ")"
    sql = (
        f"INSERT INTO {table} ({', '.join(cols)}) VALUES %s"
    )
    with conn.cursor() as cur:
        execute_values(cur, sql, rows, template=placeholders, page_size=2000)


def _copy_df(conn, table: str, df: pd.DataFrame, cols: list[str]) -> None:
    """COPY a DataFrame's rows into `table` (text format)."""
    buf = io.StringIO()
    df[cols].to_csv(buf, index=False, header=False, sep="\t",
                    na_rep=r"\N", date_format="%Y-%m-%d")
    buf.seek(0)
    with conn.cursor() as cur:
        cur.copy_expert(
            f"COPY {table} ({', '.join(cols)}) FROM STDIN WITH (FORMAT text)",
            buf,
        )


# -----------------------------------------------------------------
# Stock + supply
# -----------------------------------------------------------------

def _load_stock_current(conn, maps: dict) -> int:
    """Load stock.csv (warehouse) + stock_stores{,_at,_slo}.csv (per
    country aggregate). UNIQUE(product_id, store_id) is respected by
    deduplicating each file on `sku`."""
    _ensure_country_stores(conn, maps)
    sources = [
        ("stock.csv",           "01"),
        ("stock_stores.csv",    "STORES_HR"),
        ("stock_stores_at.csv", "STORES_AT"),
        ("stock_stores_slo.csv","STORES_SLO"),
    ]
    total = 0
    seen_pairs: set[tuple[int, int]] = set()
    rows: list[tuple] = []
    for fname, store_code in sources:
        fp = DATA_DIR / fname
        if not fp.exists():
            print(f"    [warn] {fname} missing — skipping")
            continue
        store_id = maps["store"].get(store_code)
        if store_id is None:
            print(f"    [warn] store '{store_code}' not in dim_stores — skipping {fname}")
            continue
        df = pd.read_csv(fp, dtype=str, encoding="utf-8")
        df["sku"] = df["sku"].fillna("").str.strip()
        df = df[df["sku"] != ""].drop_duplicates(subset=["sku"], keep="first")
        _autodiscover_skus(conn, df["sku"], maps)
        for _, r in df.iterrows():
            sku = r["sku"]
            pid = maps["product"].get(sku)
            if pid is None:
                continue
            key = (pid, store_id)
            if key in seen_pairs:
                continue
            seen_pairs.add(key)
            try:
                qty = float(r["on_hand"]) if r["on_hand"] not in (None, "", "nan") else None
            except (ValueError, TypeError):
                qty = None
            rows.append((pid, store_id, qty))
        total += len(df)
    _truncate(conn, "erp_stock_current")
    _executemany_insert(
        conn, "erp_stock_current",
        ["product_id", "store_id", "stock_qty"],
        rows,
    )
    return len(rows)


def _load_supply_master(conn, maps: dict) -> int:
    fp = DATA_DIR / "supply_master.csv"
    if not fp.exists():
        print("    [warn] supply_master.csv missing — skipping")
        return 0
    df = pd.read_csv(fp, dtype=str, encoding="utf-8")
    df["sku"] = df["sku"].fillna("").str.strip()
    df["supplier"] = df["supplier"].fillna("").str.strip()
    df = df[df["sku"] != ""]
    _autodiscover_skus(conn, df["sku"], maps)
    _autodiscover_suppliers(conn, df["supplier"], maps)
    rows = []
    for _, r in df.iterrows():
        pid = maps["product"].get(r["sku"])
        if pid is None:
            continue
        sup_code = r["supplier"][:64] if r["supplier"] else None
        sup_id = maps["supplier"].get(sup_code) if sup_code else None
        try:
            ltw = float(r["lead_time_weeks"]) if r["lead_time_weeks"] else None
        except (ValueError, TypeError):
            ltw = None
        try:
            moq = float(r["moq"]) if r["moq"] else None
        except (ValueError, TypeError):
            moq = None
        rows.append((pid, sup_id, ltw, moq))
    _truncate(conn, "supply_master")
    _executemany_insert(
        conn, "supply_master",
        ["product_id", "supplier_id", "lead_time_weeks", "moq"],
        rows,
    )
    return len(rows)


def _load_incoming_supply(conn, maps: dict) -> int:
    fp = DATA_DIR / "incoming_supply.csv"
    if not fp.exists():
        print("    [warn] incoming_supply.csv missing — skipping")
        return 0
    df = pd.read_csv(fp, encoding="utf-8")
    df.columns = [c.strip().lower() for c in df.columns]  # tolerate uppercase
    df["sku"] = df["sku"].astype(str).fillna("").str.strip()
    df = df[df["sku"] != ""]
    _autodiscover_skus(conn, df["sku"], maps)
    rows = []
    for _, r in df.iterrows():
        pid = maps["product"].get(r["sku"])
        if pid is None:
            continue
        rows.append((
            pid,
            int(r["year"]) if pd.notna(r.get("year")) else None,
            int(r["week"]) if pd.notna(r.get("week")) else None,
            float(r["qty"]) if pd.notna(r.get("qty")) else None,
            r.get("status") if "status" in df.columns else None,
        ))
    _truncate(conn, "incoming_supply")
    _executemany_insert(
        conn, "incoming_supply",
        ["product_id", "year", "week", "quantity", "status"],
        rows,
    )
    return len(rows)


# -----------------------------------------------------------------
# Promo + pricing
# -----------------------------------------------------------------

def _load_promo_weeks(conn, maps: dict) -> int:
    """erp_promo_calendar.csv → erp_promo_weeks (~423K rows). Loaded
    via COPY in 50K chunks."""
    fp = DATA_DIR / "erp_promo_calendar.csv"
    if not fp.exists():
        print("    [warn] erp_promo_calendar.csv missing — skipping")
        return 0
    _truncate(conn, "erp_promo_weeks")
    total = 0
    chunk_n = 0
    CHUNK = 50_000
    cols = ["product_id", "year", "week", "promo_types", "is_erp_promo"]
    for chunk in pd.read_csv(fp, dtype={"sku": "string", "promo_types": "string"},
                              chunksize=CHUNK, encoding="utf-8",
                              low_memory=False):
        chunk_n += 1
        chunk["sku"] = chunk["sku"].fillna("").str.strip()
        _autodiscover_skus(conn, chunk["sku"], maps)
        out = pd.DataFrame()
        out["product_id"] = chunk["sku"].map(maps["product"]).astype("Int64")
        out["year"] = pd.to_numeric(chunk["year"], errors="coerce").astype("Int64")
        out["week"] = pd.to_numeric(chunk["week"], errors="coerce").astype("Int64")
        out["promo_types"] = chunk["promo_types"].fillna("").where(
            chunk["promo_types"].notna() & (chunk["promo_types"].fillna("").str.strip() != ""),
            None,
        )
        v = pd.to_numeric(chunk["is_erp_promo"], errors="coerce")
        out["is_erp_promo"] = (v == 1)
        out = out.dropna(subset=["product_id"])
        _copy_df(conn, "erp_promo_weeks", out, cols)
        conn.commit()
        total += len(out)
        print(f"    chunk {chunk_n:>2d}: +{len(out):>6,}  (total: {total:>7,})")
    return total


def _load_nc30(conn, maps: dict) -> int:
    fp = DATA_DIR / "nc30.csv"
    if not fp.exists():
        print("    [warn] nc30.csv missing — skipping")
        return 0
    df = pd.read_csv(fp, dtype={"sku": "string"}, encoding="utf-8")
    df["sku"] = df["sku"].fillna("").str.strip()
    df = df[df["sku"] != ""]
    _autodiscover_skus(conn, df["sku"], maps)
    rows = []
    for _, r in df.iterrows():
        pid = maps["product"].get(r["sku"])
        if pid is None:
            continue
        try:
            price = float(r["nc30_price"]) if pd.notna(r.get("nc30_price")) else None
        except (ValueError, TypeError):
            price = None
        rows.append((pid, price))
    _truncate(conn, "erp_nc30")
    _executemany_insert(conn, "erp_nc30", ["product_id", "nc30_price"], rows)
    return len(rows)


def _load_prices(conn, maps: dict) -> int:
    fp = DATA_DIR / "sku_prices.csv"
    if not fp.exists():
        print("    [warn] sku_prices.csv missing — skipping")
        return 0
    df = pd.read_csv(fp, dtype={"sku": "string"}, encoding="utf-8")
    df["sku"] = df["sku"].fillna("").str.strip()
    df = df[df["sku"] != ""]
    _autodiscover_skus(conn, df["sku"], maps)
    rows = []
    for _, r in df.iterrows():
        pid = maps["product"].get(r["sku"])
        if pid is None:
            continue
        def num(col):
            v = r.get(col)
            try:
                return float(v) if pd.notna(v) else None
            except (ValueError, TypeError):
                return None
        rows.append((
            pid,
            num("avg_sell_price"),
            num("normal_retail_ppp"),
            num("normal_webshop_ppp"),
            int(r["weeks_active"]) if pd.notna(r.get("weeks_active")) else None,
        ))
    _truncate(conn, "erp_prices")
    _executemany_insert(
        conn, "erp_prices",
        ["product_id", "avg_sell_price", "normal_retail_ppp",
         "normal_webshop_ppp", "weeks_active"],
        rows,
    )
    return len(rows)


def _load_costs(conn, maps: dict) -> int:
    fp = DATA_DIR / "sku_costs.csv"
    if not fp.exists():
        print("    [warn] sku_costs.csv missing — skipping")
        return 0
    df = pd.read_csv(fp, dtype={"sku": "string"}, encoding="utf-8")
    df["sku"] = df["sku"].fillna("").str.strip()
    df = df[df["sku"] != ""]
    _autodiscover_skus(conn, df["sku"], maps)
    rows = []
    for _, r in df.iterrows():
        pid = maps["product"].get(r["sku"])
        if pid is None:
            continue
        try:
            cp = float(r["cost_price"]) if pd.notna(r.get("cost_price")) else None
        except (ValueError, TypeError):
            cp = None
        try:
            ruc = float(r["ruc"]) if pd.notna(r.get("ruc")) else None
        except (ValueError, TypeError):
            ruc = None
        rows.append((pid, cp, ruc))
    _truncate(conn, "erp_costs")
    _executemany_insert(
        conn, "erp_costs",
        ["product_id", "cost_price", "ruc"],
        rows,
    )
    return len(rows)


# -----------------------------------------------------------------
# Forecast + planning
# -----------------------------------------------------------------

def _load_backtest(conn, maps: dict) -> int:
    fp = DATA_DIR / "backtest_fa.csv"
    if not fp.exists():
        print("    [warn] backtest_fa.csv missing — skipping")
        return 0
    df = pd.read_csv(fp, dtype={"sku": "string"}, encoding="utf-8")
    df["sku"] = df["sku"].fillna("").str.strip()
    df = df[df["sku"] != ""]
    _autodiscover_skus(conn, df["sku"], maps)
    rows = []
    for _, r in df.iterrows():
        pid = maps["product"].get(r["sku"])
        if pid is None:
            continue
        def num(col):
            v = r.get(col)
            try:
                return float(v) if pd.notna(v) else None
            except (ValueError, TypeError):
                return None
        rows.append((
            pid,
            int(r["year"]) if pd.notna(r.get("year")) else None,
            int(r["week"]) if pd.notna(r.get("week")) else None,
            num("forecast"),
            num("actual"),
            r.get("model"),
            r.get("channel_mode"),
            num("forecast_retail"),
            num("forecast_wholesale"),
            num("actual_retail"),
            num("actual_wholesale"),
            num("ws_share"),
        ))
    _truncate(conn, "backtest_results")
    _executemany_insert(
        conn, "backtest_results",
        ["product_id", "year", "week", "forecast", "actual",
         "model", "channel_mode",
         "forecast_retail", "forecast_wholesale",
         "actual_retail", "actual_wholesale", "ws_share"],
        rows,
    )
    return len(rows)


def _load_factor_history(conn, maps: dict) -> int:
    fp = DATA_DIR / "factor_history.csv"
    if not fp.exists():
        print("    [warn] factor_history.csv missing — skipping")
        return 0
    df = pd.read_csv(fp, dtype={"sku": "string"}, encoding="utf-8")
    df["sku"] = df["sku"].fillna("").str.strip()
    df = df[df["sku"] != ""]
    _autodiscover_skus(conn, df["sku"], maps)
    rows = []
    for _, r in df.iterrows():
        pid = maps["product"].get(r["sku"])
        if pid is None:
            continue
        rd = pd.to_datetime(r.get("run_date"), errors="coerce")
        rows.append((
            pid,
            int(r["run_year"])    if pd.notna(r.get("run_year"))    else None,
            int(r["run_week"])    if pd.notna(r.get("run_week"))    else None,
            int(r["target_year"]) if pd.notna(r.get("target_year")) else None,
            int(r["target_week"]) if pd.notna(r.get("target_week")) else None,
            float(r["factor"])    if pd.notna(r.get("factor"))      else None,
            rd.to_pydatetime() if pd.notna(rd) else None,
        ))
    _truncate(conn, "factor_history")
    _executemany_insert(
        conn, "factor_history",
        ["product_id", "run_year", "run_week", "target_year",
         "target_week", "factor", "run_date"],
        rows,
    )
    return len(rows)


def _load_forecast_log(conn, maps: dict) -> int:
    fp = DATA_DIR / "forecast_log.csv"
    if not fp.exists():
        print("    [info] forecast_log.csv does not exist yet — skipping")
        return 0
    df = pd.read_csv(fp, dtype={"sku": "string"}, encoding="utf-8")
    if df.empty:
        print("    [info] forecast_log.csv is empty — skipping")
        return 0
    df["sku"] = df["sku"].fillna("").str.strip()
    df = df[df["sku"] != ""]

    # CSV is append-only across forecast runs — keep only the LATEST run_id
    # (most recent timestamp). Otherwise downstream aggregates multiply by
    # the number of runs the user has executed since the last clean reload.
    if "run_id" in df.columns:
        latest_run = df["run_id"].astype(str).max()  # ISO timestamps sort lexically
        n_before = len(df)
        df = df[df["run_id"].astype(str) == latest_run]
        print(f"    [info] forecast_log.csv has {df['run_id'].nunique() + 1 - 1} runs; "
              f"keeping latest ({latest_run}) — kept {len(df):,}/{n_before:,} rows")

    _autodiscover_skus(conn, df["sku"], maps)

    # Link the loaded rows to the latest forecast_runs.id so downstream
    # queries that join via run_id (Revenue Forecast page, Demand Planning)
    # actually find these rows. Without this, `forecasts.run_id = NULL`
    # while `forecast_runs.id` increments — a mismatch that silently
    # returns 0 forecast rows on the Revenue Forecast page.
    with conn.cursor() as cur:
        cur.execute("SELECT MAX(id) FROM forecast_runs")
        latest_run_id = cur.fetchone()[0]

    rows = []
    for _, r in df.iterrows():
        pid = maps["product"].get(r["sku"])
        if pid is None:
            continue
        def num(col):
            v = r.get(col)
            try:
                return float(v) if pd.notna(v) else None
            except (ValueError, TypeError):
                return None
        rows.append((
            latest_run_id,                       # link to forecast_runs.MAX(id)
            pid,
            int(r["target_year"]) if pd.notna(r.get("target_year")) else
              (int(r["year"]) if pd.notna(r.get("year")) else None),
            int(r["target_week"]) if pd.notna(r.get("target_week")) else
              (int(r["week"]) if pd.notna(r.get("week")) else None),
            num("baseline"),
            num("on_top_vp"),
            num("on_top_mp"),
            num("promo_uplift"),
            num("planner_factor"),
            num("forecast_total") or num("forecast"),
            r.get("model_used") or r.get("model"),
            r.get("channel_mode"),
            num("forecast_retail"),
            num("forecast_wholesale"),
        ))
    _truncate(conn, "forecasts")
    _executemany_insert(
        conn, "forecasts",
        ["run_id", "product_id", "year", "week", "baseline",
         "on_top_wholesale", "on_top_retail", "promo_uplift",
         "planner_factor", "total", "model_used", "channel_mode",
         "forecast_retail", "forecast_wholesale"],
        rows,
    )
    return len(rows)


# -----------------------------------------------------------------
# Entry point
# -----------------------------------------------------------------

_STEPS = [
    ("erp_stock_current", _load_stock_current),
    ("supply_master",     _load_supply_master),
    ("incoming_supply",   _load_incoming_supply),
    ("erp_promo_weeks",   _load_promo_weeks),
    ("erp_nc30",          _load_nc30),
    ("erp_prices",        _load_prices),
    ("erp_costs",         _load_costs),
    ("backtest_results",  _load_backtest),
    ("factor_history",    _load_factor_history),
    ("forecasts",         _load_forecast_log),
]


def migrate_remaining() -> None:
    print("Connecting to Postgres...")
    conn = get_connection()
    conn.autocommit = False
    print("Loading dimension lookup maps...")
    maps = _load_lookup_maps(conn)
    print(f"  products={len(maps['product']):>5,}  "
          f"stores={len(maps['store']):>3,}  "
          f"suppliers={len(maps['supplier']):>4,}")

    summary: list[tuple[str, str]] = []
    try:
        for label, fn in _STEPS:
            print()
            print(f"=== {label} ===")
            try:
                n = fn(conn, maps)
                conn.commit()
                summary.append((label, f"{n:>9,} rows"))
                print(f"    -> {n:,} rows")
            except Exception as e:
                conn.rollback()
                summary.append((label, f"FAILED: {type(e).__name__}: {e}"))
                print(f"    -> FAILED: {type(e).__name__}: {e}")

        # erp_transactions wasn't touched by this script, but the user asked
        # for the refresh — keep it so v_sales_weekly stays in sync if it
        # was stale.
        print()
        print("Refreshing v_sales_weekly (materialized)...")
        try:
            with conn.cursor() as cur:
                cur.execute("REFRESH MATERIALIZED VIEW v_sales_weekly")
            conn.commit()
            print("    -> refreshed")
        except Exception as e:
            conn.rollback()
            print(f"    -> FAILED: {e}")

        # v_sales_by_store is currently a regular VIEW (not materialized).
        # Check just in case the schema ever changes.
        with conn.cursor() as cur:
            cur.execute("""
                SELECT 1 FROM pg_matviews
                 WHERE schemaname='public' AND matviewname='v_sales_by_store'
            """)
            if cur.fetchone():
                print("Refreshing v_sales_by_store (materialized)...")
                cur.execute("REFRESH MATERIALIZED VIEW v_sales_by_store")
                conn.commit()
                print("    -> refreshed")
    finally:
        # Final counts straight from Postgres
        print()
        print("=== Final row counts ===")
        verify_tables = [
            "erp_stock_current", "supply_master", "incoming_supply",
            "erp_promo_weeks", "erp_nc30", "erp_prices", "erp_costs",
            "backtest_results", "factor_history", "forecasts",
        ]
        with conn.cursor() as cur:
            for t in verify_tables:
                try:
                    cur.execute(f"SELECT COUNT(*) FROM {t}")
                    n = cur.fetchone()[0]
                    print(f"    {t:22s} {n:>9,}")
                except Exception as e:
                    print(f"    {t:22s} ERROR ({e})")

        print()
        print("=== Step summary ===")
        for label, result in summary:
            print(f"    {label:22s} {result}")
        conn.close()


if __name__ == "__main__":
    migrate_remaining()
