"""
BizNode Backup Engine
Dumps PostgreSQL database and Qdrant vector collections,
then uploads everything to Google Drive.
"""

import os
import sys
import json
import shutil
import logging
import subprocess
import requests
from pathlib import Path
from datetime import datetime, timezone

# Add project root to path
PROJECT_ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(PROJECT_ROOT))

logger = logging.getLogger("biznode.backup")

BACKUP_DIR = PROJECT_ROOT / "backups"
STATE_FILE = Path(__file__).parent / "last_backup.json"

# PostgreSQL pg_dump path (Windows)
PG_DUMP = r"C:\Program Files\PostgreSQL\18\bin\pg_dump.exe"


def load_env():
    """Load database config from .env file."""
    env = {}
    env_file = PROJECT_ROOT / ".env"
    if env_file.exists():
        for line in env_file.read_text().splitlines():
            line = line.strip()
            if line and not line.startswith("#") and "=" in line:
                key, _, val = line.partition("=")
                env[key.strip()] = val.strip()
    return env


def dump_postgresql(env, output_dir):
    """Dump PostgreSQL database using pg_dump."""
    db_host = env.get("DB_HOST", "localhost")
    db_port = env.get("DB_PORT", "5432")
    db_name = env.get("DB_NAME", "biznode")
    db_user = env.get("DB_USER", "biznode_user")
    db_pass = env.get("DB_PASSWORD", "")

    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    dump_file = output_dir / f"postgresql_{db_name}_{timestamp}.sql"

    pg_dump_path = PG_DUMP
    if not os.path.exists(pg_dump_path):
        # Try system PATH
        pg_dump_path = "pg_dump"

    cmd = [
        pg_dump_path,
        "-h", db_host,
        "-p", db_port,
        "-U", db_user,
        "-d", db_name,
        "--no-password",
        "-f", str(dump_file),
    ]

    env_vars = os.environ.copy()
    env_vars["PGPASSWORD"] = db_pass

    logger.info(f"Dumping PostgreSQL database '{db_name}' ...")
    result = subprocess.run(cmd, env=env_vars, capture_output=True, text=True, timeout=300)

    if result.returncode != 0:
        logger.error(f"pg_dump failed: {result.stderr}")
        raise RuntimeError(f"pg_dump failed: {result.stderr}")

    size_mb = dump_file.stat().st_size / (1024 * 1024)
    logger.info(f"PostgreSQL dump complete: {dump_file.name} ({size_mb:.2f} MB)")
    return dump_file


def snapshot_qdrant_collection(collection_name, qdrant_host, qdrant_port, output_dir):
    """Create and download a Qdrant collection snapshot."""
    base_url = f"http://{qdrant_host}:{qdrant_port}"
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")

    # Create snapshot (with retry)
    local_file = output_dir / f"qdrant_{collection_name}_{timestamp}.snapshot"
    max_retries = 3
    for attempt in range(1, max_retries + 1):
        try:
            logger.info(f"Creating snapshot for Qdrant collection '{collection_name}' (attempt {attempt}/{max_retries}) ...")
            resp = requests.post(f"{base_url}/collections/{collection_name}/snapshots", timeout=300)
            resp.raise_for_status()
            snapshot_info = resp.json()["result"]
            snapshot_name = snapshot_info["name"]

            # Download snapshot
            download_url = f"{base_url}/collections/{collection_name}/snapshots/{snapshot_name}"
            logger.info(f"Downloading snapshot '{snapshot_name}' ...")
            with requests.get(download_url, stream=True, timeout=600) as r:
                r.raise_for_status()
                with open(local_file, "wb") as f:
                    for chunk in r.iter_content(chunk_size=65536):
                        f.write(chunk)
            break  # Success
        except Exception as e:
            if attempt == max_retries:
                raise
            logger.warning(f"Attempt {attempt} failed for '{collection_name}': {e}. Retrying...")
            import time
            time.sleep(5)

    size_mb = local_file.stat().st_size / (1024 * 1024)
    logger.info(f"Qdrant snapshot saved: {local_file.name} ({size_mb:.2f} MB)")

    # Clean up remote snapshot
    try:
        requests.delete(
            f"{base_url}/collections/{collection_name}/snapshots/{snapshot_name}",
            timeout=30,
        )
    except Exception:
        pass

    return local_file


