// synops-backup — PG-dump + CAS-filiste + metadata-snapshot.
//
// Erstatter scripts/backup-pg.sh (cron-script fra oppgave 12.2).
// Gjør tre ting:
//   1. pg_dump -Fc via docker exec (konsistent snapshot)
//   2. Lister alle CAS-filer med størrelse
//   3. Skriver metadata-snapshot (JSON) med tidsstempel, modus, statistikk
//
// Bruk:
//   synops-backup --full              # Full backup
//   synops-backup --incremental       # Inkrementell (kun endringer siden forrige)
//   synops-backup --payload-json '{"mode":"full"}'  # Jobbkø-dispatch
//
// Output: backup-sti til stdout (JSON).
// Feil: stderr + exit code != 0.
//
// Ref: docs/infra/backup.md, docs/retninger/unix_filosofi.md

use chrono::Utc;
use clap::Parser;
use serde::Serialize;
use std::path::{Path, PathBuf};

/// Backup-modus.
#[derive(Debug, Clone, Copy, PartialEq)]
enum Mode {
    Full,
    Incremental,
}

/// PG-dump + CAS-filiste + metadata-snapshot.
#[derive(Parser)]
#[command(name = "synops-backup", about = "Backup: PG-dump, CAS-filiste og metadata-snapshot")]
struct Cli {
    /// Full backup (PG-dump + CAS-liste + metadata)
    #[arg(long, conflicts_with = "incremental")]
    full: bool,

    /// Inkrementell backup (kun CAS-filer nyere enn forrige backup)
    #[arg(long, conflicts_with = "full")]
    incremental: bool,

    /// Backup-katalog (default: /srv/synops/backup)
    #[arg(long, default_value = "/srv/synops/backup")]
    backup_dir: String,

    /// PG Docker-container (default: sidelinja-postgres-1)
    #[arg(long, default_value = "sidelinja-postgres-1")]
    pg_container: String,

    /// Database-bruker (default: sidelinja)
    #[arg(long, default_value = "sidelinja")]
    db_user: String,

    /// Databasenavn (default: sidelinja)
    #[arg(long, default_value = "sidelinja")]
    db_name: String,

    /// Antall dager å beholde gamle backuper (default: 30)
    #[arg(long, default_value = "30")]
    retain_days: u32,

    /// Payload fra jobbkø (JSON). Overstyrer andre argumenter.
    #[arg(long)]
    payload_json: Option<String>,
}

/// Resultat fra backup.
#[derive(Serialize)]
struct BackupResult {
    ok: bool,
    mode: String,
    timestamp: String,
    backup_dir: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pg_dump: Option<PgDumpResult>,
    #[serde(skip_serializing_if = "Option::is_none")]
    cas_manifest: Option<CasManifestResult>,
    #[serde(skip_serializing_if = "Option::is_none")]
    metadata_file: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    rotation: Option<RotationResult>,
    #[serde(skip_serializing_if = "Option::is_none")]
    error: Option<String>,
}

#[derive(Serialize)]
struct PgDumpResult {
    path: String,
    size_bytes: u64,
    size_human: String,
}

#[derive(Serialize)]
struct CasManifestResult {
    path: String,
    file_count: u64,
    total_size_bytes: u64,
    total_size_human: String,
}

#[derive(Serialize)]
struct RotationResult {
    deleted_count: u32,
    retain_days: u32,
}

/// Metadata-snapshot som skrives til backup-katalogen.
#[derive(Serialize)]
struct MetadataSnapshot {
    timestamp: String,
    mode: String,
    pg_dump_size_bytes: Option<u64>,
    cas_file_count: Option<u64>,
    cas_total_size_bytes: Option<u64>,
    hostname: String,
    db_name: String,
}

