#!/usr/bin/env python3
"""Ransomware backup strategy audit agent.

Audits backup infrastructure for ransomware resilience by checking
3-2-1 backup rule compliance, air-gapped/immutable backup presence,
backup encryption status, recovery point objectives (RPO), and
backup integrity verification schedules.
"""
import argparse
import json
import os
import subprocess
import sys
from datetime import datetime, timezone, timedelta


def check_veeam_backups(server_url, token):
    """Check Veeam backup status via REST API."""
    try:
        import requests
    except ImportError:
        return [{"check": "Veeam API", "status": "SKIP", "severity": "INFO",
                 "detail": "requests library not installed"}]

    findings = []
    headers = {"Authorization": f"Bearer {token}", "Accept": "application/json"}

    # Check backup jobs
    try:
        resp = requests.get(f"{server_url}/api/v1/jobs", headers=headers, timeout=30)
        resp.raise_for_status()
        jobs = resp.json().get("data", [])
        for job in jobs:
            job_name = job.get("name", "Unknown")
            last_result = job.get("lastResult", "None")
            schedule_enabled = job.get("scheduleEnabled", False)
            if last_result == "Failed":
                findings.append({
                    "check": f"Backup job: {job_name}",
                    "status": "FAIL",
                    "severity": "CRITICAL",
                    "detail": "Last backup failed",
                })
            elif not schedule_enabled:
                findings.append({
                    "check": f"Backup job: {job_name}",
                    "status": "WARN",
                    "severity": "HIGH",
                    "detail": "Schedule disabled",
                })
            else:
                findings.append({
                    "check": f"Backup job: {job_name}",
                    "status": "PASS",
                    "severity": "INFO",
                    "detail": f"Last result: {last_result}",
                })
    except Exception as e:
        findings.append({"check": "Veeam job check", "status": "ERROR",
                         "severity": "HIGH", "detail": str(e)})
    return findings


def check_restic_repository(repo_path, password_file=None):
    """Audit a Restic backup repository for integrity and freshness."""
    findings = []
    restic_bin = None
    for name in ["restic", "restic.exe"]:
        for d in os.environ.get("PATH", "").split(os.pathsep):
            if os.path.isfile(os.path.join(d, name)):
                restic_bin = os.path.join(d, name)
                break
    if not restic_bin:
        findings.append({"check": "Restic binary", "status": "SKIP",
                         "severity": "INFO", "detail": "restic not found"})
        return findings

    env = dict(os.environ)
    env["RESTIC_REPOSITORY"] = repo_path
    if password_file:
        env["RESTIC_PASSWORD_FILE"] = password_file

    # Check snapshots
    try:
        result = subprocess.run(
            [restic_bin, "snapshots", "--json"],
            capture_output=True, text=True, timeout=120, env=env,
        )
        if result.returncode == 0:
            snapshots = json.loads(result.stdout)
            if not snapshots:
                findings.append({"check": "Snapshots exist", "status": "FAIL",
                                 "severity": "CRITICAL", "detail": "No snapshots found"})
            else:
                latest = max(snapshots, key=lambda s: s.get("time", ""))
                latest_time = latest.get("time", "")[:19]
                findings.append({"check": "Latest snapshot", "status": "PASS",
                                 "severity": "INFO",
                                 "detail": f"{latest_time} ({len(snapshots)} total)"})
                try:
                    latest_dt = datetime.fromisoformat(latest_time.replace("Z", "+00:00"))
                    age_hours = (datetime.now(timezone.utc) - latest_dt).total_seconds() / 3600
                    if age_hours > 48:
                        findings.append({"check": "Backup freshness", "status": "FAIL",
                                        "severity": "HIGH",
                                        "detail": f"Latest backup is {age_hours:.0f}h old (>48h)"})
                    elif age_hours > 24:
                        findings.append({"check": "Backup freshness", "status": "WARN",
                                        "severity": "MEDIUM",
                                        "detail": f"Latest backup is {age_hours:.0f}h old (>24h)"})
                except (ValueError, TypeError):
                    pass
        else:
            findings.append({"check": "Repository access", "status": "FAIL",
                             "severity": "CRITICAL", "detail": result.stderr[:200]})
    except subprocess.TimeoutExpired:
        findings.append({"check": "Repository access", "status": "FAIL",
                         "severity": "HIGH", "detail": "Command timed out"})

    # Check repository integrity
    try:
        result = subprocess.run(
            [restic_bin, "check", "--read-data-subset=1%"],
            capture_output=True, text=True, timeout=300, env=env,
        )
        if result.returncode == 0:
            findings.append({"check": "Repository integrity", "status": "PASS",
                             "severity": "INFO", "detail": "Integrity check passed"})
        else:
            findings.append({"check": "Repository integrity", "status": "FAIL",
                             "severity": "CRITICAL", "detail": "Integrity check failed"})
    except subprocess.TimeoutExpired:
        findings.append({"check": "Repository integrity", "status": "WARN",
                         "severity": "MEDIUM", "detail": "Integrity check timed out"})

    return findings


