use crate::error::{TrazaeoError, TrazaeoResult};
use crate::utils::Hash;
#[cfg(feature = "bao-range-proofs")]
use std::io::Read;

#[cfg(feature = "bao-range-proofs")]
use bao::decode::SliceDecoder;
#[cfg(feature = "bao-range-proofs")]
use bao::encode::SliceExtractor;
use serde::{Deserialize, Serialize};

pub const CONTENT_COMMITMENT_PROFILE_BLAKE3: &str = "blake3_root_v1";
pub const CONTENT_PROOF_TYPE_FULL_ROOT_V1: &str = "full_root_v1";
pub const CONTENT_PROOF_TYPE_BAO_OUTBOARD_V1: &str = "bao_outboard_v1";

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct StorageBinding {
    pub binding_type: String,
    pub uri: String,
    pub binding_hash: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ContentDescriptor {
    pub artifact_id: String,
    pub content_root_hash: String,
    pub content_commitment_profile: String,
    pub chunk_size: usize,
    pub leaf_count: usize,
    pub byte_length: u64,
    pub media_type: String,
    pub created_at: String,
    pub outboard_ref: Option<String>,
    pub outboard_hash: Option<String>,
    pub storage_bindings: Vec<StorageBinding>,
    pub container_profile: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct VerifiedRange {
    pub start: u64,
    pub end: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RangeProofPackage {
    pub proof_version: String,
    pub artifact_id: String,
    pub content_descriptor: ContentDescriptor,
    pub content_proof_type: String,
    pub content_proof_bytes: Vec<u8>,
    pub verified_ranges: Vec<VerifiedRange>,
}

#[cfg(feature = "bao-range-proofs")]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct BaoOutboardData {
    pub outboard_bytes: Vec<u8>,
    pub outboard_hash: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContentDescriptorInput<'a> {
    pub artifact_id: &'a str,
    pub root: Hash,
    pub chunk_size: usize,
    pub leaf_count: usize,
    pub byte_length: u64,
    pub media_type: &'a str,
    pub created_at: &'a str,
}

/// Builds content descriptor.
pub fn build_content_descriptor(input: ContentDescriptorInput<'_>) -> ContentDescriptor {
    ContentDescriptor {
        artifact_id: input.artifact_id.to_string(),
        content_root_hash: hex::encode(input.root.0),
        content_commitment_profile: CONTENT_COMMITMENT_PROFILE_BLAKE3.to_string(),
        chunk_size: input.chunk_size,
        leaf_count: input.leaf_count,
        byte_length: input.byte_length,
        media_type: input.media_type.to_string(),
        created_at: input.created_at.to_string(),
        outboard_ref: None,
        outboard_hash: None,
        storage_bindings: Vec::new(),
        container_profile: None,
    }
}

/// Validates content descriptor.
pub fn validate_content_descriptor(descriptor: &ContentDescriptor) -> TrazaeoResult<()> {
    if descriptor.artifact_id.trim().is_empty() {
        return Err(TrazaeoError::invalid_input(
            "validate content descriptor",
            "artifact_id must not be empty",
        ));
    }
    if descriptor.content_root_hash.trim().is_empty() {
        return Err(TrazaeoError::invalid_input(
            "validate content descriptor",
            "content_root_hash must not be empty",
        ));
    }
    if descriptor.content_commitment_profile != CONTENT_COMMITMENT_PROFILE_BLAKE3 {
        return Err(TrazaeoError::invalid_input(
            "validate content descriptor",
            "unsupported content_commitment_profile",
        ));
    }
    if descriptor.chunk_size == 0 {
        return Err(TrazaeoError::invalid_input(
            "validate content descriptor",
            "chunk_size must be greater than zero",
        ));
    }
    if descriptor.media_type.trim().is_empty() {
        return Err(TrazaeoError::invalid_input(
            "validate content descriptor",
            "media_type must not be empty",
        ));
    }
    Ok(())
}

/// Computes a deterministic hash of the content descriptor.
pub fn compute_content_descriptor_hash(descriptor: &ContentDescriptor) -> Hash {
    let payload =
        serde_json::to_vec(descriptor).expect("content descriptor serialization should succeed");
    Hash(*blake3::hash(&payload).as_bytes())
}

/// Builds range proof package.
pub fn build_range_proof_package(
    descriptor: &ContentDescriptor,
    content_proof_type: &str,
    content_proof_bytes: Vec<u8>,
    verified_ranges: Vec<VerifiedRange>,
) -> RangeProofPackage {
    RangeProofPackage {
        proof_version: "1.0.0".to_string(),
        artifact_id: descriptor.artifact_id.clone(),
        content_descriptor: descriptor.clone(),
        content_proof_type: content_proof_type.to_string(),
        content_proof_bytes,
        verified_ranges,
    }
}

/// Builds a baseline full-artifact proof package.
pub fn build_full_root_proof_package(
    descriptor: &ContentDescriptor,
) -> TrazaeoResult<RangeProofPackage> {
    validate_content_descriptor(descriptor)?;
    Ok(build_range_proof_package(
        descriptor,
        CONTENT_PROOF_TYPE_FULL_ROOT_V1,
        Vec::new(),
        vec![VerifiedRange {
            start: 0,
            end: descriptor.byte_length,
        }],
    ))
}

/// Generates bao outboard.
#[cfg(feature = "bao-range-proofs")]
pub fn generate_bao_outboard(data: &[u8]) -> BaoOutboardData {
    let (outboard, _hash) = bao::encode::outboard(data);
    BaoOutboardData {
        outboard_hash: hex::encode(blake3::hash(&outboard).as_bytes()),
        outboard_bytes: outboard,
    }
}

/// Attaches bao outboard.
#[cfg(feature = "bao-range-proofs")]
pub fn attach_bao_outboard(
    descriptor: &ContentDescriptor,
    outboard_ref: Option<&str>,
    outboard: &BaoOutboardData,
) -> ContentDescriptor {
    let mut updated = descriptor.clone();
    updated.outboard_ref = outboard_ref.map(str::to_string);
    updated.outboard_hash = Some(outboard.outboard_hash.clone());
    updated
}

/// Builds bao range proof package.
#[cfg(feature = "bao-range-proofs")]
pub fn build_bao_range_proof_package(
    descriptor: &ContentDescriptor,
    data: &[u8],
    outboard: &[u8],
    start: u64,
    len: u64,
) -> TrazaeoResult<RangeProofPackage> {
    validate_content_descriptor(descriptor)?;
    let expected_root = hex::encode(blake3::hash(data).as_bytes());
    if descriptor.content_root_hash != expected_root {
        return Err(TrazaeoError::invalid_input(
            "build bao range proof package",
            "content_descriptor root does not match Bao content hash",
        ));
    }
    let mut extractor = SliceExtractor::new_outboard(
        std::io::Cursor::new(data),
        std::io::Cursor::new(outboard),
        start,
        len,
    );
    let mut proof_bytes = Vec::new();
    extractor.read_to_end(&mut proof_bytes).map_err(|e| {
        TrazaeoError::external(
            "build bao range proof package",
            format!("failed to extract bao slice: {e}"),
        )
    })?;
    Ok(build_range_proof_package(
        descriptor,
        CONTENT_PROOF_TYPE_BAO_OUTBOARD_V1,
        proof_bytes,
        vec![VerifiedRange {
            start,
            end: start + len,
        }],
    ))
}

/// Verifies a baseline full-artifact proof package against artifact bytes.
pub fn verify_full_root_proof_package(
    package: &RangeProofPackage,
    artifact_bytes: &[u8],
) -> TrazaeoResult<Vec<u8>> {
    validate_content_descriptor(&package.content_descriptor)?;
    if package.content_proof_type != CONTENT_PROOF_TYPE_FULL_ROOT_V1 {
        return Err(TrazaeoError::invalid_input(
            "verify full root proof package",
            "content_proof_type must be full_root_v1",
        ));
    }
    if !package.content_proof_bytes.is_empty() {
        return Err(TrazaeoError::invalid_input(
            "verify full root proof package",
            "content_proof_bytes must be empty for full_root_v1",
        ));
    }
    if package.verified_ranges.len() != 1 {
        return Err(TrazaeoError::invalid_input(
            "verify full root proof package",
            "verified_ranges must contain exactly one full-artifact range",
        ));
    }
    let range = &package.verified_ranges[0];
    if range.start != 0 || range.end != package.content_descriptor.byte_length {
        return Err(TrazaeoError::invalid_input(
            "verify full root proof package",
            "verified range must cover the entire artifact",
        ));
    }
    if artifact_bytes.len() as u64 != package.content_descriptor.byte_length {
        return Err(TrazaeoError::invalid_input(
            "verify full root proof package",
            "artifact byte length does not match content descriptor",
        ));
    }
    let expected_root = hex::encode(blake3::hash(artifact_bytes).as_bytes());
    if package.content_descriptor.content_root_hash != expected_root {
        return Err(TrazaeoError::invalid_input(
            "verify full root proof package",
            "artifact bytes do not match content_root_hash",
        ));
    }
    Ok(artifact_bytes.to_vec())
}

/// Verifies bao range proof package.
#[cfg(feature = "bao-range-proofs")]
pub fn verify_bao_range_proof_package(package: &RangeProofPackage) -> TrazaeoResult<Vec<u8>> {
    validate_content_descriptor(&package.content_descriptor)?;
    if package.content_proof_type != CONTENT_PROOF_TYPE_BAO_OUTBOARD_V1 {
        return Err(TrazaeoError::invalid_input(
            "verify bao range proof package",
            "content_proof_type must be bao_outboard_v1",
        ));
    }
    let Some(range) = package.verified_ranges.first() else {
        return Err(TrazaeoError::invalid_input(
            "verify bao range proof package",
            "verified_ranges must not be empty",
        ));
    };
    let root = hex::decode(&package.content_descriptor.content_root_hash).map_err(|e| {
        TrazaeoError::invalid_input(
            "verify bao range proof package",
            format!("invalid content_root_hash hex: {e}"),
        )
    })?;
    let root: [u8; 32] = root.as_slice().try_into().map_err(|_| {
        TrazaeoError::invalid_input(
            "verify bao range proof package",
            "content_root_hash must be exactly 32 bytes",
        )
    })?;
    let mut decoder = SliceDecoder::new(
        std::io::Cursor::new(&package.content_proof_bytes),
        &bao::Hash::from(root),
        range.start,
        range.end - range.start,
    );
    let mut decoded = Vec::new();
    decoder.read_to_end(&mut decoded).map_err(|e| {
        TrazaeoError::external(
            "verify bao range proof package",
            format!("failed to verify bao slice: {e}"),
        )
    })?;
    Ok(decoded)
}

/// Encodes range proof package.
pub fn encode_range_proof_package(package: &RangeProofPackage) -> Vec<u8> {
    serde_json::to_vec(package).expect("range proof package serialization should succeed")
}

/// Decodes range proof package.
pub fn decode_range_proof_package(payload: &[u8]) -> TrazaeoResult<RangeProofPackage> {
    let package: RangeProofPackage = serde_json::from_slice(payload).map_err(|_| {
        TrazaeoError::serialization(
            "decode range proof package",
            "malformed range proof package",
        )
    })?;
    validate_content_descriptor(&package.content_descriptor)?;
    if package.artifact_id.trim().is_empty() {
        return Err(TrazaeoError::invalid_input(
            "decode range proof package",
            "artifact_id must not be empty",
        ));
    }
    if package.content_proof_type.trim().is_empty() {
        return Err(TrazaeoError::invalid_input(
            "decode range proof package",
            "content_proof_type must not be empty",
        ));
    }
    Ok(package)
}

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

    /// Tests that descriptor validation rejects empty artifact id.
    #[test]
    fn descriptor_validation_rejects_empty_artifact_id() {
        let mut descriptor = build_content_descriptor(ContentDescriptorInput {
            artifact_id: "artifact-1",
            root: Hash([1u8; 32]),
            chunk_size: 4,
            leaf_count: 2,
            byte_length: 8,
            media_type: "application/octet-stream",
            created_at: "2026-01-01T00:00:00Z",
        });
        descriptor.artifact_id.clear();
        assert!(validate_content_descriptor(&descriptor).is_err());
    }

    /// Tests that range proof package roundtrip.
    #[test]
    fn range_proof_package_roundtrip() {
        let descriptor = build_content_descriptor(ContentDescriptorInput {
            artifact_id: "artifact-1",
            root: Hash([1u8; 32]),
            chunk_size: 4,
            leaf_count: 2,
            byte_length: 8,
            media_type: "application/octet-stream",
            created_at: "2026-01-01T00:00:00Z",
        });
        let package = build_range_proof_package(
            &descriptor,
            CONTENT_PROOF_TYPE_FULL_ROOT_V1,
            vec![1, 2, 3],
            vec![VerifiedRange { start: 0, end: 4 }],
        );
        let decoded = decode_range_proof_package(&encode_range_proof_package(&package))
            .expect("decode range proof package");
        assert_eq!(decoded.artifact_id, "artifact-1");
    }

    /// Tests that full-root proof package roundtrip verifies full artifact bytes.
    #[test]
    fn full_root_proof_package_roundtrip() {
        let data = b"abcdefghijklmno";
        let descriptor = build_content_descriptor(ContentDescriptorInput {
            artifact_id: "artifact-1",
            root: Hash(*blake3::hash(data).as_bytes()),
            chunk_size: 1024,
            leaf_count: 1,
            byte_length: data.len() as u64,
            media_type: "application/octet-stream",
            created_at: "2026-01-01T00:00:00Z",
        });
        let package = build_full_root_proof_package(&descriptor).expect("full root package");
        let decoded = decode_range_proof_package(&encode_range_proof_package(&package))
            .expect("decode full root proof package");
        let verified =
            verify_full_root_proof_package(&decoded, data).expect("verify full root proof package");
        assert_eq!(verified, data);
    }

    /// Tests that bao outboard and range proof roundtrip.
    #[cfg(feature = "bao-range-proofs")]
    #[test]
    fn bao_outboard_and_range_proof_roundtrip() {
        let data = b"abcdefghijklmno";
        let descriptor = build_content_descriptor(ContentDescriptorInput {
            artifact_id: "artifact-1",
            root: Hash(*blake3::hash(data).as_bytes()),
            chunk_size: 1024,
            leaf_count: 1,
            byte_length: data.len() as u64,
            media_type: "application/octet-stream",
            created_at: "2026-01-01T00:00:00Z",
        });
        let outboard = generate_bao_outboard(data);
        let descriptor = attach_bao_outboard(&descriptor, Some("outboard://1"), &outboard);
        let package =
            build_bao_range_proof_package(&descriptor, data, &outboard.outboard_bytes, 0, 4)
                .expect("bao package");
        let decoded = verify_bao_range_proof_package(&package).expect("verify bao package");
        assert_eq!(decoded, b"abcd");
    }
}