#[tokio::main]
async fn main() {
    synops_common::logging::init("synops_backup");

    let cli = Cli::parse();

    // Resolve mode fra args eller payload-json
    let (mode, backup_dir, pg_container, db_user, db_name, retain_days) =
        if let Some(ref json_str) = cli.payload_json {
            let payload: serde_json::Value = serde_json::from_str(json_str).unwrap_or_else(|e| {
                eprintln!("Ugyldig --payload-json: {e}");
                std::process::exit(1);
            });
            let mode = match payload["mode"].as_str().unwrap_or("full") {
                "incremental" => Mode::Incremental,
                _ => Mode::Full,
            };
            let backup_dir = payload["backup_dir"]
                .as_str()
                .unwrap_or("/srv/synops/backup")
                .to_string();
            let pg_container = payload["pg_container"]
                .as_str()
                .unwrap_or("sidelinja-postgres-1")
                .to_string();
            let db_user = payload["db_user"]
                .as_str()
                .unwrap_or("sidelinja")
                .to_string();
            let db_name = payload["db_name"]
                .as_str()
                .unwrap_or("sidelinja")
                .to_string();
            let retain_days = payload["retain_days"].as_u64().unwrap_or(30) as u32;
            (mode, backup_dir, pg_container, db_user, db_name, retain_days)
        } else {
            let mode = if cli.incremental {
                Mode::Incremental
            } else {
                Mode::Full // default
            };
            (
                mode,
                cli.backup_dir,
                cli.pg_container,
                cli.db_user,
                cli.db_name,
                cli.retain_days,
            )
        };

    let timestamp = Utc::now().format("%Y%m%d_%H%M%S").to_string();
    let timestamp_iso = Utc::now().to_rfc3339();

    // Opprett backup-kataloger
    let pg_dir = PathBuf::from(&backup_dir).join("pg");
    let manifest_dir = PathBuf::from(&backup_dir).join("manifests");
    for dir in [&pg_dir, &manifest_dir] {
        if let Err(e) = tokio::fs::create_dir_all(dir).await {
            let result = BackupResult {
                ok: false,
                mode: mode_str(mode).to_string(),
                timestamp: timestamp_iso.clone(),
                backup_dir: backup_dir.clone(),
                pg_dump: None,
                cas_manifest: None,
                metadata_file: None,
                rotation: None,
                error: Some(format!("Kunne ikke opprette katalog {}: {e}", dir.display())),
            };
            println!("{}", serde_json::to_string_pretty(&result).unwrap());
            std::process::exit(1);
        }
    }

    let mut all_ok = true;
    let mut pg_dump_result = None;
    let mut cas_manifest_result = None;
    let mut rotation_result = None;
    let mut error_msg = None;

    // 1. PG-dump
    tracing::info!(mode = mode_str(mode), "Starter backup");
    match run_pg_dump(&pg_container, &db_user, &db_name, &pg_dir, &timestamp).await {
        Ok(r) => {
            tracing::info!(path = %r.path, size = r.size_human, "PG-dump ferdig");
            pg_dump_result = Some(r);
        }
        Err(e) => {
            tracing::error!(error = %e, "PG-dump feilet");
            all_ok = false;
            error_msg = Some(e);
        }
    }

    // 2. CAS-filiste
    let cas_root = synops_common::cas::root();
    match build_cas_manifest(&cas_root, &manifest_dir, &timestamp, mode).await {
        Ok(r) => {
            tracing::info!(
                files = r.file_count,
                size = r.total_size_human,
                "CAS-manifest ferdig"
            );
            cas_manifest_result = Some(r);
        }
        Err(e) => {
            tracing::error!(error = %e, "CAS-manifest feilet");
            all_ok = false;
            if let Some(ref mut existing) = error_msg {
                existing.push_str(&format!("; CAS: {e}"));
            } else {
                error_msg = Some(e);
            }
        }
    }

    // 3. Metadata-snapshot
    let hostname = std::env::var("HOSTNAME")
        .or_else(|_| {
            std::process::Command::new("hostname")
                .output()
                .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
        })
        .unwrap_or_else(|_| "unknown".to_string());

    let metadata = MetadataSnapshot {
        timestamp: timestamp_iso.clone(),
        mode: mode_str(mode).to_string(),
        pg_dump_size_bytes: pg_dump_result.as_ref().map(|r| r.size_bytes),
        cas_file_count: cas_manifest_result.as_ref().map(|r| r.file_count),
        cas_total_size_bytes: cas_manifest_result.as_ref().map(|r| r.total_size_bytes),
        hostname,
        db_name: db_name.clone(),
    };

    let metadata_path = manifest_dir.join(format!("backup_{timestamp}.json"));
    let metadata_json = serde_json::to_string_pretty(&metadata).unwrap();
    if let Err(e) = tokio::fs::write(&metadata_path, &metadata_json).await {
        tracing::error!(error = %e, "Kunne ikke skrive metadata-snapshot");
        all_ok = false;
    }

    // 4. Rotasjon (kun ved full backup)
    if mode == Mode::Full && all_ok {
        match rotate_old_backups(&pg_dir, retain_days).await {
            Ok(r) => {
                if r.deleted_count > 0 {
                    tracing::info!(
                        deleted = r.deleted_count,
                        retain_days = r.retain_days,
                        "Rotasjon ferdig"
                    );
                }
                rotation_result = Some(r);
            }
            Err(e) => {
                tracing::warn!(error = %e, "Rotasjon feilet (backup er OK)");
                // Rotasjonsfeil er ikke kritisk — backupen er allerede tatt
            }
        }
    }

    // Output
    let result = BackupResult {
        ok: all_ok,
        mode: mode_str(mode).to_string(),
        timestamp: timestamp_iso,
        backup_dir,
        pg_dump: pg_dump_result,
        cas_manifest: cas_manifest_result,
        metadata_file: Some(metadata_path.to_string_lossy().to_string()),
        rotation: rotation_result,
        error: error_msg,
    };

    println!("{}", serde_json::to_string_pretty(&result).unwrap());

    if !all_ok {
        std::process::exit(1);
    }
}

