#!/usr/bin/env python3
"""Weekly encrypted backup for JARVIS/Hermes health system.

Creates a local backup bundle containing:
- health SQLite DB (consistent sqlite .backup copy)
- SQLite schema dump and integrity report
- health scripts (no PDFs/raw documents)
- Hermes official quick backup (config, .env, auth, cron, state)
- Hermes skills and user scripts

Then encrypts the tarball with GPG symmetric AES256 and uploads only the
encrypted `.tar.gz.gpg` plus a small non-sensitive manifest to Google Drive.

This script is quiet unless it creates/uploads a backup or fails.
"""
from __future__ import annotations

import json
import os
import shutil
import sqlite3
import subprocess
import sys
import tarfile
import tempfile
from datetime import datetime
from pathlib import Path

HOME = Path.home()
HERMES = HOME / ".hermes"
HEALTH = HERMES / "assets" / "Gesundheit"
DB = HEALTH / "health_data.db"
BACKUP_ROOT = HERMES / "backups" / "weekly_health_drive"
KEYFILE = HERMES / "backup_keys" / "health_weekly_gpg_passphrase.txt"
DRIVE_FOLDER_ID = "1dlFpkKYXOCKaausFfVVnBK-qk3hFHA8T"  # Gesundheitsdaten Inbox/Hermes_Health_Backups
ACCOUNT = "friday.uplink@gmail.com"
GOG_SECRET_ENV = HERMES / "secrets" / "gog_keyring.env"
EXCLUDES = {
    ".git", "__pycache__", ".pytest_cache", ".mypy_cache",
}


def run(cmd: list[str], *, env: dict[str, str] | None = None, timeout: int = 300, cwd: Path | None = None) -> subprocess.CompletedProcess:
    return subprocess.run(cmd, cwd=str(cwd) if cwd else None, env=env, text=True, capture_output=True, timeout=timeout, check=True)


def copytree_filtered(src: Path, dst: Path) -> None:
    def ignore(dirpath: str, names: list[str]) -> set[str]:
        ignored = set()
        for n in names:
            if n in EXCLUDES:
                ignored.add(n)
            # Do not include raw medical documents or generated binary reports in this bundle.
            if n.lower().endswith((".pdf", ".png", ".jpg", ".jpeg", ".webp", ".zip", ".gpg")):
                ignored.add(n)
        return ignored
    if src.exists():
        shutil.copytree(src, dst, ignore=ignore)


def sqlite_backup(src: Path, dst: Path) -> str:
    src_con = sqlite3.connect(src)
    try:
        integrity = src_con.execute("PRAGMA integrity_check").fetchone()[0]
        if integrity != "ok":
            raise RuntimeError(f"SQLite integrity_check failed: {integrity}")
        dst_con = sqlite3.connect(dst)
        try:
            src_con.backup(dst_con)
        finally:
            dst_con.close()
        return integrity
    finally:
        src_con.close()


def schema_dump(db: Path, out: Path) -> None:
    # Keep the backup independent of the optional sqlite3 CLI. Python's
    # sqlite3 module is already required for the consistent database copy.
    con = sqlite3.connect(db)
    try:
        out.write_text("\n".join(con.iterdump()) + "\n", encoding="utf-8")
    finally:
        con.close()


def load_gog_keyring_password() -> str:
    value = os.environ.get("GOG_KEYRING_PASSWORD", "").strip()
    if value:
        return value
    if not GOG_SECRET_ENV.is_file():
        raise RuntimeError(f"Missing gog keyring secret environment file: {GOG_SECRET_ENV}")
    if GOG_SECRET_ENV.stat().st_mode & 0o077:
        raise RuntimeError(f"Unsafe permissions on gog keyring secret file: {GOG_SECRET_ENV}")
    for raw_line in GOG_SECRET_ENV.read_text(encoding="utf-8").splitlines():
        line = raw_line.strip()
        if not line or line.startswith("#"):
            continue
        if line.startswith("export "):
            line = line[7:].strip()
        key, sep, raw_value = line.partition("=")
        if sep and key.strip() == "GOG_KEYRING_PASSWORD":
            secret = raw_value.strip().strip('"').strip("'")
            if secret:
                return secret
            raise RuntimeError(f"GOG_KEYRING_PASSWORD empty in {GOG_SECRET_ENV}")
    raise RuntimeError(f"GOG_KEYRING_PASSWORD missing in {GOG_SECRET_ENV}")


