//! Bridge between ExtractedClaim and Episteme Assertion.
//!
//! Converts claims extracted from source code into Episteme assertions
//! that can be ingested into the knowledge graph.

use blake3::Hasher;
use ed25519_dalek::{Signer, SigningKey};
use stemedb_core::types::{
    Assertion, Hash, HlcTimestamp, LifecycleStage, SignatureEntry, SourceClass,
};
use tracing::instrument;

use crate::types::ExtractedClaim;

/// Convert an ExtractedClaim to an Episteme Assertion.
///
/// The assertion is signed with the provided keypair and timestamped.
/// Uses `SourceClass::Expert` (Tier 3) for code-extracted claims.
#[instrument(skip(signing_key), fields(concept_path = %claim.concept_path, predicate = %claim.predicate))]
pub fn claim_to_assertion(
    claim: &ExtractedClaim,
    signing_key: &SigningKey,
    timestamp: u64,
) -> Assertion {
    claim_to_assertion_with_tier(claim, signing_key, timestamp, SourceClass::Expert)
}

/// Convert an ExtractedClaim to a Tier 4 (Community) observation.
///
/// Used for claims that have no authority conflict — these become "project memory"
/// that persists across commits and enables future drift detection.
///
/// Observations are lower-weight assertions (Tier 4, 0.3 authority weight) that
/// record what the code actually does without making authoritative claims.
#[instrument(skip(signing_key), fields(concept_path = %claim.concept_path, predicate = %claim.predicate))]
pub fn claim_to_observation(
    claim: &ExtractedClaim,
    signing_key: &SigningKey,
    timestamp: u64,
) -> Assertion {
    claim_to_assertion_with_tier(claim, signing_key, timestamp, SourceClass::Community)
}

/// Internal helper to create assertions with a specific source class.
fn claim_to_assertion_with_tier(
    claim: &ExtractedClaim,
    signing_key: &SigningKey,
    timestamp: u64,
    source_class: SourceClass,
) -> Assertion {
    // Build source metadata
    let source_metadata = serde_json::json!({
        "file": claim.file,
        "line": claim.line,
        "matched_text": claim.matched_text,
        "scan_tool": "aphoria",
        "scan_version": env!("CARGO_PKG_VERSION"),
    });

    // Compute source hash from file:line:matched_text
    let source_hash = compute_source_hash(&claim.file, claim.line, &claim.matched_text);

    // Create signature (version 1: signs subject:predicate)
    let message = format!("{}:{}", claim.concept_path, claim.predicate);
    let signature = signing_key.sign(message.as_bytes());
    let verifying_key = signing_key.verifying_key();

    let signature_entry = SignatureEntry {
        agent_id: verifying_key.to_bytes(),
        signature: signature.to_bytes(),
        timestamp,
        version: 1,
    };

    Assertion {
        subject: claim.concept_path.clone(),
        predicate: claim.predicate.clone(),
        object: claim.value.clone(),
        parent_hash: None,
        source_hash,
        source_class,
        visual_hash: None,
        epoch: None,
        source_metadata: serde_json::to_vec(&source_metadata).ok(),
        lifecycle: LifecycleStage::Approved,
        signatures: vec![signature_entry],
        confidence: claim.confidence,
        timestamp,
        hlc_timestamp: HlcTimestamp::default(),
        vector: None,
    }
}

/// Compute the content hash of an assertion for deduplication.
#[allow(dead_code)]
pub fn compute_assertion_hash(assertion: &Assertion) -> Hash {
    let mut hasher = Hasher::new();
    hasher.update(assertion.subject.as_bytes());
    hasher.update(assertion.predicate.as_bytes());
    hasher.update(format!("{:?}", assertion.object).as_bytes());
    hasher.update(&assertion.source_hash);
    hasher.update(&[assertion.source_class.tier()]);
    *hasher.finalize().as_bytes()
}

/// Compute the source hash from file location and matched text.
fn compute_source_hash(file: &str, line: usize, matched_text: &str) -> Hash {
    let mut hasher = Hasher::new();
    hasher.update(file.as_bytes());
    hasher.update(&line.to_le_bytes());
    hasher.update(matched_text.as_bytes());
    *hasher.finalize().as_bytes()
}

/// Generate a new Ed25519 signing key for an Aphoria agent.
pub fn generate_signing_key() -> SigningKey {
    use rand::rngs::OsRng;
    SigningKey::generate(&mut OsRng)
}

/// Load or generate the project's signing key.
///
/// The key is stored at `.aphoria/agent.key` in the project root.
pub fn load_or_generate_key(project_root: &std::path::Path) -> std::io::Result<SigningKey> {
    let aphoria_dir = project_root.join(".aphoria");
    let key_path = aphoria_dir.join("agent.key");

    if key_path.exists() {
        load_key_from_file(&key_path)
    } else {
        // Generate new key
        let key = generate_signing_key();
        std::fs::create_dir_all(&aphoria_dir)?;
        std::fs::write(&key_path, key.to_bytes())?;
        Ok(key)
    }
}