fn mode_str(mode: Mode) -> &'static str {
    match mode {
        Mode::Full => "full",
        Mode::Incremental => "incremental",
    }
}

/// Kjør pg_dump via docker exec. Returnerer dump-filsti og størrelse.
async fn run_pg_dump(
    container: &str,
    db_user: &str,
    db_name: &str,
    pg_dir: &Path,
    timestamp: &str,
) -> Result<PgDumpResult, String> {
    // Sjekk at containeren kjører
    let inspect = tokio::process::Command::new("docker")
        .args(["inspect", container, "--format", "{{.State.Running}}"])
        .output()
        .await
        .map_err(|e| format!("Kunne ikke kjøre docker inspect: {e}"))?;

    let running = String::from_utf8_lossy(&inspect.stdout).trim().to_string();
    if running != "true" {
        return Err(format!("PostgreSQL-container {container} kjører ikke"));
    }

    let dump_file = pg_dir.join(format!("{db_name}_{timestamp}.dump"));

    // pg_dump via docker exec, output til fil
    let output = tokio::process::Command::new("docker")
        .args([
            "exec", container, "pg_dump", "-U", db_user, "-Fc", db_name,
        ])
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped())
        .output()
        .await
        .map_err(|e| format!("Kunne ikke kjøre pg_dump: {e}"))?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(format!("pg_dump feilet: {stderr}"));
    }

    // Skriv dump til fil
    tokio::fs::write(&dump_file, &output.stdout)
        .await
        .map_err(|e| format!("Kunne ikke skrive dump-fil: {e}"))?;

    let size_bytes = output.stdout.len() as u64;
    if size_bytes < 100 {
        // Slett tom/korrupt dump
        let _ = tokio::fs::remove_file(&dump_file).await;
        return Err(format!(
            "Dump-filen er for liten ({size_bytes} bytes), noe gikk galt"
        ));
    }

    Ok(PgDumpResult {
        path: dump_file.to_string_lossy().to_string(),
        size_bytes,
        size_human: human_size(size_bytes),
    })
}