def create_bundle() -> tuple[Path, dict]:
    ts = datetime.now().strftime("%Y%m%d_%H%M%S")
    BACKUP_ROOT.mkdir(parents=True, exist_ok=True)
    work = Path(tempfile.mkdtemp(prefix=f"health_backup_{ts}_", dir=str(BACKUP_ROOT)))
    stage = work / "bundle"
    stage.mkdir()

    manifest = {
        "created_at": datetime.now().isoformat(),
        "host": os.uname().nodename,
        "backup_type": "weekly_health_drive_encrypted",
        "contents": [],
        "excludes": ["PDFs", "images", "raw documents", "unencrypted external upload"],
    }

    # Health DB + schema.
    db_out = stage / "health" / "health_data.db"
    db_out.parent.mkdir(parents=True, exist_ok=True)
    integrity = sqlite_backup(DB, db_out)
    schema_dump(DB, stage / "health" / "schema.sql")
    manifest["sqlite_integrity_check"] = integrity
    manifest["health_db_bytes"] = DB.stat().st_size
    manifest["contents"].extend(["health/health_data.db", "health/schema.sql"])

    # Health scripts and lightweight docs/reports metadata, but no PDFs/images.
    copytree_filtered(HEALTH / "scripts", stage / "health" / "scripts")
    for rel in ["reports/health_intelligence_workflow.md", "reports/arztbericht_drive_link.json"]:
        src = HEALTH / rel
        if src.exists():
            dst = stage / "health" / rel
            dst.parent.mkdir(parents=True, exist_ok=True)
            shutil.copy2(src, dst)
    manifest["contents"].append("health/scripts")

    # Official Hermes quick backup: config, state, .env, auth, cron.
    hermes_zip = stage / "hermes_quick_backup.zip"
    run(["hermes", "backup", "--quick", "--label", "weekly-health", "--output", str(hermes_zip)], timeout=300)
    manifest["contents"].append("hermes_quick_backup.zip")

    # Skills and user cron scripts are small and important for behaviour/personality/workflows.
    copytree_filtered(HERMES / "skills", stage / "hermes" / "skills")
    copytree_filtered(HERMES / "scripts", stage / "hermes" / "scripts")
    manifest["contents"].extend(["hermes/skills", "hermes/scripts"])

    # Encrypted bundle may safely include external credentials needed for restore.
    # Never upload these unencrypted.
    external = stage / "external_credentials"
    for src in [
        HOME / ".config" / "gogcli" / "credentials.json",
        HERMES / "google_credentials.json",
        HOME / ".todoist_token",
    ]:
        if src.exists():
            dst = external / src.relative_to(HOME)
            dst.parent.mkdir(parents=True, exist_ok=True)
            shutil.copy2(src, dst)
            manifest["contents"].append(f"external_credentials/{src.relative_to(HOME)}")

    (stage / "backup_manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")

    tar_path = BACKUP_ROOT / f"health_hermes_backup_{ts}.tar.gz"
    with tarfile.open(tar_path, "w:gz") as tar:
        tar.add(stage, arcname=".")
    shutil.rmtree(work)
    return tar_path, manifest


def encrypt(tar_path: Path) -> Path:
    if not KEYFILE.exists():
        raise RuntimeError(f"Missing GPG passphrase file: {KEYFILE}")
    out = tar_path.with_suffix(tar_path.suffix + ".gpg")
    run([
        "gpg", "--batch", "--yes", "--pinentry-mode", "loopback",
        "--passphrase-file", str(KEYFILE), "--symmetric", "--cipher-algo", "AES256",
        "--output", str(out), str(tar_path),
    ], timeout=600)
    out.chmod(0o600)
    tar_path.unlink(missing_ok=True)
    return out


def upload(path: Path) -> dict:
    env = os.environ.copy()
    env["GOG_KEYRING_PASSWORD"] = load_gog_keyring_password()
    res = run(["gog", "-a", ACCOUNT, "drive", "upload", str(path), "--parent", DRIVE_FOLDER_ID, "--json"], env=env, timeout=600)
    return json.loads(res.stdout)


def main() -> int:
    try:
        tar_path, manifest = create_bundle()
        encrypted = encrypt(tar_path)
        upload_result = upload(encrypted)
        # Local encrypted snapshots are useful for quick restore; keep them. Drive is offsite copy.
        manifest_path = encrypted.with_suffix(encrypted.suffix + ".manifest.json")
        manifest["encrypted_file"] = str(encrypted)
        manifest["encrypted_bytes"] = encrypted.stat().st_size
        manifest["drive_upload"] = upload_result
        manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
        manifest_path.chmod(0o600)
        link = upload_result.get("file", {}).get("webViewLink") or upload_result.get("webViewLink") or ""
        print(f"Wöchentliches Gesundheits-/Hermes-Backup erstellt und verschlüsselt hochgeladen: {encrypted.name}\nDrive: {link}")
        return 0
    except Exception as e:
        print(f"Wöchentliches Backup FEHLER: {e}", file=sys.stderr)
        return 1


if __name__ == "__main__":
    raise SystemExit(main())
