#!/usr/bin/env python3
r"""
Сравнение: бэкапы D:\irelyinstall\backup vs живые БД D:\i21SQLData.
Вопрос: есть ли среди бэкапов базы, которых НЕТ в живом списке (сироты = единственные копии).
Обратное: живые БД без бэкапов (информативно).

Источники (операционные данные, уже на диске):
- dirlist_D.txt (dir /s /b D:\ снят с сервера 09/11) -> живые файлы
- backups_latest_only.csv (23 группы бэкапов)
"""
import csv
import difflib
import re
from pathlib import Path

BASE = Path("/root/ir-assessment/redteam/irelydata/dirlists")
DIRLIST_D = BASE / "i21sqldata_listing_with_sizes.txt"
BACKUPS_CSV = BASE / "backups_latest_only.csv"
OUT_MD = BASE / "backups_vs_live_comparison.md"

# ---------- живые БД из D:\i21SQLData (dir-листинг с размерами) ----------
live_files = []          # (name, size)
pat_live = re.compile(r"^\d{2}/\d{2}/\d{4}\s+\d{2}:\d{2}\s+[AP]M\s+([\d,]+)\s+(.+?)\s*$")
for line in DIRLIST_D.read_text(encoding="utf-8", errors="replace").splitlines():
    m = pat_live.match(line.strip())
    if m:
        size, name = m.groups()
        live_files.append((name, int(size.replace(",", ""))))
assert len(live_files) == 188, f"ожидалось 188 файлов, получили {len(live_files)}"

live_size = {n: s for n, s in live_files}
mdf = [n for n, _ in live_files if n.lower().endswith(".mdf")]
ldf_only_bases = set()
mdf_bases = set()
for f in mdf:
    stem = f[:-4]
    stem = re.sub(r"_Primary$", "", stem, flags=re.I)
    mdf_bases.add(stem)
# БД, у которых есть только _log.ldf без mdf (осиротевшие логи)
for f, _ in live_files:
    if f.lower().endswith(".ldf"):
        stem = re.sub(r"(_log|_Primary)?(_\d+)?$", "", f[:-4], flags=re.I)
        stem = re.sub(r"_Primary$", "", stem, flags=re.I)
        if stem not in mdf_bases and not any(b.lower().startswith(stem.lower()) for b in mdf_bases):
            ldf_only_bases.add(stem)

def strip_prefix(name: str) -> str:
    """Снять ведущий 4-значный префикс (2210/2430/2510/2610/2710...)."""
    return re.sub(r"^\d{4}", "", name)

def canon(name: str) -> str:
    """Каноническая форма для сравнения."""
    s = name.lower()
    s = re.sub(r"[^a-z0-9]", "", s)
    s = re.sub(r"^\d{4}", "", s)
    return s

live_exact = {b.lower(): b for b in mdf_bases}
live_canon = {canon(b): b for b in mdf_bases}

# ---------- бэкапы (23 группы) ----------
with BACKUPS_CSV.open() as f:
    backups = list(csv.DictReader(f))

tail_date = re.compile(r"(_backup)?_\d{6,8}$")
def backup_dbname(filename: str) -> str:
    stem = filename.rpartition(".")[0]
    if stem.startswith("Jenkins4_"):
        stem = stem[len("Jenkins4_"):]
    stem = tail_date.sub("", stem)
    return stem

orphans = []       # нет вообще похожего в живых
prefix_match = []  # совпало только после снятия префикса
exact_match = []   # точное совпадение
near_match = []    # fuzzy-похожие (нужно решение оператора)

for row in backups:
    fn = row["latest_filename"]
    db = backup_dbname(fn)
    dl = db.lower()
    dc = canon(db)
    if dl in live_exact:
        exact_match.append((db, fn, row))
    elif dc in live_canon:
        prefix_match.append((db, live_canon[dc], fn, row))
    else:
        close = difflib.get_close_matches(dc, list(live_canon.keys()), n=1, cutoff=0.72)
        if close:
            near_match.append((db, live_canon[close[0]], fn, row))
        else:
            orphans.append((db, fn, row))

# ---------- живые БД без бэкапов ----------
backed_canon = set()
for row in backups:
    backed_canon.add(canon(backup_dbname(row["latest_filename"])))
