//! Pruning-logikk for CAS-filer.
//!
//! TTL per modalitet, signaler som forlenger levetid, og disk-nødventil.
//! Ref: docs/retninger/maskinrommet.md § CAS og intelligent pruning
//!
//! ## TTL per modalitet
//! | Modalitet     | Standard TTL | Begrunnelse                          |
//! |---------------|-------------|--------------------------------------|
//! | Tekst         | Aldri       | Billig, essensen av innhold          |
//! | Transkripsjon | Aldri       | Tekstrepresentasjon bevarer mening   |
//! | Lyd           | 30 dager    | Transkripsjon bevarer innholdet      |
//! | Bilde         | 30 dager    | Beskrivelse/metadata bevarer kontekst|
//! | Video         | 14 dager    | Dyrest, transkripsjon + thumbnail    |
//!
//! ## Signaler som forlenger levetid
//! - Publishing-edge → behold for alltid
//! - Tilgang (last_accessed_at) innenfor TTL → forleng med ny TTL-periode
//! - Uredigert transkripsjon → utranskribert lyd beholdes
//!
//! ## Disk-nødventil
//! | Terskel | Handling                                                 |
//! |---------|----------------------------------------------------------|
//! | >85%    | Slett generert innhold (TTS, thumbnails — regenererbart) |
//! | >90%    | Aggressiv pruning for alle samlinger                     |
//! | >95%    | Kritisk: alt uten publishing-edge slettes. Tekst beholdes|

use chrono::{DateTime, Utc};
use sqlx::PgPool;
use uuid::Uuid;

use crate::cas::CasStore;

/// Konfigurasjon for pruning.
#[derive(Debug, Clone)]
pub struct PruningConfig {
    /// Standard TTL for lyd i dager.
    pub audio_ttl_days: i64,
    /// Standard TTL for bilder i dager.
    pub image_ttl_days: i64,
    /// Standard TTL for video i dager.
    pub video_ttl_days: i64,
    /// Disk-terskel for å slette generert innhold (prosent).
    pub disk_warning_pct: f64,
    /// Disk-terskel for aggressiv pruning (prosent).
    pub disk_aggressive_pct: f64,
    /// Disk-terskel for kritisk alarm (prosent).
    pub disk_critical_pct: f64,
}

impl Default for PruningConfig {
    fn default() -> Self {
        Self {
            audio_ttl_days: 30,
            image_ttl_days: 30,
            video_ttl_days: 14,
            disk_warning_pct: 85.0,
            disk_aggressive_pct: 90.0,
            disk_critical_pct: 95.0,
        }
    }
}

/// En CAS-fil-kandidat for pruning.
#[derive(Debug, sqlx::FromRow)]
#[allow(dead_code)]
struct PruneCandidate {
    id: Uuid,
    cas_hash: String,
    mime_category: String,
    size_bytes: i64,
    created_at: DateTime<Utc>,
    last_accessed_at: Option<DateTime<Utc>>,
    has_publishing_edge: bool,
    is_generated: bool,
    has_transcription: bool,
}

/// Resultat fra en pruning-kjøring.
#[derive(Debug, Default, serde::Serialize)]
pub struct PruneResult {
    pub candidates_checked: usize,
    pub files_deleted: usize,
    pub bytes_freed: u64,
    pub disk_pct_before: f64,
    pub disk_pct_after: f64,
    pub emergency_level: &'static str,
}

