#!/usr/bin/env python3
r"""
Построение очередей бэкапа+выкачивания из СОХРАНЁННЫХ артефактов (без live-запросов).
Вход:
  schema_inventory/freshness_smlog.tsv          — актуальность по tblSMLog (43 БД)
  dirlists/master_files.txt                      — размеры примонтированных БД (sys.master_files)
  dirlists/i21sqldata_listing_with_sizes.txt     — все файлы D:\i21SQLData (вкл. отмонтированные)
  dirlists/backups_latest_only.csv               — 23 свежих .bak уже на диске
Выход:
  schema_inventory/BACKUP_QUEUES.md / .csv
Логика:
  - Активные БД (идёт работа) -> ОЧЕРЕДЬ 1 (свежий BACKUP).
  - Если активная БД МАЛЕНЬКАЯ и новая (2026, мало строк) -> её полный архив ПРЕДЫДУЩЕЙ версии
    в ОЧЕРЕДЬ 2 (готовый .bak = просто скачать; нет .bak, но есть live/отмонтированный предок = бэкап/копия).
"""
import csv
import re
from datetime import datetime
from pathlib import Path

ROOT = Path("/root/ir-assessment/redteam/irelydata")
FRESH = ROOT / "schema_inventory" / "freshness_smlog.tsv"
MASTER = ROOT / "dirlists" / "master_files.txt"
DISK = ROOT / "dirlists" / "i21sqldata_listing_with_sizes.txt"
BAK = ROOT / "dirlists" / "backups_latest_only.csv"
OUT_MD = ROOT / "schema_inventory" / "BACKUP_QUEUES.md"
OUT_CSV = ROOT / "schema_inventory" / "BACKUP_QUEUES.csv"

SERVER_NOW = datetime(2026, 9, 14)

# ---- 1. свежесть ----
fresh = {}  # db -> (last_smlog datetime, rows)
for line in FRESH.read_text().splitlines():
    if line.startswith(("db|", "--|", "Warning")) or not line.strip():
        continue
    p = line.split("|")
    if len(p) != 4:
        continue
    db, dt, rows, _ = p
    try:
        d = datetime.strptime(dt, "%Y-%m-%d %H:%M:%S")
    except ValueError:
        d = None
    fresh[db] = (d, int(rows) if rows.isdigit() else 0)

# ---- 2. размеры примонтированных БД (ROWS+LOG и ROWS) ----
import ntpath
mnt_rows = {}
mnt_total = {}
for line in MASTER.read_text().splitlines():
    p = line.strip().split("|")
    if len(p) != 4:
        continue
    db, typ, path, sz = p[0], p[1], p[2], float(p[3])
    mnt_total[db] = mnt_total.get(db, 0.0) + sz
    if typ == "ROWS":
        mnt_rows[db] = mnt_rows.get(db, 0.0) + sz

# ---- 3. ВСЕ файлы на диске (для отмонтированных предков) ----
pat = re.compile(r"^\d{2}/\d{2}/\d{4}\s+\d{2}:\d{2}\s+[AP]M\s+([\d,]+)\s+(.+?)\s*$")
disk_files = {}  # name_lower -> (name, bytes)
for line in DISK.read_text(encoding="utf-8", errors="replace").splitlines():
    m = pat.match(line.strip())
    if m:
        size, name = m.groups()
        disk_files[name.lower()] = (name, int(size.replace(",", "")))

# отмонтированные БД = mdf без соответствующей примонтированной БД
import ntpath
mounted_files = set()
for line in MASTER.read_text().splitlines():
    p = line.strip().split("|")
    if len(p) == 4:
        mounted_files.add(ntpath.basename(p[2]).lower())

# ---- 4. готовые .bak ----
baks = []  # (db_normalized, filename, date, gb)
with BAK.open() as f:
    for r in csv.DictReader(f):
        baks.append((r["latest_filename"], r["date_column"], float(r["size_gb"])))

def bak_norm(fn):
    s = fn.rpartition(".")[0]
    if s.startswith("Jenkins4_"):
        s = s[len("Jenkins4_"):]
    s = re.sub(r"(_backup)?_\d{6,8}$", "", s)
    return s

bak_by_db = {}
for fn, dt, gb in baks:
    bak_by_db[bak_norm(fn).upper()] = (fn, dt, gb)

# ---- 5. карта миграции: активная новая БД -> предок (семейство клиента) ----
# Нормализуем имя клиента: снять 4-значный префикс, UAP\d*, хвосты
def family(db):
    s = db.upper()
    s = re.sub(r"^\d{4}", "", s)
    s = re.sub(r"UAP.*$", "", s)
    s = re.sub(r"\d+$", "", s)
    return s

# Явные алиасы (имена не всегда совпадают идеально)
ALIAS = {
    "JOHNSONPETROLEUM": "JOHNSONPETRO",
    "PAMDALE": "PALMDALE",
    "PAMDALEOIL": "PALMDALEOIL",
    "BERRYOIL": "BERRYOIL",
}
def fam2(db):
    f = family(db)
    return ALIAS.get(f, f)