def dump_qdrant(env, output_dir):
    """Snapshot all Qdrant collections."""
    qdrant_host = env.get("QDRANT_HOST", "localhost")
    qdrant_port = env.get("QDRANT_PORT", "6333")
    base_url = f"http://{qdrant_host}:{qdrant_port}"

    # Get all collections
    resp = requests.get(f"{base_url}/collections", timeout=10)
    resp.raise_for_status()
    collections = [c["name"] for c in resp.json()["result"]["collections"]]

    if not collections:
        logger.warning("No Qdrant collections found to back up")
        return []

    logger.info(f"Found {len(collections)} Qdrant collections: {collections}")

    snapshot_files = []
    for coll in collections:
        try:
            fpath = snapshot_qdrant_collection(coll, qdrant_host, qdrant_port, output_dir)
            snapshot_files.append(fpath)
        except Exception as e:
            logger.error(f"Failed to snapshot collection '{coll}': {e}")

    return snapshot_files


def save_state(success, error=None):
    """Save last backup state for catch-up logic."""
    state = {
        "last_backup": datetime.now(timezone.utc).isoformat(),
        "success": success,
        "error": str(error) if error else None,
    }
    STATE_FILE.write_text(json.dumps(state, indent=2))


def get_last_backup_time():
    """Get the last successful backup time."""
    if not STATE_FILE.exists():
        return None
    try:
        state = json.loads(STATE_FILE.read_text())
        if state.get("success"):
            return datetime.fromisoformat(state["last_backup"])
    except Exception:
        pass
    return None


def cleanup_local(output_dir, keep_days=7):
    """Remove local backup files older than keep_days."""
    from datetime import timedelta
    cutoff = datetime.now().timestamp() - (keep_days * 86400)
    removed = 0
    for f in output_dir.iterdir():
        if f.is_file() and f.stat().st_mtime < cutoff:
            f.unlink()
            removed += 1
    if removed:
        logger.info(f"Cleaned up {removed} local backup files older than {keep_days} days")


def run_backup(upload_to_gdrive=True):
    """
    Execute a full backup: PostgreSQL + Qdrant -> local files -> Google Drive.
    Returns (success: bool, backup_files: list, error: str|None)
    """
    timestamp = datetime.now().strftime("%Y-%m-%d")
    day_dir = BACKUP_DIR / timestamp
    day_dir.mkdir(parents=True, exist_ok=True)

    env = load_env()
    backup_files = []
    errors = []

    # 1. PostgreSQL dump
    db_type = env.get("DB_TYPE", "sqlite").lower()
    if db_type in ("postgresql", "postgres", "mysql"):
        try:
            pg_file = dump_postgresql(env, day_dir)
            backup_files.append(pg_file)
        except Exception as e:
            logger.error(f"PostgreSQL backup failed: {e}")
            errors.append(f"PostgreSQL: {e}")
    else:
        # SQLite - just copy the db file
        sqlite_path = PROJECT_ROOT / env.get("SQLITE_PATH", "memory/biznode.db")
        if sqlite_path.exists():
            ts = datetime.now().strftime("%Y%m%d_%H%M%S")
            dest = day_dir / f"sqlite_biznode_{ts}.db"
            shutil.copy2(sqlite_path, dest)
            backup_files.append(dest)
            logger.info(f"SQLite backup copied: {dest.name}")

    # 2. Qdrant snapshots
    try:
        qdrant_files = dump_qdrant(env, day_dir)
        backup_files.extend(qdrant_files)
    except Exception as e:
        logger.error(f"Qdrant backup failed: {e}")
        errors.append(f"Qdrant: {e}")

    if not backup_files:
        error_msg = "No backup files produced. " + "; ".join(errors)
        save_state(False, error_msg)
        return False, [], error_msg

    # 3. Upload to Google Drive
    if upload_to_gdrive:
        try:
            from backup.gdrive_uploader import upload_backup_to_gdrive
            uploaded = upload_backup_to_gdrive(
                [str(f) for f in backup_files], date_str=timestamp
            )
            logger.info(f"Uploaded {len(uploaded)} files to Google Drive")
        except Exception as e:
            logger.error(f"Google Drive upload failed: {e}")
            errors.append(f"GDrive: {e}")

    # 4. Clean up old local backups
    try:
        cleanup_local(BACKUP_DIR, keep_days=7)
    except Exception as e:
        logger.warning(f"Local cleanup failed: {e}")

    success = len(errors) == 0
    error_msg = "; ".join(errors) if errors else None
    save_state(success, error_msg)

    return success, backup_files, error_msg