def audit_321_rule(backup_config):
    """Audit compliance with the 3-2-1 backup rule."""
    findings = []
    copies = backup_config.get("copies", 0)
    media_types = backup_config.get("media_types", [])
    offsite_locations = backup_config.get("offsite_locations", [])
    immutable = backup_config.get("immutable_backup", False)
    air_gapped = backup_config.get("air_gapped", False)

    # 3 copies
    if copies >= 3:
        findings.append({"check": "3-2-1: At least 3 copies", "status": "PASS",
                         "severity": "INFO", "detail": f"{copies} copies"})
    else:
        findings.append({"check": "3-2-1: At least 3 copies", "status": "FAIL",
                         "severity": "CRITICAL",
                         "detail": f"Only {copies} copies (need 3)"})

    # 2 different media types
    if len(media_types) >= 2:
        findings.append({"check": "3-2-1: 2 different media types", "status": "PASS",
                         "severity": "INFO", "detail": ", ".join(media_types)})
    else:
        findings.append({"check": "3-2-1: 2 different media types", "status": "FAIL",
                         "severity": "HIGH",
                         "detail": f"Only {len(media_types)} type(s): {', '.join(media_types)}"})

    # 1 offsite copy
    if offsite_locations:
        findings.append({"check": "3-2-1: 1 offsite copy", "status": "PASS",
                         "severity": "INFO", "detail": ", ".join(offsite_locations)})
    else:
        findings.append({"check": "3-2-1: 1 offsite copy", "status": "FAIL",
                         "severity": "CRITICAL", "detail": "No offsite backup"})

    # Immutable backup (ransomware protection)
    if immutable:
        findings.append({"check": "Immutable backup", "status": "PASS",
                         "severity": "INFO", "detail": "WORM/immutable storage enabled"})
    else:
        findings.append({"check": "Immutable backup", "status": "FAIL",
                         "severity": "CRITICAL",
                         "detail": "No immutable backup - vulnerable to ransomware encryption"})

    # Air-gapped backup
    if air_gapped:
        findings.append({"check": "Air-gapped backup", "status": "PASS",
                         "severity": "INFO", "detail": "Offline/air-gapped copy exists"})
    else:
        findings.append({"check": "Air-gapped backup", "status": "WARN",
                         "severity": "HIGH",
                         "detail": "No air-gapped backup - consider tape or offline storage"})

    # Encryption
    if backup_config.get("encrypted", False):
        findings.append({"check": "Backup encryption", "status": "PASS",
                         "severity": "INFO"})
    else:
        findings.append({"check": "Backup encryption", "status": "FAIL",
                         "severity": "HIGH", "detail": "Backups are not encrypted"})

    return findings


def format_summary(all_findings):
    """Print audit summary."""
    print(f"\n{'='*60}")
    print(f"  Ransomware Backup Strategy Audit")
    print(f"{'='*60}")

    severity_counts = {}
    for f in all_findings:
        sev = f.get("severity", "INFO")
        severity_counts[sev] = severity_counts.get(sev, 0) + 1

    pass_count = sum(1 for f in all_findings if f.get("status") == "PASS")
    fail_count = sum(1 for f in all_findings if f.get("status") == "FAIL")
    warn_count = sum(1 for f in all_findings if f.get("status") == "WARN")

    print(f"  Checks   : {len(all_findings)}")
    print(f"  Passed   : {pass_count}")
    print(f"  Failed   : {fail_count}")
    print(f"  Warnings : {warn_count}")

    print(f"\n  By Severity:")
    for sev in ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"]:
        count = severity_counts.get(sev, 0)
        if count > 0:
            print(f"    {sev:10s}: {count}")

    print(f"\n  Detailed Results:")
    for f in all_findings:
        status_icon = "OK" if f["status"] == "PASS" else "!!" if f["status"] == "FAIL" else "~~"
        detail = f.get("detail", "")
        print(f"    [{status_icon}] [{f['severity']:8s}] {f['check']}"
              + (f": {detail}" if detail else ""))

    return severity_counts


def main():
    parser = argparse.ArgumentParser(
        description="Ransomware backup strategy audit agent"
    )
    parser.add_argument("--config", help="Backup configuration JSON file")
    parser.add_argument("--restic-repo", help="Restic repository path to audit")
    parser.add_argument("--restic-password-file", help="Restic password file")
    parser.add_argument("--veeam-url", help="Veeam server URL")
    parser.add_argument("--veeam-token", help="Veeam API token")
    parser.add_argument("--output", "-o", help="Output JSON report path")
    parser.add_argument("--verbose", "-v", action="store_true")
    args = parser.parse_args()

    all_findings = []

    if args.config:
        with open(args.config, "r") as f:
            backup_config = json.load(f)
        all_findings.extend(audit_321_rule(backup_config))

    if args.restic_repo:
        all_findings.extend(check_restic_repository(args.restic_repo, args.restic_password_file))

    if args.veeam_url and args.veeam_token:
        all_findings.extend(check_veeam_backups(args.veeam_url, args.veeam_token))

    if not all_findings:
        print("[!] No audit sources specified. Use --config, --restic-repo, or --veeam-url.",
              file=sys.stderr)
        parser.print_help()
        sys.exit(1)

    severity_counts = format_summary(all_findings)

    report = {
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "tool": "Ransomware Backup Audit",
        "findings": all_findings,
        "severity_counts": severity_counts,
        "risk_level": (
            "CRITICAL" if severity_counts.get("CRITICAL", 0) > 0
            else "HIGH" if severity_counts.get("HIGH", 0) > 0
            else "MEDIUM" if severity_counts.get("MEDIUM", 0) > 0
            else "LOW"
        ),
    }

    if args.output:
        with open(args.output, "w") as f:
            json.dump(report, f, indent=2)
        print(f"\n[+] Report saved to {args.output}")
    elif args.verbose:
        print(json.dumps(report, indent=2))


if __name__ == "__main__":
    main()