/// Load a signing key from a specific file path.
///
/// Returns an error if the file doesn't exist or contains invalid data.
pub fn load_key_from_file(key_path: &std::path::Path) -> std::io::Result<SigningKey> {
    let key_bytes = std::fs::read(key_path)?;
    if key_bytes.len() == 32 {
        let mut arr = [0u8; 32];
        arr.copy_from_slice(&key_bytes);
        Ok(SigningKey::from_bytes(&arr))
    } else {
        Err(std::io::Error::new(
            std::io::ErrorKind::InvalidData,
            format!("Invalid key file: expected 32 bytes, got {}", key_bytes.len()),
        ))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use stemedb_core::types::ObjectValue;

    #[test]
    fn test_claim_to_assertion() {
        let claim = ExtractedClaim {
            concept_path: "code://rust/myapp/tls/cert_verification".to_string(),
            predicate: "enabled".to_string(),
            value: ObjectValue::Boolean(false),
            file: "src/client.rs".to_string(),
            line: 42,
            matched_text: "danger_accept_invalid_certs(true)".to_string(), // aphoria:ignore - Test fixture
            confidence: 1.0,
            description: "TLS verification disabled".to_string(),
        };

        let key = generate_signing_key();
        let timestamp = 1706832000;

        let assertion = claim_to_assertion(&claim, &key, timestamp);

        assert_eq!(assertion.subject, claim.concept_path);
        assert_eq!(assertion.predicate, "enabled");
        assert_eq!(assertion.object, ObjectValue::Boolean(false));
        assert_eq!(assertion.source_class, SourceClass::Expert);
        assert_eq!(assertion.confidence, 1.0);
        assert_eq!(assertion.timestamp, timestamp);
        assert!(!assertion.signatures.is_empty());
    }

    #[test]
    fn test_claim_to_observation_sets_tier4() {
        let claim = ExtractedClaim {
            concept_path: "code://rust/myapp/logging/level".to_string(),
            predicate: "value".to_string(),
            value: ObjectValue::Text("debug".to_string()),
            file: "src/config.rs".to_string(),
            line: 15,
            matched_text: "log_level = \"debug\"".to_string(),
            confidence: 0.9,
            description: "Logging level set to debug".to_string(),
        };

        let key = generate_signing_key();
        let timestamp = 1706832000;

        let observation = claim_to_observation(&claim, &key, timestamp);

        assert_eq!(observation.subject, claim.concept_path);
        assert_eq!(observation.predicate, "value");
        assert_eq!(observation.object, ObjectValue::Text("debug".to_string()));
        assert_eq!(observation.source_class, SourceClass::Community); // Tier 4
        assert_eq!(observation.source_class.tier(), 4);
        assert!((observation.source_class.authority_weight() - 0.3).abs() < f32::EPSILON);
        assert_eq!(observation.confidence, 0.9);
        assert_eq!(observation.timestamp, timestamp);
        assert!(!observation.signatures.is_empty());
    }

    #[test]
    fn test_claim_to_observation_preserves_metadata() {
        let claim = ExtractedClaim {
            concept_path: "code://rust/myapp/db/pool_size".to_string(),
            predicate: "value".to_string(),
            value: ObjectValue::Number(10.0),
            file: "src/db/config.rs".to_string(),
            line: 42,
            matched_text: "pool_size = 10".to_string(),
            confidence: 1.0,
            description: "Database pool size".to_string(),
        };

        let key = generate_signing_key();
        let timestamp = 1706832000;

        let observation = claim_to_observation(&claim, &key, timestamp);

        // Verify source metadata is preserved
        let metadata: serde_json::Value =
            serde_json::from_slice(observation.source_metadata.as_ref().expect("metadata"))
                .expect("parse");

        assert_eq!(metadata["file"], "src/db/config.rs");
        assert_eq!(metadata["line"], 42);
        assert_eq!(metadata["matched_text"], "pool_size = 10");
        assert_eq!(metadata["scan_tool"], "aphoria");
    }

    #[test]
    fn test_assertion_hash_deterministic() {
        let claim = ExtractedClaim {
            concept_path: "code://rust/myapp/tls/cert_verification".to_string(),
            predicate: "enabled".to_string(),
            value: ObjectValue::Boolean(false),
            file: "src/client.rs".to_string(),
            line: 42,
            matched_text: "danger_accept_invalid_certs(true)".to_string(), // aphoria:ignore - Test fixture
            confidence: 1.0,
            description: "TLS verification disabled".to_string(),
        };

        let key = generate_signing_key();
        let assertion1 = claim_to_assertion(&claim, &key, 1000);
        let assertion2 = claim_to_assertion(&claim, &key, 1000);

        let hash1 = compute_assertion_hash(&assertion1);
        let hash2 = compute_assertion_hash(&assertion2);

        assert_eq!(hash1, hash2);
    }

    #[test]
    fn test_load_or_generate_key() {
        let temp_dir = tempfile::tempdir().expect("create temp dir");
        let key1 = load_or_generate_key(temp_dir.path()).expect("generate key");
        let key2 = load_or_generate_key(temp_dir.path()).expect("load key");

        // Same key should be loaded
        assert_eq!(key1.to_bytes(), key2.to_bytes());
    }
}