no_backup = []
for b in sorted(mdf_bases):
    bc = canon(b)
    # учитываем и близкие соответствия (fuzzy), чтобы не завысить список
    if bc in backed_canon:
        continue
    if difflib.get_close_matches(bc, list(backed_canon), n=1, cutoff=0.85):
        continue
    no_backup.append(b)

# размеры живых файлов для no_backup (из dirlist_D нет размеров; берём из paste? нет — только имена)
print("=" * 100)
print("СРАВНЕНИЕ: бэкапы (23 группы) vs живые БД D:\\i21SQLData")
print("=" * 100)

print(f"\nЖивых .mdf баз: {len(mdf_bases)}; бэкап-групп: {len(backups)}")

print(f"\n[A] ТОЧНОЕ совпадение с живой БД ({len(exact_match)}):")
for db, fn, row in exact_match:
    print(f"    {db:<42} <- {fn}")

print(f"\n[B] Совпадение ПОСЛЕ СНЯТИЯ ПРЕФИКСА ({len(prefix_match)}):")
for db, live, fn, row in prefix_match:
    print(f"    backup {db:<38} ~ live {live}")

print(f"\n[C] ПОХОЖИЕ (fuzzy, вероятно другая версия/инстанс той же БД) ({len(near_match)}):")
for db, live, fn, row in near_match:
    print(f"    backup {db:<38} ~ live {live:<38} ({fn})")

print(f"\n[D] СИРОТЫ — бэкапы БЕЗ живой БД (единственная копия данных!) ({len(orphans)}):")
for db, fn, row in orphans:
    print(f"    {fn:<60} {row['date_column']}  {int(row['size_bytes'])/1024**3:.1f} GB")

if ldf_only_bases:
    print(f"\n[E] Осиротевшие _log.ldf без mdf в живых: {sorted(ldf_only_bases)}")

print(f"\n[F] Живые БД БЕЗ бэкапа в D:\\irelyinstall\\backup ({len(no_backup)}) — единственный источник: live MDF/BCP:")
def base_total(b):
    bl = b.lower()
    return sum(s for n, s in live_files
               if n.lower().endswith((".mdf", ".ldf"))
               and (n[:-4].lower().startswith(bl) or re.sub(r"_primary$", "", n[:-4].lower()) == bl))
for b in sorted(no_backup, key=base_total, reverse=True):
    print(f"    {b:<42} {base_total(b)/1024**3:>7.1f} GB")

OUT_MD.write_text("\n".join([
    "# Бэкапы vs живые БД (D:\\irelyinstall\\backup vs D:\\i21SQLData)",
    "",
    f"- Живых .mdf баз: {len(mdf_bases)}",
    f"- Бэкап-групп (последние версии): {len(backups)}",
    f"- Точные совпадения: {len(exact_match)}",
    f"- Совпадения после снятия префикса: {len(prefix_match)}",
    f"- Fuzzy-похожие: {len(near_match)}",
    f"- СИРОТЫ (бэкап = единственная копия): {len(orphans)}",
    f"- Живые БД без бэкапа: {len(no_backup)}",
    "",
    "## [D] Сироты — бэкапы без живой БД (единственные копии, приоритет скачивания)",
    "",
    "| Файл | Дата копии | GB |",
    "|------|-----------|----|",
] + [f"| `{fn}` | {row['date_column']} | {int(row['size_bytes'])/1024**3:.1f} |" for db, fn, row in orphans] + [
    "",
    "## [C] Fuzzy-похожие (вероятно другие версии/инстансы)",
    "",
] + [f"- backup `{db}` ~ live `{live}` ({fn})" for db, live, fn, row in near_match] + [
    "",
    "## [B] Совпадения после снятия 4-значного префикса",
    "",
] + [f"- `{db}` ~ live `{live}`" for db, live, fn, row in prefix_match] + [
    "",
    "## [A] Точные совпадения",
    "",
] + [f"- `{db}`" for db, fn, row in exact_match] + [
    "",
    "## [F] Живые БД без бэкапа (единственный источник — live MDF/BCP-дамп)",
    "",
    "| Живая БД | Суммарный размер файлов, GB |",
    "|----------|------------------------------|",
] + [f"| {b} | {base_total(b)/1024**3:.1f} |" for b in sorted(no_backup, key=base_total, reverse=True)] + [""]))

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