/// Kjør pruning-logikk. Returnerer statistikk.
pub async fn run_pruning(
    db: &PgPool,
    cas: &CasStore,
    config: &PruningConfig,
) -> Result<PruneResult, String> {
    let disk_pct = cas.disk_usage_percent().await.map_err(|e| format!("Disk-sjekk feilet: {e}"))?;

    let emergency_level = if disk_pct >= config.disk_critical_pct {
        "critical"
    } else if disk_pct >= config.disk_aggressive_pct {
        "aggressive"
    } else if disk_pct >= config.disk_warning_pct {
        "warning"
    } else {
        "normal"
    };

    tracing::info!(
        disk_pct = disk_pct,
        level = emergency_level,
        "Pruning startet"
    );

    let mut result = PruneResult {
        disk_pct_before: disk_pct,
        emergency_level,
        ..Default::default()
    };

    // Fase 1: Slett generert innhold ved disk ≥ warning (85%)
    if disk_pct >= config.disk_warning_pct {
        let freed = prune_generated(db, cas).await?;
        result.files_deleted += freed.0;
        result.bytes_freed += freed.1;
        tracing::info!(
            files = freed.0,
            bytes = freed.1,
            "Fase 1: Slettet generert innhold"
        );
    }

    // Fase 2: Standard TTL-basert pruning (kjører alltid)
    let ttl_freed = prune_by_ttl(db, cas, config, emergency_level).await?;
    result.candidates_checked += ttl_freed.0;
    result.files_deleted += ttl_freed.1;
    result.bytes_freed += ttl_freed.2;

    // Fase 3: Kritisk — slett alt uten publishing-edge (unntatt tekst)
    if disk_pct >= config.disk_critical_pct {
        let critical_freed = prune_critical(db, cas).await?;
        result.files_deleted += critical_freed.0;
        result.bytes_freed += critical_freed.1;
        tracing::warn!(
            files = critical_freed.0,
            bytes = critical_freed.1,
            "Fase 3: KRITISK pruning — alt uten publishing-edge slettet"
        );
    }

    // Sjekk disk etter pruning
    result.disk_pct_after = cas
        .disk_usage_percent()
        .await
        .unwrap_or(disk_pct);

    tracing::info!(
        files_deleted = result.files_deleted,
        bytes_freed = result.bytes_freed,
        disk_before = format!("{:.1}%", result.disk_pct_before),
        disk_after = format!("{:.1}%", result.disk_pct_after),
        "Pruning fullført"
    );

    Ok(result)
}

/// Slett generert innhold (TTS, thumbnails osv.) — kan regenereres.
/// Identifiseres ved metadata.tts eller metadata.generated = true.
async fn prune_generated(db: &PgPool, cas: &CasStore) -> Result<(usize, u64), String> {
    let rows: Vec<(String, Uuid)> = sqlx::query_as(
        r#"
        SELECT metadata->>'cas_hash' AS cas_hash, id
        FROM nodes
        WHERE node_kind = 'media'
          AND metadata->>'cas_hash' IS NOT NULL
          AND (
            metadata ? 'tts'
            OR metadata->>'generated' = 'true'
          )
        "#,
    )
    .fetch_all(db)
    .await
    .map_err(|e| format!("Spørring for generert innhold feilet: {e}"))?;

    let mut deleted = 0usize;
    let mut freed = 0u64;

    for (hash, node_id) in &rows {
        match cas.delete(hash).await {
            Ok(bytes) if bytes > 0 => {
                deleted += 1;
                freed += bytes;
                log_prune(db, *node_id, hash, bytes, "generated_cleanup").await;
            }
            Ok(_) => {} // Allerede borte
            Err(e) => {
                tracing::warn!(hash = %hash, error = %e, "Kunne ikke slette generert fil");
            }
        }
    }

    Ok((deleted, freed))
}

