use hmac::{Hmac, Mac};
use serde_json::Value;
use sha2::{Digest, Sha256};
use std::fmt::Write as _;

use crate::pipe::Action;
use crate::security::SecurityError;

type HmacSha256 = Hmac<Sha256>;

pub fn derive_session_key(hmac_seed: &str) -> Result<Vec<u8>, SecurityError> {
    let seed = hex::decode(hmac_seed)?;
    if seed.is_empty() {
        return Err(SecurityError::InvalidSeed(
            "hmac_seed must not be empty".to_string(),
        ));
    }

    let mut hasher = Sha256::new();
    hasher.update(seed);
    hasher.update(b"sgclaw-session-v1");
    Ok(hasher.finalize().to_vec())
}

pub fn sign_command(
    session_key: &[u8],
    seq: u64,
    action: &Action,
    params: &Value,
    expected_domain: &str,
) -> Result<String, SecurityError> {
    if session_key.is_empty() {
        return Err(SecurityError::InvalidSeed(
            "session key must not be empty".to_string(),
        ));
    }

    let mut mac = HmacSha256::new_from_slice(session_key)
        .map_err(|err| SecurityError::Hmac(err.to_string()))?;
    let canonical = format!(
        "{}\n{}\n{}\n{}",
        seq,
        action.as_str(),
        stable_json_string(params)?,
        expected_domain
    );
    mac.update(canonical.as_bytes());

    Ok(hex::encode(mac.finalize().into_bytes()))
}

fn stable_json_string(value: &Value) -> Result<String, SecurityError> {
    match value {
        Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {
            serde_json::to_string(value).map_err(SecurityError::from)
        }
        Value::Array(items) => {
            let mut out = String::from("[");
            for (index, item) in items.iter().enumerate() {
                if index > 0 {
                    out.push(',');
                }
                out.push_str(&stable_json_string(item)?);
            }
            out.push(']');
            Ok(out)
        }
        Value::Object(map) => {
            let mut keys: Vec<&str> = map.keys().map(String::as_str).collect();
            keys.sort_unstable();

            let mut out = String::from("{");
            for (index, key) in keys.iter().enumerate() {
                if index > 0 {
                    out.push(',');
                }
                write!(
                    out,
                    "{}:{}",
                    serde_json::to_string(key)?,
                    stable_json_string(&map[*key])?
                )
                .map_err(|err| SecurityError::Hmac(err.to_string()))?;
            }
            out.push('}');
            Ok(out)
        }
    }
}
