//! `policy` subcommand — render and validate `BackupPolicy` declarations.
//!
//! This is the first end-to-end mode wired into the binary. It exercises:
//!
//! - Reading workspace declarations via `nickel export --import-path …`.
//! - Deserialising the resulting JSON into [`crate::policy`] structs.
//! - Reporting structured information about policies, groups, and system
//!   backup definitions without touching destinations or vault.
//!
//! Subsequent modes (`backup`, `restore`, `mount`, etc.) build on the same
//! deserialisation pipeline implemented here.

use std::path::Path;
use std::process::Command;

use crate::error::{BackupError, Result};
use crate::policy::BackupPolicy;

/// Render a Nickel file to JSON via the `nickel` CLI.
///
/// Honours the `nickel_import_path` provided in [`crate::config::PoliciesRef`].
pub fn nickel_export_to_json(path: &Path, import_path: &str) -> Result<serde_json::Value> {
    let output = Command::new("nickel")
        .args(["export", "--format", "json", "--import-path", import_path])
        .arg(path)
        .output()
        .map_err(|e| BackupError::Policy(format!(
            "failed to invoke `nickel export` on {:?}: {}", path, e
        )))?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(BackupError::Policy(format!(
            "nickel export failed for {:?}: {}", path, stderr
        )));
    }

    serde_json::from_slice::<serde_json::Value>(&output.stdout).map_err(BackupError::from)
}

/// Result of validating a single policy file.
#[derive(Debug)]
pub struct PolicyValidation {
    /// Path to the source Nickel file.
    pub source: String,
    /// Whether the file successfully exported and deserialised.
    pub ok: bool,
    /// Error message if validation failed.
    pub error: Option<String>,
    /// Component count (top-level keys in the JSON object).
    pub component_count: usize,
    /// Names of components for which `concerns.backup.kind == "enabled"`.
    pub enabled_backup_components: Vec<String>,
}

/// Validate a single Nickel policy file by exporting and deserialising it.
pub fn validate_policy(path: &Path, import_path: &str) -> PolicyValidation {
    let source = path.display().to_string();
    let json = match nickel_export_to_json(path, import_path) {
        Ok(v) => v,
        Err(e) => {
            return PolicyValidation {
                source,
                ok: false,
                error: Some(e.to_string()),
                component_count: 0,
                enabled_backup_components: Vec::new(),
            };
        }
    };

    let object = match json.as_object() {
        Some(map) => map,
        None => {
            return PolicyValidation {
                source,
                ok: false,
                error: Some("policy file did not export a JSON object".to_string()),
                component_count: 0,
                enabled_backup_components: Vec::new(),
            };
        }
    };

    let mut enabled = Vec::new();
    for (component_name, component_value) in object {
        // Best-effort: locate concerns.backup.kind inside the component.
        // Components produced by `ext.make_X` helpers may nest the policy in
        // different ways; we don't fail on missing concerns here, we just
        // skip when not found. Strict enforcement is the Nickel layer's job.
        if let Some(kind) = component_value
            .pointer("/concerns/backup/kind")
            .and_then(serde_json::Value::as_str)
        {
            if kind == "enabled" {
                enabled.push(component_name.clone());
            }
        }
    }

    PolicyValidation {
        source,
        ok: true,
        error: None,
        component_count: object.len(),
        enabled_backup_components: enabled,
    }
}

/// Try to deserialise a `BackupPolicy` directly from its JSON representation.
/// Used by tests and by future scope-expansion code; not exposed via CLI yet.
pub fn deserialize_backup_policy(value: serde_json::Value) -> Result<BackupPolicy> {
    serde_json::from_value(value).map_err(BackupError::from)
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn deserialize_minimal_backup_policy() {
        let value = json!({
            "provider": { "name": "restic" },
            "destinations": [
                {
                    "name": "primary",
                    "kind": "s3",
                    "uri": "s3:fsn1.hetzner/test",
                    "cred_ref": { "path": "creds/test", "kind": "s3" },
                    "role": "primary"
                },
                {
                    "name": "replica",
                    "kind": "b2",
                    "uri": "b2:test-replica",
                    "cred_ref": { "path": "creds/replica", "kind": "b2" },
                    "role": "replica"
                }
            ],
            "encryption": {
                "path": "backup-manager/master-key",
                "algorithm": "age_x25519"
            },
            "schedule": { "kind": "cron", "cron_expr": "0 2 * * *" },
            "retention": {
                "keep_last": 7,
                "keep_daily": 7,
                "keep_weekly": 4,
                "keep_monthly": 6,
                "keep_yearly": 0
            },
            "scopes": [
                {
                    "kind": "service_full",
                    "name": "main",
                    "paths": ["/data"]
                }
            ],
            "tag_strategy": {
                "component_label": "test-component"
            }
        });

        let policy = deserialize_backup_policy(value).expect("deserialise");
        assert_eq!(policy.destinations.len(), 2);
        assert_eq!(policy.scopes.len(), 1);
        assert_eq!(policy.provider.name, "restic");
    }
}