/// TTL-basert pruning: slett CAS-filer som har utløpt basert på modalitet.
/// Ved aggressive/critical senkes TTL drastisk.
async fn prune_by_ttl(
    db: &PgPool,
    cas: &CasStore,
    config: &PruningConfig,
    emergency_level: &str,
) -> Result<(usize, usize, u64), String> {
    // Hent kandidater: media-noder med CAS-hash, MIME, alder, edges, tilgangstid
    let candidates: Vec<PruneCandidate> = sqlx::query_as(
        r#"
        SELECT
            n.id,
            n.metadata->>'cas_hash' AS cas_hash,
            CASE
                WHEN n.metadata->>'mime' LIKE 'audio/%' THEN 'audio'
                WHEN n.metadata->>'mime' LIKE 'image/%' THEN 'image'
                WHEN n.metadata->>'mime' LIKE 'video/%' THEN 'video'
                ELSE 'other'
            END AS mime_category,
            COALESCE((n.metadata->>'size_bytes')::bigint, (n.metadata->>'size')::bigint, 0) AS size_bytes,
            n.created_at,
            n.last_accessed_at,
            EXISTS(
                SELECT 1 FROM edges e
                WHERE (e.source_id = n.id OR e.target_id = n.id)
                  AND e.edge_type IN ('belongs_to', 'has_media')
                  AND EXISTS(
                    SELECT 1 FROM edges pub
                    WHERE pub.edge_type = 'publishing'
                      AND (pub.source_id = e.target_id OR pub.source_id = e.source_id)
                  )
            ) AS has_publishing_edge,
            COALESCE(n.metadata ? 'tts' OR n.metadata->>'generated' = 'true', false) AS is_generated,
            EXISTS(
                SELECT 1 FROM transcription_segments ts
                WHERE ts.node_id = n.id
            ) AS has_transcription
        FROM nodes n
        WHERE n.node_kind = 'media'
          AND n.metadata->>'cas_hash' IS NOT NULL
        ORDER BY n.created_at ASC
        "#,
    )
    .fetch_all(db)
    .await
    .map_err(|e| format!("TTL-kandidatspørring feilet: {e}"))?;

    let now = Utc::now();
    let mut checked = 0usize;
    let mut deleted = 0usize;
    let mut freed = 0u64;

    for c in &candidates {
        checked += 1;

        // Tekst/transkripsjon slettes aldri
        if c.mime_category == "other" {
            continue;
        }

        // Publishing-edge = behold for alltid (unntatt ved kritisk)
        if c.has_publishing_edge && emergency_level != "critical" {
            continue;
        }

        // Bestem TTL basert på modalitet og nødsituasjon
        let ttl_days = match emergency_level {
            "critical" => 0, // Slett alt umiddelbart
            "aggressive" => match c.mime_category.as_str() {
                "audio" => config.audio_ttl_days / 3, // 10 dager
                "image" => config.image_ttl_days / 3,
                "video" => 1, // Video slettes nesten umiddelbart
                _ => i64::MAX,
            },
            _ => match c.mime_category.as_str() {
                "audio" => config.audio_ttl_days,
                "image" => config.image_ttl_days,
                "video" => config.video_ttl_days,
                _ => i64::MAX,
            },
        };

        if ttl_days == i64::MAX {
            continue;
        }

        // Beregn effektiv alder — siste tilgang forlenger levetiden
        let reference_time = c.last_accessed_at.unwrap_or(c.created_at);
        let age_days = (now - reference_time).num_days();

        if age_days < ttl_days {
            continue; // Ikke utløpt ennå
        }

        // Lyd uten transkripsjon: behold lengre (trenger transkribering først)
        if c.mime_category == "audio" && !c.has_transcription && emergency_level == "normal" {
            tracing::debug!(
                node_id = %c.id,
                "Beholder utranskribert lyd — trenger transkribering"
            );
            continue;
        }

        // Allerede slettet generert innhold i fase 1 — hopp over
        if c.is_generated {
            continue;
        }

        // Slett filen
        match cas.delete(&c.cas_hash).await {
            Ok(bytes) if bytes > 0 => {
                deleted += 1;
                freed += bytes;
                log_prune(db, c.id, &c.cas_hash, bytes, "ttl_expired").await;
            }
            Ok(_) => {} // Allerede borte
            Err(e) => {
                tracing::warn!(
                    hash = %c.cas_hash,
                    node_id = %c.id,
                    error = %e,
                    "Kunne ikke slette CAS-fil"
                );
            }
        }
    }

    Ok((checked, deleted, freed))
}

