#!/usr/bin/env python3
"""Backup diario de la base de datos histórica (pg_dump -Fc) con rotación.

Corre vía LaunchAgent (com.bia.backup-postgres, 4:00 AM diario). También se
puede correr a mano: python3 backup_postgres.py
"""

from __future__ import annotations

import os
import subprocess
import sys
import time
from datetime import datetime
from pathlib import Path
from urllib.parse import unquote, urlsplit

from app_config import db_config

BASE_DIR = Path(__file__).resolve().parent
BACKUP_DIR = BASE_DIR / "backups" / "postgres"
RETENTION_DAYS = int(os.getenv("BACKUP_RETENTION_DIAS", "30"))
PG_DUMP_BIN = os.getenv("PG_DUMP_BIN", "/opt/homebrew/bin/pg_dump")

# La conexión a la base remota (172.19.0.160) se corta de forma intermitente
# de tanto en tanto (visto varias veces en uso interactivo) — sin reintento,
# eso deja un .dump de 0 bytes que a simple vista parece un backup válido.
MAX_INTENTOS = 3
ESPERA_ENTRE_INTENTOS_SEG = 60


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


def _conn_args() -> tuple[list[str], dict]:
    """Arma los flags de conexión para pg_dump y las env vars — SIEMPRE con
    -h/-p/-U/-d + PGPASSWORD, nunca con la password embebida en el DSN como
    argumento de línea de comandos (queda visible en `ps aux` mientras corre)."""
    cfg = db_config()
    env = os.environ.copy()
    if "dsn" in cfg:
        parts = urlsplit(cfg["dsn"])
        args = [
            "-h", parts.hostname or "localhost",
            "-p", str(parts.port or 5432),
            "-U", unquote(parts.username or ""),
            "-d", unquote(parts.path.lstrip("/")),
        ]
        if parts.password:
            env["PGPASSWORD"] = unquote(parts.password)
    else:
        args = [
            "-h", cfg["host"], "-p", str(cfg["port"]),
            "-U", cfg["user"], "-d", cfg["dbname"],
        ]
        if cfg.get("password"):
            env["PGPASSWORD"] = cfg["password"]
    return args, env


def _run_dump(dest: Path) -> None:
    conn_args, env = _conn_args()
    cmd = [PG_DUMP_BIN, *conn_args, "-Fc", "-f", str(dest)]

    last_err: subprocess.CalledProcessError | None = None
    for intento in range(1, MAX_INTENTOS + 1):
        try:
            subprocess.run(cmd, check=True, env=env)
            return
        except subprocess.CalledProcessError as e:
            last_err = e
            dest.unlink(missing_ok=True)  # no dejar un .dump parcial/vacío que parezca válido
            if intento < MAX_INTENTOS:
                _log(f"Intento {intento}/{MAX_INTENTOS} falló (código {e.returncode}), "
                     f"reintentando en {ESPERA_ENTRE_INTENTOS_SEG}s…")
                time.sleep(ESPERA_ENTRE_INTENTOS_SEG)
    raise last_err  # todos los intentos fallaron


def _rotar_viejos() -> int:
    limite = time.time() - RETENTION_DAYS * 86400
    borrados = 0
    for f in BACKUP_DIR.glob("historico_talento_humano_*.dump"):
        if f.stat().st_mtime < limite:
            f.unlink()
            borrados += 1
    return borrados


def main() -> None:
    BACKUP_DIR.mkdir(parents=True, exist_ok=True)
    ts = datetime.now().strftime("%Y%m%d_%H%M%S")
    dest = BACKUP_DIR / f"historico_talento_humano_{ts}.dump"

    _log(f"Iniciando backup → {dest.name}")
    _run_dump(dest)
    size_mb = dest.stat().st_size / (1024 * 1024)
    _log(f"Backup completo: {size_mb:.1f} MB")

    borrados = _rotar_viejos()
    if borrados:
        _log(f"Rotación: {borrados} backup(s) eliminado(s) (> {RETENTION_DAYS} días)")


if __name__ == "__main__":
    try:
        main()
    except subprocess.CalledProcessError as e:
        _log(f"ERROR: pg_dump falló tras {MAX_INTENTOS} intentos (código {e.returncode})")
        sys.exit(1)
