#![forbid(unsafe_code)]

use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::{fs, path::Path};

/// Policy definition for signing operations.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Policy {
    /// Default signature algorithm (e.g., "mldsa-87").
    pub default_alg: String,
    /// Artifact digest algorithm (e.g., "sha512").
    pub digest_alg: String,
    /// Allowed signature algorithm identifiers.
    pub allow_algs: Vec<String>,
    /// Required signature structure (m-of-n or required kids).
    pub required_signatures: RequiredSignatures,
    /// Whether offline verification is acceptable.
    pub offline_ok: bool,
    /// Enforce FIPS-only digest algorithms.
    pub require_fips_only: bool,
    /// Optional comments (ignored by canonical hash).
    pub comments: Option<String>,
}

/// Canonical hash of a policy (for binding to signatures).
pub fn canonical_hash(policy: &Policy) -> [u8; 32] {
    // Serialize a reduced, order-stable view.
    #[derive(Serialize)]
    struct Canon<'a> {
        default_alg: &'a str,
        digest_alg: &'a str,
        allow_algs: &'a [String],
        required_signatures: &'a RequiredSignatures,
        offline_ok: bool,
        require_fips_only: bool,
    }
    let canon = Canon {
        default_alg: &policy.default_alg,
        digest_alg: &policy.digest_alg,
        allow_algs: &policy.allow_algs,
        required_signatures: &policy.required_signatures,
        offline_ok: policy.offline_ok,
        require_fips_only: policy.require_fips_only,
    };
    let json = serde_json::to_vec(&canon).expect("serialize policy");
    let mut h = Sha256::new();
    h.update(json);
    let mut out = [0u8; 32];
    out.copy_from_slice(&h.finalize());
    out
}

/// Quorum policy (m-of-n) or explicit required signers.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum RequiredSignatures {
    Quorum { m: u8, n: u8 },
    RequiredKids { required: Vec<String> },
}

impl RequiredSignatures {
    fn validate(&self) -> Result<(), ValidationError> {
        match self {
            RequiredSignatures::Quorum { m, n } => {
                if *m == 0 || *n == 0 || m > n {
                    return Err(ValidationError::InvalidQuorum { m: *m, n: *n });
                }
            }
            RequiredSignatures::RequiredKids { required } => {
                if required.is_empty() {
                    return Err(ValidationError::InvalidQuorum { m: 0, n: 0 });
                }
            }
        }
        Ok(())
    }
}

/// Error while loading or parsing a policy file.
#[derive(Debug)]
pub enum Error {
    Io(std::io::Error),
    Parse(String),
    Unsupported(&'static str),
}

impl From<std::io::Error> for Error {
    fn from(err: std::io::Error) -> Self {
        Error::Io(err)
    }
}

/// Policy validation failures (enforced at runtime).
#[derive(Debug)]
pub enum ValidationError {
    FipsRequired,
    InvalidQuorum { m: u8, n: u8 },
    QuorumUnsatisfied { required_m: u8, total_n: u8, collected: usize },
    Level5Requirement(String),
}

/// Explicit serialization formats.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Format {
    Json,
    Yaml,
}

/// Deserialize policy from a string using the provided (or inferred) format.
pub fn load_policy_str(contents: &str, fmt: Option<Format>) -> Result<Policy, Error> {
    match fmt.unwrap_or(Format::Json) {
        Format::Json => {
            #[cfg(feature = "json")]
            {
                serde_json::from_str::<Policy>(contents).map_err(|e| Error::Parse(e.to_string()))
            }
            #[cfg(not(feature = "json"))]
            {
                Err(Error::Unsupported("json feature disabled"))
            }
        }
        Format::Yaml => {
            #[cfg(feature = "yaml")]
            {
                serde_yaml::from_str::<Policy>(contents).map_err(|e| Error::Parse(e.to_string()))
            }
            #[cfg(not(feature = "yaml"))]
            {
                Err(Error::Unsupported("yaml feature disabled"))
            }
        }
    }
}

/// Load policy from disk, inferring format by extension (`.json`, `.yaml`, `.yml`).
pub fn load_policy_file(path: &Path) -> Result<Policy, Error> {
    let data = fs::read_to_string(path)?;
    let fmt = match path.extension().and_then(|s| s.to_str()) {
        Some("yaml") | Some("yml") => Some(Format::Yaml),
        _ => Some(Format::Json),
    };
    load_policy_str(&data, fmt)
}

fn is_level5_sig_alg(alg: &str) -> bool {
    matches!(
        alg,
        "mldsa-87"
            | "slh-dsa-sha2-256s"
            | "slh-dsa-sha2-256f"
            | "slh-dsa-shake-256s"
            | "slh-dsa-shake-256f"
    )
}