/// Kritisk pruning: slett ALT uten publishing-edge (unntatt tekst/transkripsjon).
async fn prune_critical(db: &PgPool, cas: &CasStore) -> Result<(usize, u64), String> {
    let rows: Vec<(String, Uuid, i64)> = sqlx::query_as(
        r#"
        SELECT
            n.metadata->>'cas_hash' AS cas_hash,
            n.id,
            COALESCE((n.metadata->>'size_bytes')::bigint, 0) AS size_bytes
        FROM nodes n
        WHERE n.node_kind = 'media'
          AND n.metadata->>'cas_hash' IS NOT NULL
          AND NOT EXISTS(
            SELECT 1 FROM edges e
            WHERE (e.source_id = n.id OR e.target_id = n.id)
              AND e.edge_type IN ('belongs_to', 'has_media')
              AND EXISTS(
                SELECT 1 FROM edges pub
                WHERE pub.edge_type = 'publishing'
                  AND (pub.source_id = e.target_id OR pub.source_id = e.source_id)
              )
          )
        "#,
    )
    .fetch_all(db)
    .await
    .map_err(|e| format!("Kritisk pruning-spørring feilet: {e}"))?;

    let mut deleted = 0usize;
    let mut freed = 0u64;

    for (hash, node_id, _size) in &rows {
        match cas.delete(hash).await {
            Ok(bytes) if bytes > 0 => {
                deleted += 1;
                freed += bytes;
                log_prune(db, *node_id, hash, bytes, "critical_emergency").await;
            }
            Ok(_) => {}
            Err(e) => {
                tracing::warn!(hash = %hash, error = %e, "Kritisk: kunne ikke slette CAS-fil");
            }
        }
    }

    Ok((deleted, freed))
}

/// Logg en pruning-hendelse til resource_usage_log.
async fn log_prune(db: &PgPool, node_id: Uuid, hash: &str, bytes: u64, reason: &str) {
    let detail = serde_json::json!({
        "hash": hash,
        "size_bytes": bytes,
        "operation": "delete",
        "reason": reason,
    });

    let _ = sqlx::query(
        r#"
        INSERT INTO resource_usage_log (target_node_id, resource_type, detail)
        VALUES ($1, 'cas', $2)
        "#,
    )
    .bind(node_id)
    .bind(&detail)
    .execute(db)
    .await
    .map_err(|e| {
        tracing::warn!(error = %e, "Kunne ikke logge pruning-hendelse");
    });
}

/// Oppdater last_accessed_at for en node basert på CAS-hash.
/// Kalles fra serving.rs når en CAS-fil hentes.
pub async fn touch_access(db: &PgPool, cas_hash: &str) {
    let _ = sqlx::query(
        "UPDATE nodes SET last_accessed_at = now() WHERE metadata->>'cas_hash' = $1",
    )
    .bind(cas_hash)
    .execute(db)
    .await
    .map_err(|e| {
        tracing::debug!(error = %e, "Kunne ikke oppdatere last_accessed_at");
    });
}

/// Start periodisk pruning-loop som bakgrunnsoppgave.
/// Kjører hvert 6. time, eller oftere ved høy diskbruk.
pub fn start_pruning_loop(db: PgPool, cas: CasStore) {
    let config = PruningConfig::default();

    tokio::spawn(async move {
        // Vent 60 sekunder etter oppstart før første kjøring
        tokio::time::sleep(std::time::Duration::from_secs(60)).await;
        tracing::info!("Pruning-loop startet (intervall: 6t, nødsjekk: 10min)");

        loop {
            match run_pruning(&db, &cas, &config).await {
                Ok(result) => {
                    if result.files_deleted > 0 {
                        tracing::info!(
                            deleted = result.files_deleted,
                            freed_mb = result.bytes_freed / 1_048_576,
                            "Pruning: {} filer slettet, {} MB frigitt",
                            result.files_deleted,
                            result.bytes_freed / 1_048_576,
                        );
                    }

                    // Ved høy diskbruk: sjekk igjen om 10 minutter
                    let sleep_secs = if result.disk_pct_after >= config.disk_warning_pct {
                        tracing::warn!(
                            disk_pct = result.disk_pct_after,
                            "Disk fortsatt høy — neste pruning om 10 min"
                        );
                        600 // 10 minutter
                    } else {
                        6 * 3600 // 6 timer
                    };

                    tokio::time::sleep(std::time::Duration::from_secs(sleep_secs)).await;
                }
                Err(e) => {
                    tracing::error!(error = %e, "Pruning feilet");
                    // Vent 30 minutter ved feil
                    tokio::time::sleep(std::time::Duration::from_secs(1800)).await;
                }
            }
        }
    });
}
