#!/usr/bin/env python3
"""
AutoLuxe Atelier · copie de siguranță automată (rulată din cron, zilnic).

Ce face:
  1. copiază baza SQLite cu API-ul oficial de backup (sigur cât timp aplicația rulează),
  2. verifică integritatea copiei (PRAGMA integrity_check),
  3. o comprimă: backups/db/autoluxe-AAAALLZZ-HHMM.sqlite.gz,
  4. arhivează fotografiile și semnăturile noi față de copia anterioară: backups/uploads/uploads-AAAALLZZ.tar.gz,
  5. păstrează ultimele N zile de copii zilnice + prima copie din fiecare lună (12 luni),
  6. curăță jurnalele de limitare a încercărilor mai vechi de o zi.

Rulare manuală:
  python3 tools/backup.py --data /home/cont/autoluxe/data
Cron (în fiecare noapte la 03:17):
  17 3 * * * /usr/bin/python3 /home/cont/autoluxe/tools/backup.py --data /home/cont/autoluxe/data >> /home/cont/autoluxe/data/logs/backup.log 2>&1

IMPORTANT: copiile din același server nu protejează la pierderea serverului.
Sincronizați dosarul backups/ în altă locație (alt server, stocare S3, NAS), de ex. cu rclone sau rsync.
"""
import argparse
import datetime as dt
import gzip
import os
import shutil
import sqlite3
import sys
import tarfile
import tempfile


def log(msg):
    print(f"[{dt.datetime.now():%Y-%m-%d %H:%M:%S}] {msg}", flush=True)


def backup_db(data, dest_dir):
    src = os.path.join(data, "autoluxe.sqlite")
    if not os.path.isfile(src):
        raise SystemExit(f"Nu găsesc baza de date: {src}")
    os.makedirs(dest_dir, mode=0o750, exist_ok=True)
    stamp = dt.datetime.now().strftime("%Y%m%d-%H%M")
    with tempfile.NamedTemporaryFile(dir=dest_dir, suffix=".sqlite", delete=False) as t:
        tmp = t.name
    try:
        s = sqlite3.connect(f"file:{src}?mode=ro", uri=True, timeout=30)
        d = sqlite3.connect(tmp)
        s.backup(d)
        s.close()
        ok = d.execute("PRAGMA integrity_check").fetchone()[0]
        d.close()
        if ok != "ok":
            raise RuntimeError(f"Verificarea integrității a eșuat: {ok}")
        out = os.path.join(dest_dir, f"autoluxe-{stamp}.sqlite.gz")
        with open(tmp, "rb") as fi, gzip.open(out, "wb", compresslevel=6) as fo:
            shutil.copyfileobj(fi, fo)
        os.chmod(out, 0o640)
        log(f"Baza de date: {out} ({os.path.getsize(out) // 1024} KB)")
        return out
    finally:
        if os.path.exists(tmp):
            os.unlink(tmp)


def backup_uploads(data, dest_dir):
    up = os.path.join(data, "uploads")
    if not os.path.isdir(up):
        return None
    os.makedirs(dest_dir, mode=0o750, exist_ok=True)
    marker = os.path.join(dest_dir, ".last")
    since = os.path.getmtime(marker) if os.path.exists(marker) else 0
    files = []
    for root, _, names in os.walk(up):
        for n in names:
            p = os.path.join(root, n)
            if n != ".htaccess" and os.path.getmtime(p) > since:
                files.append(p)
    if not files:
        log("Fotografii: nimic nou.")
        return None
    out = os.path.join(dest_dir, f"uploads-{dt.datetime.now():%Y%m%d-%H%M}.tar.gz")
    with tarfile.open(out, "w:gz") as tar:
        for p in files:
            tar.add(p, arcname=os.path.relpath(p, data))
    os.chmod(out, 0o640)
    with open(marker, "w") as f:
        f.write(dt.datetime.now().isoformat())
    log(f"Fotografii: {len(files)} fișiere noi în {out}")
    return out


def rotate(dest_dir, keep_days, keep_months):
    """Păstrează ultimele keep_days zile + prima copie din fiecare din ultimele keep_months luni."""
    items = sorted(f for f in os.listdir(dest_dir) if f.startswith("autoluxe-") and f.endswith(".sqlite.gz"))
    today = dt.date.today()
    keep = set()
    months = {}
    for f in items:
        try:
            d = dt.datetime.strptime(f[9:17], "%Y%m%d").date()
        except ValueError:
            continue
        if (today - d).days < keep_days:
            keep.add(f)
        months.setdefault((d.year, d.month), f)
    for (y, m), f in sorted(months.items())[-keep_months:]:
        keep.add(f)
    for f in items:
        if f not in keep:
            os.unlink(os.path.join(dest_dir, f))
            log(f"Șters (rotație): {f}")


def cleanup(data):
    db = sqlite3.connect(os.path.join(data, "autoluxe.sqlite"), timeout=30)
    cut = int(dt.datetime.now().timestamp()) - 86400
    db.execute("DELETE FROM rate_hits WHERE at < ?", (cut,))
    db.execute("DELETE FROM login_attempts WHERE at < ?", (cut,))
    db.commit()
    db.close()


def main():
    ap = argparse.ArgumentParser(description="Copie de siguranță AutoLuxe Atelier")
    ap.add_argument("--data", required=True, help="dosarul data/ al aplicației")
    ap.add_argument("--out", help="unde se pun copiile (implicit data/backups)")
    ap.add_argument("--keep-days", type=int, default=30)
    ap.add_argument("--keep-months", type=int, default=12)
    ap.add_argument("--no-uploads", action="store_true", help="doar baza de date")
    a = ap.parse_args()
    data = os.path.abspath(a.data)
    out = os.path.abspath(a.out or os.path.join(data, "backups"))
    try:
        backup_db(data, os.path.join(out, "db"))
        if not a.no_uploads:
            backup_uploads(data, os.path.join(out, "uploads"))
        rotate(os.path.join(out, "db"), a.keep_days, a.keep_months)
        cleanup(data)
        log("Gata.")
    except Exception as e:  # noqa: BLE001 — orice eroare trebuie să apară în jurnalul cron
        log(f"EROARE: {e}")
        sys.exit(1)


if __name__ == "__main__":
    main()