# кандидаты-предки: все БД (примонтированные + отмонтированные имена файлов) того же семейства
def predecessors(active_db):
    fam = fam2(active_db)
    cands = []
    # примонтированные того же семейства, не равные самой активной
    for db in mnt_total:
        if db == active_db or db.endswith("cfg") or db in ("master", "model", "msdb", "tempdb"):
            continue
        if fam2(db) == fam:
            cands.append(("LIVE", db, mnt_total.get(db, 0.0)))
    # отмонтированные mdf того же семейства
    for name_l, (name, sz) in disk_files.items():
        if not name_l.endswith(".mdf"):
            continue
        if name_l in mounted_files:
            continue
        stem = re.sub(r"\.mdf$", "", name, flags=re.I)
        stem = re.sub(r"_Primary$", "", stem, flags=re.I)
        if fam2(stem) == fam:
            # суммарный размер = mdf + парный ldf
            ldf_sz = 0
            for ln, (lname, lsz) in disk_files.items():
                if ln.endswith(".ldf") and fam2(re.sub(r"(_log|_Primary)?\d*$", "", re.sub(r"\.ldf$", "", lname, flags=re.I))) == fam:
                    ldf_sz += lsz
            cands.append(("OFFLINE_MDF", stem, (sz + ldf_sz) / 1024**3))
    # готовые .bak того же семейства
    for key, (fn, dt, gb) in bak_by_db.items():
        if fam2(key) == fam and key != active_db.upper():
            cands.append(("BAK", key, gb))
    return cands

# ---- классификация активности ----
def tier(db):
    d = fresh.get(db, (None, 0))[0]
    if not d:
        return None, None
    age = (SERVER_NOW - d).days
    if age <= 7:
        return "HOT", age
    if age <= 21:
        return "WARM", age
    if age <= 60:
        return "COOL", age
    return "COLD", age

# порог "маленькая БД": новый инстанс 2026 + мало данных/строк
SMALL_GB = 25.0

active = []  # (db, tier, age, last, rows, total_gb)
for db in fresh:
    t, age = tier(db)
    if t in ("HOT", "WARM", "COOL"):
        active.append((db, t, age, fresh[db][0], fresh[db][1], mnt_total.get(db, 0.0)))
active.sort(key=lambda x: x[3], reverse=True)

print("=" * 96)
print("ОЧЕРЕДЬ 1 — АКТИВНЫЕ БД (идёт работа) → свежий BACKUP DATABASE")
print("=" * 96)
print(f"{'БД':<30}{'акт.':<6}{'послед.лог':<12}{'строк SMLog':>12}{'размер GB':>11}  малая?")
q1_total = 0.0
for db, t, age, last, rows, gb in active:
    small = "ДА→Q2" if (gb <= SMALL_GB and last.year >= 2026) else ""
    q1_total += gb
    print(f"{db:<30}{t:<6}{last.strftime('%Y-%m-%d'):<12}{rows:>12,}{gb:>11.2f}  {small}")
print(f"\nИТОГО очередь 1: {len(active)} БД, ~{q1_total:.1f} GB данных (без сжатия)")

print("\n" + "=" * 96)
print("ОЧЕРЕДЬ 2 — для МАЛЫХ активных БД: полный архив ПРЕДЫДУЩЕЙ версии")
print("=" * 96)
q2 = []
seen_pred = set()
for db, t, age, last, rows, gb in active:
    if not (gb <= SMALL_GB and last.year >= 2026):
        continue
    preds = predecessors(db)
    # выбираем самый полный источник-предок
    preds.sort(key=lambda x: -x[2])
    print(f"\n  {db} ({gb:.2f} GB, создана {last.year}, {rows} строк) — предки:")
    if not preds:
        print("      (предок не найден по имени — требуется ручная сверка)")
        continue
    for kind, name, sz in preds:
        dup = " [дубль уже в Q2]" if (kind, name.upper()) in seen_pred else ""
        print(f"      {kind:<11} {name:<34} {sz:>8.1f} GB{dup}")
        if (kind, name.upper()) not in seen_pred:
            q2.append((db, kind, name, sz))
            seen_pred.add((kind, name.upper()))

# группируем Q2 по типу источника
q2_bak = [x for x in q2 if x[1] == "BAK"]
q2_live = [x for x in q2 if x[1] == "LIVE"]
q2_off = [x for x in q2 if x[1] == "OFFLINE_MDF"]
print(f"\n  → готовых .bak (просто скачать): {len(q2_bak)}, ~{sum(x[3] for x in q2_bak):.1f} GB")
print(f"  → live-предок (нужен BACKUP): {len(q2_live)}, ~{sum(x[3] for x in q2_live):.1f} GB")
print(f"  → отмонтированный MDF (копия файла): {len(q2_off)}, ~{sum(x[3] for x in q2_off):.1f} GB")

# ---- CSV ----
with OUT_CSV.open("w", newline="") as f:
    w = csv.writer(f)
    w.writerow(["queue", "db_active", "activity_tier", "last_smlog", "smlog_rows",
                "source_kind", "source_name", "size_gb", "action"])
    for db, t, age, last, rows, gb in active:
        small = gb <= SMALL_GB and last.year >= 2026
        w.writerow(["Q1", db, t, last.strftime("%Y-%m-%d"), rows, "LIVE", db, f"{gb:.2f}",
                    "BACKUP DATABASE (COMPRESSION, COPY_ONLY)"])
    for db, kind, name, sz in q2:
        action = {"BAK": "download existing .bak", "LIVE": "BACKUP DATABASE predka",
                  "OFFLINE_MDF": "copy .mdf+.ldf file"}[kind]
        w.writerow(["Q2", db, "", "", "", kind, name, f"{sz:.1f}", action])

print(f"\nСохранено: {OUT_CSV}")
