use std::path::PathBuf;

use anyhow::{Context, Result};
use base64::{engine::general_purpose::STANDARD, Engine as _};
use chrono::{DateTime, Utc};
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use rand::{rngs::OsRng, RngCore};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use zeroize::Zeroize;

use crate::{
    approvals::ApprovalScope,
    context, fs_util, logs,
    policy::AccessRequest,
    vault::{self, VaultEnvelope},
};

const RECEIPT_SCHEMA_VERSION: u32 = 1;
const SIGNATURE_ALGORITHM: &str = "ed25519";
const SIGNING_SEED_LEN: usize = 32;

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApprovalReceiptPayload {
    pub schema_version: u32,
    pub grant_id: uuid::Uuid,
    pub request_id: uuid::Uuid,
    pub project: String,
    pub agent: Option<String>,
    pub branch: Option<String>,
    pub command_hash: String,
    pub requested_env: Vec<String>,
    pub approved_env: Vec<String>,
    pub scope: ApprovalScope,
    pub expires_at: Option<DateTime<Utc>>,
    pub critical_confirmation: bool,
    pub created_at: DateTime<Utc>,
    pub signer_key_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub agent_key_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub verified_worktree: Option<PathBuf>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub verified_git_remote: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub verified_commit: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApprovalReceipt {
    pub payload: ApprovalReceiptPayload,
    pub payload_hash: String,
    pub signer_key_id: String,
    pub signature_algorithm: String,
    pub signature: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SigningKeyCiphertext {
    pub signer_key_id: String,
    pub encrypted_private_key: VaultEnvelope,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApprovalKeyFile {
    pub version: u32,
    pub project: String,
    pub key_id: String,
    pub public_key: String,
    pub encrypted_private_key: VaultEnvelope,
    pub created_at: DateTime<Utc>,
}

#[derive(Debug)]
pub struct SessionSigningKey {
    pub signer_key_id: String,
    seed: [u8; SIGNING_SEED_LEN],
}

impl Drop for SessionSigningKey {
    fn drop(&mut self) {
        self.seed.zeroize();
    }
}

pub fn keys_dir() -> PathBuf {
    logs::ward_home().join("keys")
}

pub fn project_key_path(project: &str) -> PathBuf {
    keys_dir().join(format!("{}.json", project_path_id(project)))
}

pub fn ensure_project_key(project: &str, passphrase: &str) -> Result<ApprovalKeyFile> {
    let path = project_key_path(project);
    if path.exists() {
        let key_file = read_project_key(project)?;
        let mut seed = decrypt_seed(&key_file.encrypted_private_key, passphrase)
            .with_context(|| format!("failed to decrypt approval key for project {project}"))?;
        seed.zeroize();
        return Ok(key_file);
    }

    let mut seed = [0_u8; SIGNING_SEED_LEN];
    OsRng.fill_bytes(&mut seed);
    let signing_key = SigningKey::from_bytes(&seed);
    let public_key = signing_key.verifying_key().to_bytes();
    let key_id = key_id_for_public_key(&public_key);
    let encrypted_private_key = encrypt_seed(&seed, passphrase)?;
    seed.zeroize();

    let key_file = ApprovalKeyFile {
        version: 1,
        project: project.to_string(),
        key_id,
        public_key: STANDARD.encode(public_key),
        encrypted_private_key,
        created_at: Utc::now(),
    };
    write_project_key(&key_file)?;
    Ok(key_file)
}

pub fn session_signing_key_ciphertext(
    project: &str,
    project_passphrase: &str,
    session_secret: &str,
) -> Result<SigningKeyCiphertext> {
    let key_file = ensure_project_key(project, project_passphrase)?;
    let mut seed = decrypt_seed(&key_file.encrypted_private_key, project_passphrase)?;
    let encrypted_private_key = encrypt_seed(&seed, session_secret)?;
    seed.zeroize();
    Ok(SigningKeyCiphertext {
        signer_key_id: key_file.key_id,
        encrypted_private_key,
    })
}

pub fn decrypt_session_signing_key(
    ciphertext: &SigningKeyCiphertext,
    session_secret: &str,
) -> Result<SessionSigningKey> {
    let seed = decrypt_seed(&ciphertext.encrypted_private_key, session_secret)
        .context("failed to decrypt session signing key")?;
    Ok(SessionSigningKey {
        signer_key_id: ciphertext.signer_key_id.clone(),
        seed,
    })
}

pub fn build_payload(
    access: &AccessRequest,
    grant_id: uuid::Uuid,
    request_id: uuid::Uuid,
    approved_env: &[String],
    scope: ApprovalScope,
    expires_at: Option<DateTime<Utc>>,
    critical_confirmation: bool,
    created_at: DateTime<Utc>,
    signer_key_id: String,
) -> ApprovalReceiptPayload {
    build_payload_with_context(
        access,
        grant_id,
        request_id,
        approved_env,
        scope,
        expires_at,
        critical_confirmation,
        created_at,
        signer_key_id,
        None,
    )
}

#[allow(clippy::too_many_arguments)]
pub fn build_payload_with_context(
    access: &AccessRequest,
    grant_id: uuid::Uuid,
    request_id: uuid::Uuid,
    approved_env: &[String],
    scope: ApprovalScope,
    expires_at: Option<DateTime<Utc>>,
    critical_confirmation: bool,
    created_at: DateTime<Utc>,
    signer_key_id: String,
    verified_context: Option<&context::VerifiedContext>,
) -> ApprovalReceiptPayload {
    ApprovalReceiptPayload {
        schema_version: RECEIPT_SCHEMA_VERSION,
        grant_id,
        request_id,
        project: access.project.clone(),
        agent: access.agent.clone(),
        branch: access.branch.clone(),
        command_hash: command_hash(&access.command),
        requested_env: sorted_strings(&access.env),
        approved_env: sorted_strings(approved_env),
        scope,
        expires_at,
        critical_confirmation,
        created_at,
        signer_key_id,
        agent_key_id: verified_context.map(|context| context.agent_key_id.clone()),
        verified_worktree: verified_context.map(|context| context.worktree.clone()),
        verified_git_remote: verified_context.map(|context| context.git_remote.clone()),
        verified_commit: verified_context.map(|context| context.commit.clone()),
    }
}

pub fn sign_payload(
    payload: ApprovalReceiptPayload,
    session_key: &SessionSigningKey,
) -> Result<ApprovalReceipt> {
    if payload.signer_key_id != session_key.signer_key_id {
        anyhow::bail!("approval receipt signer key id does not match active signing key");
    }
    let canonical = canonical_payload_bytes(&payload);
    let payload_hash = hash_bytes(&canonical);
    let signing_key = SigningKey::from_bytes(&session_key.seed);
    let signature = signing_key.sign(&canonical);
    Ok(ApprovalReceipt {
        payload,
        payload_hash,
        signer_key_id: session_key.signer_key_id.clone(),
        signature_algorithm: SIGNATURE_ALGORITHM.to_string(),
        signature: STANDARD.encode(signature.to_bytes()),
    })
}

pub fn verify_receipt_signature(project: &str, receipt: &ApprovalReceipt) -> bool {
    let Ok(key_file) = read_project_key(project) else {
        return false;
    };
    if key_file.key_id != receipt.signer_key_id
        || key_file.key_id != receipt.payload.signer_key_id
        || receipt.signature_algorithm != SIGNATURE_ALGORITHM
    {
        return false;
    }
    let canonical = canonical_payload_bytes(&receipt.payload);
    if hash_bytes(&canonical) != receipt.payload_hash {
        return false;
    }
    let Ok(public_key_bytes) = STANDARD.decode(&key_file.public_key) else {
        return false;
    };
    let Ok(public_key_bytes) = <[u8; 32]>::try_from(public_key_bytes.as_slice()) else {
        return false;
    };
    let Ok(signature_bytes) = STANDARD.decode(&receipt.signature) else {
        return false;
    };
    let verifying_key = VerifyingKey::from_bytes(&public_key_bytes).ok();
    let signature = Signature::try_from(signature_bytes.as_slice()).ok();
    verifying_key
        .zip(signature)
        .is_some_and(|(verifying_key, signature)| {
            verifying_key.verify(&canonical, &signature).is_ok()
        })
}

pub fn command_hash(command: &str) -> String {
    hash_bytes(command.as_bytes())
}

pub fn canonical_payload_bytes(payload: &ApprovalReceiptPayload) -> Vec<u8> {
    serde_json::to_vec(payload).expect("approval receipt payload serialization is infallible")
}

fn read_project_key(project: &str) -> Result<ApprovalKeyFile> {
    let path = project_key_path(project);
    let contents = std::fs::read_to_string(&path)
        .with_context(|| format!("failed to read {}", path.display()))?;
    let key_file = serde_json::from_str::<ApprovalKeyFile>(&contents)
        .with_context(|| format!("failed to parse {}", path.display()))?;
    if key_file.version != 1 {
        anyhow::bail!("unsupported approval key version {}", key_file.version);
    }
    if key_file.project != project {
        anyhow::bail!("approval key project mismatch");
    }
    Ok(key_file)
}

fn write_project_key(key_file: &ApprovalKeyFile) -> Result<()> {
    fs_util::ensure_private_dir(&keys_dir())?;
    let path = project_key_path(&key_file.project);
    let contents =
        serde_json::to_string_pretty(key_file).expect("approval key serialization is infallible");
    fs_util::write_private_file(&path, format!("{contents}\n").as_bytes())
}

fn encrypt_seed(seed: &[u8; SIGNING_SEED_LEN], passphrase: &str) -> Result<VaultEnvelope> {
    vault::encrypt_env(&STANDARD.encode(seed), passphrase)
}

fn decrypt_seed(envelope: &VaultEnvelope, passphrase: &str) -> Result<[u8; SIGNING_SEED_LEN]> {
    let encoded = vault::decrypt_env(envelope, passphrase)?;
    let mut bytes = STANDARD
        .decode(encoded.trim())
        .context("approval signing key is not valid base64")?;
    let seed = <[u8; SIGNING_SEED_LEN]>::try_from(bytes.as_slice())
        .map_err(|_| anyhow::anyhow!("approval signing key has invalid length"))?;
    bytes.zeroize();
    Ok(seed)
}

fn sorted_strings(values: &[String]) -> Vec<String> {
    let mut sorted = values.to_vec();
    sorted.sort();
    sorted
}

fn hash_bytes(bytes: &[u8]) -> String {
    let mut hasher = Sha256::new();
    hasher.update(bytes);
    hex::encode(hasher.finalize())
}

fn key_id_for_public_key(public_key: &[u8; 32]) -> String {
    format!("{SIGNATURE_ALGORITHM}:{}", hash_bytes(public_key))
}

fn project_path_id(project: &str) -> String {
    hash_bytes(project.as_bytes())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::approvals::ApprovalScope;
    use serial_test::serial;
    use std::sync::{Mutex, OnceLock};

    fn env_lock() -> std::sync::MutexGuard<'static, ()> {
        static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
        LOCK.get_or_init(|| Mutex::new(())).lock().unwrap()
    }

    fn access(env: Vec<&str>) -> AccessRequest {
        AccessRequest {
            project: "demo".to_string(),
            agent: Some("codex".to_string()),
            branch: Some("main".to_string()),
            action: Some("Run dev server".to_string()),
            command: "pnpm dev".to_string(),
            env: env.into_iter().map(str::to_string).collect(),
        }
    }

    #[test]
    #[serial]
    fn approval_key_generation_round_trips_and_rejects_wrong_passphrase() {
        let _guard = env_lock();
        let home = tempfile::tempdir().unwrap();
        std::env::set_var("WARD_HOME", home.path());

        let key_file = ensure_project_key("demo", "1234").unwrap();
        assert_eq!(key_file.project, "demo");
        assert!(ensure_project_key("demo", "wrong").is_err());
        assert_eq!(
            ensure_project_key("demo", "1234").unwrap().key_id,
            key_file.key_id
        );

        std::env::remove_var("WARD_HOME");
    }

    #[test]
    #[serial]
    fn session_signing_ciphertext_round_trips_and_rejects_wrong_secret() {
        let _guard = env_lock();
        let home = tempfile::tempdir().unwrap();
        std::env::set_var("WARD_HOME", home.path());

        let ciphertext = session_signing_key_ciphertext("demo", "1234", "session").unwrap();
        let session_key = decrypt_session_signing_key(&ciphertext, "session").unwrap();
        assert_eq!(session_key.signer_key_id, ciphertext.signer_key_id);
        assert!(decrypt_session_signing_key(&ciphertext, "wrong").is_err());

        std::env::remove_var("WARD_HOME");
    }

    #[test]
    #[serial]
    fn canonical_receipt_is_deterministic_for_reordered_envs() {
        let _guard = env_lock();
        let home = tempfile::tempdir().unwrap();
        std::env::set_var("WARD_HOME", home.path());
        let ciphertext = session_signing_key_ciphertext("demo", "1234", "session").unwrap();
        let session_key = decrypt_session_signing_key(&ciphertext, "session").unwrap();
        let now = Utc::now();
        let first = build_payload(
            &access(vec!["B_KEY", "A_KEY"]),
            uuid::Uuid::nil(),
            uuid::Uuid::nil(),
            &["B_KEY".to_string(), "A_KEY".to_string()],
            ApprovalScope::Always,
            None,
            false,
            now,
            session_key.signer_key_id.clone(),
        );
        let second = build_payload(
            &access(vec!["A_KEY", "B_KEY"]),
            uuid::Uuid::nil(),
            uuid::Uuid::nil(),
            &["A_KEY".to_string(), "B_KEY".to_string()],
            ApprovalScope::Always,
            None,
            false,
            now,
            session_key.signer_key_id.clone(),
        );

        assert_eq!(
            canonical_payload_bytes(&first),
            canonical_payload_bytes(&second)
        );

        std::env::remove_var("WARD_HOME");
    }

    #[test]
    #[serial]
    fn signature_verifies_and_fails_after_payload_change() {
        let _guard = env_lock();
        let home = tempfile::tempdir().unwrap();
        std::env::set_var("WARD_HOME", home.path());
        let ciphertext = session_signing_key_ciphertext("demo", "1234", "session").unwrap();
        let session_key = decrypt_session_signing_key(&ciphertext, "session").unwrap();
        let payload = build_payload(
            &access(vec!["DATABASE_URL"]),
            uuid::Uuid::new_v4(),
            uuid::Uuid::new_v4(),
            &["DATABASE_URL".to_string()],
            ApprovalScope::Always,
            None,
            false,
            Utc::now(),
            session_key.signer_key_id.clone(),
        );
        let mut receipt = sign_payload(payload, &session_key).unwrap();

        assert!(verify_receipt_signature("demo", &receipt));
        receipt.payload.scope = ApprovalScope::Session;
        assert!(!verify_receipt_signature("demo", &receipt));

        std::env::remove_var("WARD_HOME");
    }

    #[test]
    #[serial]
    fn signing_and_verification_reject_bad_metadata_and_key_files() {
        let _guard = env_lock();
        let home = tempfile::tempdir().unwrap();
        std::env::set_var("WARD_HOME", home.path());
        let ciphertext = session_signing_key_ciphertext("demo", "1234", "session").unwrap();
        let session_key = decrypt_session_signing_key(&ciphertext, "session").unwrap();
        let payload = build_payload(
            &access(vec!["DATABASE_URL"]),
            uuid::Uuid::new_v4(),
            uuid::Uuid::new_v4(),
            &["DATABASE_URL".to_string()],
            ApprovalScope::Always,
            None,
            false,
            Utc::now(),
            session_key.signer_key_id.clone(),
        );
        let receipt = sign_payload(payload.clone(), &session_key).unwrap();

        let mut mismatched_payload = payload;
        mismatched_payload.signer_key_id = "wrong".to_string();
        assert!(sign_payload(mismatched_payload, &session_key).is_err());
        assert!(!verify_receipt_signature("missing", &receipt));

        let mut bad_receipt = receipt.clone();
        bad_receipt.signer_key_id = "wrong".to_string();
        assert!(!verify_receipt_signature("demo", &bad_receipt));
        let mut bad_receipt = receipt.clone();
        bad_receipt.signature_algorithm = "not-ed25519".to_string();
        assert!(!verify_receipt_signature("demo", &bad_receipt));
        let mut bad_receipt = receipt.clone();
        bad_receipt.payload_hash = "wrong".to_string();
        assert!(!verify_receipt_signature("demo", &bad_receipt));
        let mut bad_receipt = receipt.clone();
        bad_receipt.signature = "not-base64".to_string();
        assert!(!verify_receipt_signature("demo", &bad_receipt));
        let mut bad_receipt = receipt.clone();
        bad_receipt.signature = STANDARD.encode([1_u8, 2, 3]);
        assert!(!verify_receipt_signature("demo", &bad_receipt));

        let mut key_file = read_project_key("demo").unwrap();
        key_file.public_key = "not-base64".to_string();
        write_project_key(&key_file).unwrap();
        assert!(!verify_receipt_signature("demo", &receipt));
        key_file.public_key = STANDARD.encode([1_u8, 2, 3]);
        write_project_key(&key_file).unwrap();
        assert!(!verify_receipt_signature("demo", &receipt));
        key_file.public_key = STANDARD.encode([0xff_u8; 32]);
        write_project_key(&key_file).unwrap();
        assert!(!verify_receipt_signature("demo", &receipt));

        key_file.version = 2;
        write_project_key(&key_file).unwrap();
        assert!(read_project_key("demo").is_err());
        key_file.version = 1;
        key_file.project = "other".to_string();
        let contents = serde_json::to_string_pretty(&key_file).unwrap();
        std::fs::write(project_key_path("demo"), contents).unwrap();
        assert!(read_project_key("demo").is_err());

        std::env::remove_var("WARD_HOME");
    }

    #[test]
    #[serial]
    fn approval_key_storage_reports_parse_write_and_seed_errors() {
        let _guard = env_lock();
        let home = tempfile::tempdir().unwrap();
        std::env::set_var("WARD_HOME", home.path());

        std::fs::create_dir_all(keys_dir()).unwrap();
        std::fs::write(project_key_path("bad-json"), "{bad-json").unwrap();
        assert!(ensure_project_key("bad-json", "1234").is_err());

        let mut bad_base64 = ensure_project_key("bad-base64", "1234").unwrap();
        bad_base64.encrypted_private_key = vault::encrypt_env("not-base64!", "1234").unwrap();
        write_project_key(&bad_base64).unwrap();
        assert!(ensure_project_key("bad-base64", "1234").is_err());

        let mut bad_length = ensure_project_key("bad-length", "1234").unwrap();
        bad_length.encrypted_private_key =
            vault::encrypt_env(&STANDARD.encode([1_u8, 2, 3]), "1234").unwrap();
        write_project_key(&bad_length).unwrap();
        assert!(ensure_project_key("bad-length", "1234").is_err());

        let blocked_home = home.path().join("blocked-home");
        std::fs::write(&blocked_home, "not a directory").unwrap();
        std::env::set_var("WARD_HOME", blocked_home);
        assert!(ensure_project_key("blocked", "1234").is_err());

        std::env::remove_var("WARD_HOME");
    }
}
