#!/usr/bin/env python3
"""Restore Script — restore LeoCRM from a backup directory.

Usage:
    python scripts/restore.py --backup-dir /backups/leocrm_backup_20260723_220000
    python scripts/restore.py --backup-dir /backups/leocrm_backup_20260723_220000 --skip-db
    python scripts/restore.py --backup-dir /backups/leocrm_backup_20260723_220000 --skip-files
    python scripts/restore.py --backup-dir /backups/leocrm_backup_20260723_220000 --dry-run

Restores:
    1. Database from database.sql (psql)
    2. Files from files/ directory (shutil copy)

Environment variables:
    DATABASE_URL          — PostgreSQL connection string
    STORAGE_PATH          — Target path for file restoration (default: /data/storage)

Exit codes:
    0 — restore successful
    1 — restore failed
    2 — configuration error
"""

from __future__ import annotations

import argparse
import json
import os
import shutil
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

# Add project root to path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

DEFAULT_STORAGE_PATH = os.environ.get("STORAGE_PATH", "/data/storage")


def get_db_connection_params() -> dict[str, str]:
    """Extract PostgreSQL connection params from DATABASE_URL env var."""
    db_url = os.environ.get("DATABASE_URL", "")
    if not db_url:
        raise ValueError("DATABASE_URL environment variable not set")

    if db_url.startswith("postgresql+asyncpg://"):
        db_url = db_url.replace("postgresql+asyncpg://", "postgresql://", 1)

    from urllib.parse import urlparse

    parsed = urlparse(db_url)
    return {
        "url": db_url,
        "host": parsed.hostname or "localhost",
        "port": str(parsed.port or 5432),
        "database": parsed.path.lstrip("/"),
        "username": parsed.username or "postgres",
        "password": parsed.password or "",
    }


def restore_database(backup_dir: Path, db_params: dict[str, str]) -> tuple[bool, str]:
    """Restore database from database.sql using psql.

    Returns (success, error_message).
    """
    db_file = backup_dir / "database.sql"
    if not db_file.is_file():
        return False, f"Database dump not found: {db_file}"

    cmd = [
        "psql",
        "--host", db_params["host"],
        "--port", db_params["port"],
        "--username", db_params["username"],
        "--dbname", db_params["database"],
        "--file", str(db_file),
        "--quiet",
    ]

    env = os.environ.copy()
    if db_params["password"]:
        env["PGPASSWORD"] = db_params["password"]

    try:
        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            env=env,
            timeout=600,
        )
        if result.returncode != 0:
            return False, result.stderr or "psql failed"
        return True, ""
    except subprocess.TimeoutExpired:
        return False, "psql timed out after 600s"
    except FileNotFoundError:
        return False, "psql command not found"
    except Exception as exc:
        return False, str(exc)


def restore_files(backup_dir: Path, target_path: str) -> tuple[bool, int, int, str]:
    """Restore files from backup files/ directory to target path.

    Returns (success, file_count, total_size_bytes, error_message).
    """
    files_dir = backup_dir / "files"
    if not files_dir.is_dir():
        return True, 0, 0, ""  # No files in backup — not an error

    try:
        os.makedirs(target_path, exist_ok=True)
        file_count = 0
        total_size = 0

        for root, dirs, files in os.walk(files_dir):
            rel_root = os.path.relpath(root, files_dir)
            dest_root = os.path.join(target_path, rel_root) if rel_root != "." else target_path
            os.makedirs(dest_root, exist_ok=True)

            for fname in files:
                src_file = os.path.join(root, fname)
                dest_file = os.path.join(dest_root, fname)
                shutil.copy2(src_file, dest_file)
                file_count += 1
                total_size += os.path.getsize(src_file)

        return True, file_count, total_size, ""
    except Exception as exc:
        return False, 0, 0, str(exc)


def validate_backup_dir(backup_dir: Path) -> dict[str, Any] | None:
    """Validate that backup directory has a manifest.json."""
    manifest_file = backup_dir / "manifest.json"
    if not manifest_file.is_file():
        return None
    try:
        with open(manifest_file) as f:
            return json.load(f)
    except Exception:
        return None