/// Bygg CAS-manifest: liste over alle filer med hash og størrelse.
///
/// Full modus: lister alle filer.
/// Inkrementell: lister kun filer nyere enn forrige manifest.
async fn build_cas_manifest(
    cas_root: &str,
    manifest_dir: &Path,
    timestamp: &str,
    mode: Mode,
) -> Result<CasManifestResult, String> {
    let cas_path = Path::new(cas_root);
    if !cas_path.exists() {
        return Err(format!("CAS-katalog {cas_root} finnes ikke"));
    }

    // For inkrementell: finn tidsstempel for forrige manifest
    let cutoff = if mode == Mode::Incremental {
        find_last_manifest_time(manifest_dir).await
    } else {
        None
    };

    // Bruk find for å liste CAS-filer effektivt
    let mut args = vec![cas_root.to_string(), "-type".to_string(), "f".to_string()];
    if let Some(ref cutoff_time) = cutoff {
        // Kun filer nyere enn cutoff
        args.extend(["-newer".to_string(), cutoff_time.clone()]);
    }

    let output = tokio::process::Command::new("find")
        .args(&args)
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped())
        .output()
        .await
        .map_err(|e| format!("Kunne ikke liste CAS-filer: {e}"))?;

    let file_list = String::from_utf8_lossy(&output.stdout);
    let mut file_count: u64 = 0;
    let mut total_size: u64 = 0;
    let mut manifest_lines = Vec::new();

    for line in file_list.lines() {
        let line = line.trim();
        if line.is_empty() {
            continue;
        }
        let path = Path::new(line);
        let size = match tokio::fs::metadata(path).await {
            Ok(m) => m.len(),
            Err(_) => continue,
        };

        // Extraher hash fra filnavn (siste path-komponent)
        let hash = path
            .file_name()
            .map(|n| n.to_string_lossy().to_string())
            .unwrap_or_default();

        manifest_lines.push(format!("{hash}\t{size}"));
        file_count += 1;
        total_size += size;
    }

    let manifest_name = if mode == Mode::Incremental {
        format!("cas_incremental_{timestamp}.tsv")
    } else {
        format!("cas_full_{timestamp}.tsv")
    };
    let manifest_path = manifest_dir.join(&manifest_name);

    // Skriv manifest (TSV: hash\tsize)
    let mode_label = mode_str(mode);
    let header = format!("# CAS manifest — {mode_label} — {timestamp}\n# hash\tsize_bytes\n");
    let content = format!("{}{}\n", header, manifest_lines.join("\n"));
    tokio::fs::write(&manifest_path, &content)
        .await
        .map_err(|e| format!("Kunne ikke skrive CAS-manifest: {e}"))?;

    Ok(CasManifestResult {
        path: manifest_path.to_string_lossy().to_string(),
        file_count,
        total_size_bytes: total_size,
        total_size_human: human_size(total_size),
    })
}

/// Finn tidspunkt for forrige manifest (brukes som cutoff for inkrementell).
async fn find_last_manifest_time(manifest_dir: &Path) -> Option<String> {
    let mut entries = match tokio::fs::read_dir(manifest_dir).await {
        Ok(e) => e,
        Err(_) => return None,
    };

    let mut latest_path: Option<PathBuf> = None;
    let mut latest_modified = std::time::SystemTime::UNIX_EPOCH;

    while let Ok(Some(entry)) = entries.next_entry().await {
        let name = entry.file_name().to_string_lossy().to_string();
        if name.starts_with("cas_") && name.ends_with(".tsv") {
            if let Ok(meta) = entry.metadata().await {
                if let Ok(modified) = meta.modified() {
                    if modified > latest_modified {
                        latest_modified = modified;
                        latest_path = Some(entry.path());
                    }
                }
            }
        }
    }

    latest_path.map(|p| p.to_string_lossy().to_string())
}

/// Slett PG-dumper eldre enn retain_days.
async fn rotate_old_backups(pg_dir: &Path, retain_days: u32) -> Result<RotationResult, String> {
    let cutoff = std::time::SystemTime::now()
        - std::time::Duration::from_secs(retain_days as u64 * 86400);

    let mut entries = tokio::fs::read_dir(pg_dir)
        .await
        .map_err(|e| format!("Kunne ikke lese backup-katalog: {e}"))?;

    let mut deleted_count = 0u32;

    while let Ok(Some(entry)) = entries.next_entry().await {
        let name = entry.file_name().to_string_lossy().to_string();
        if !name.ends_with(".dump") {
            continue;
        }
        if let Ok(meta) = entry.metadata().await {
            if let Ok(modified) = meta.modified() {
                if modified < cutoff {
                    if tokio::fs::remove_file(entry.path()).await.is_ok() {
                        tracing::info!(file = %name, "Slettet gammel dump");
                        deleted_count += 1;
                    }
                }
            }
        }
    }

    Ok(RotationResult {
        deleted_count,
        retain_days,
    })
}

/// Formater bytes til lesbar størrelse.
fn human_size(bytes: u64) -> String {
    const UNITS: &[&str] = &["B", "KiB", "MiB", "GiB", "TiB"];
    let mut size = bytes as f64;
    for unit in UNITS {
        if size < 1024.0 {
            return format!("{size:.1} {unit}");
        }
        size /= 1024.0;
    }
    format!("{size:.1} PiB")
}