def run_restore(
    backup_dir_path: str,
    target_storage_path: str,
    skip_db: bool = False,
    skip_files: bool = False,
    dry_run: bool = False,
) -> dict[str, Any]:
    """Run the full restore pipeline and return a report."""
    backup_dir = Path(backup_dir_path)
    report: dict[str, Any] = {
        "backup_dir": str(backup_dir),
        "dry_run": dry_run,
        "started_at": datetime.now(timezone.utc).isoformat(),
        "database": {"success": False, "error": ""},
        "files": {"success": False, "count": 0, "size_bytes": 0, "error": ""},
        "overall_success": False,
        "errors": [],
    }

    # Validate backup directory
    if not backup_dir.is_dir():
        report["errors"].append(f"Backup directory not found: {backup_dir}")
        report["finished_at"] = datetime.now(timezone.utc).isoformat()
        return report

    manifest = validate_backup_dir(backup_dir)
    if manifest:
        report["manifest"] = manifest
        print(f"  Backup from: {manifest.get('created_at', 'unknown')}")
    else:
        print(f"  Warning: No manifest.json found in {backup_dir}")

    if dry_run:
        report["database"]["error"] = "[DRY-RUN] Would restore database"
        report["files"]["error"] = "[DRY-RUN] Would restore files"
        report["overall_success"] = True
        report["finished_at"] = datetime.now(timezone.utc).isoformat()
        return report

    # Step 1: Database restore
    if not skip_db:
        print("▶ Step 1: Database restore (psql)")
        try:
            db_params = get_db_connection_params()
            db_success, db_error = restore_database(backup_dir, db_params)
            report["database"] = {"success": db_success, "error": db_error}
            if db_success:
                print("  ✓ Database restored")
            else:
                print(f"  ✗ Database restore failed: {db_error}")
                report["errors"].append(f"Database: {db_error}")
        except Exception as exc:
            report["database"]["error"] = str(exc)
            report["errors"].append(f"Database config: {exc}")
            print(f"  ✗ Database config error: {exc}")
    else:
        print("▶ Step 1: Database restore — SKIPPED")
        report["database"] = {"success": True, "error": "skipped"}

    # Step 2: File restore
    if not skip_files:
        print(f"▶ Step 2: File restore → {target_storage_path}")
        files_success, files_count, files_size, files_error = restore_files(backup_dir, target_storage_path)
        report["files"] = {
            "success": files_success,
            "count": files_count,
            "size_bytes": files_size,
            "error": files_error,
        }
        if files_success:
            print(f"  ✓ Files restored: {files_count} files, {files_size} bytes")
        else:
            print(f"  ✗ File restore failed: {files_error}")
            report["errors"].append(f"Files: {files_error}")
    else:
        print("▶ Step 2: File restore — SKIPPED")
        report["files"] = {"success": True, "count": 0, "size_bytes": 0, "error": "skipped"}

    # Determine overall success
    report["overall_success"] = report["database"]["success"] and report["files"]["success"]
    report["finished_at"] = datetime.now(timezone.utc).isoformat()

    return report


# ─── CLI Entry Point ─────────────────────────────────────────────────


def main() -> int:
    parser = argparse.ArgumentParser(
        description="LeoCRM Restore — restore database and files from a backup directory."
    )
    parser.add_argument(
        "--backup-dir",
        required=True,
        help="Path to the backup directory (e.g. /backups/leocrm_backup_20260723_220000)",
    )
    parser.add_argument(
        "--storage-path",
        default=DEFAULT_STORAGE_PATH,
        help=f"Target path for file restoration (default: {DEFAULT_STORAGE_PATH})",
    )
    parser.add_argument(
        "--skip-db",
        action="store_true",
        help="Skip database restoration",
    )
    parser.add_argument(
        "--skip-files",
        action="store_true",
        help="Skip file restoration",
    )
    parser.add_argument(
        "--dry-run",
        action="store_true",
        help="Simulate restore without executing",
    )
    parser.add_argument(
        "--output",
        default="-",
        help="Write JSON report to file (default: stdout)",
    )
    args = parser.parse_args()

    print(f"\n{'='*60}")
    print(f"  LeoCRM Restore Pipeline")
    print(f"  {'[DRY-RUN]' if args.dry_run else '[LIVE]'}")
    print(f"{'='*60}\n")

    report = run_restore(
        backup_dir_path=args.backup_dir,
        target_storage_path=args.storage_path,
        skip_db=args.skip_db,
        skip_files=args.skip_files,
        dry_run=args.dry_run,
    )

    report_json = json.dumps(report, indent=2, ensure_ascii=False)
    if args.output == "-":
        print(f"\n{'='*60}")
        print("  Restore Report")
        print(f"{'='*60}")
        print(report_json)
    else:
        with open(args.output, "w", encoding="utf-8") as f:
            f.write(report_json)
        print(f"\nReport written to {args.output}", file=sys.stderr)

    return 0 if report["overall_success"] else 1


if __name__ == "__main__":
    sys.exit(main())
