//! CIDv1 + raw-leaves IPFS implementation.
//!
//! Generates UnixFS-compatible CIDs matching Kubo `ipfs add --cid-version=1`:
//! - 256 KB (262144 byte) chunks
//! - Raw leaves (codec 0x55, no dag-pb wrapper on leaf blocks)
//! - Balanced tree (up to 174 links per internal node)
//! - SHA2-256 multihash
//! - CIDv1 Base32 output (e.g. bafybe...)
//!
//! Compatible with modern pinning services (Pinata, NFT.Storage, web3.storage, etc.).

use anyhow::Result;
use bytes::Bytes;
use cid::Cid;
use multibase::Base;
use multihash_codetable::{Code, MultihashDigest};

use ipld_dagpb::{DAG_PB_CODE, PbLink, PbNode};

/// Raw codec for leaf blocks (0x55).
const RAW_CODEC: u64 = 0x55;

/// Chunk size: 256 KB, matching Kubo default.
const CHUNK_SIZE: usize = 262144;

/// Max links per internal node (go-ipfs / js-ipfs-unixfs-importer default).
const MAX_LINKS_PER_NODE: usize = 174;

/// Encodes UnixFS Data message for a File node.
/// Type=File(2), filesize, blocksizes. No inline Data for internal nodes.
fn encode_unixfs_file_data(filesize: u64, blocksizes: &[u64]) -> Vec<u8> {
    let mut out = Vec::new();
    // Field 1 (Type): varint, File = 2
    write_varint_field(&mut out, 1, 2);
    // Field 3 (filesize): varint
    write_varint_field(&mut out, 3, filesize);
    // Field 4 (blocksizes): repeated varint
    for &s in blocksizes {
        write_varint_field(&mut out, 4, s);
    }
    out
}

fn write_varint_field(out: &mut Vec<u8>, field: u32, value: u64) {
    let tag = (field << 3) | 0; // wire type 0 = varint
    write_varint(out, tag as u64);
    write_varint(out, value);
}

fn write_varint(out: &mut Vec<u8>, mut value: u64) {
    loop {
        let b = (value & 0x7F) as u8;
        value >>= 7;
        if value == 0 {
            out.push(b);
            break;
        }
        out.push(b | 0x80);
    }
}

/// Computes CIDv1 for a raw leaf block (chunk bytes).
fn raw_leaf_cid(data: &[u8]) -> Cid {
    let hash = Code::Sha2_256.digest(data);
    Cid::new_v1(RAW_CODEC, hash)
}

/// Builds a balanced tree of dag-pb nodes from leaf (cid, size) pairs.
/// Returns the root CID.
fn build_balanced_tree(leaves: Vec<(Cid, u64)>) -> Cid {
    let mut layer: Vec<(Cid, u64)> = leaves;
    while layer.len() > 1 {
        let mut next = Vec::new();
        for chunk in layer.chunks(MAX_LINKS_PER_NODE) {
            let links: Vec<PbLink> = chunk
                .iter()
                .map(|(cid, size)| PbLink {
                    cid: *cid,
                    name: None,
                    size: Some(*size),
                })
                .collect();
            let blocksizes: Vec<u64> = chunk.iter().map(|(_, s)| *s).collect();
            let filesize: u64 = blocksizes.iter().sum();
            let data = encode_unixfs_file_data(filesize, &blocksizes);
            let pb_node = PbNode {
                links,
                data: Some(Bytes::from(data)),
            };
            let encoded = pb_node.into_bytes();
            let hash = Code::Sha2_256.digest(&encoded);
            let cid = Cid::new_v1(DAG_PB_CODE, hash);
            next.push((cid, filesize));
        }
        layer = next;
    }
    layer.into_iter().next().unwrap().0
}

/// Computes IPFS CID from content bytes using CIDv1 + raw-leaves.
/// Returns CIDv1 Base32 string (e.g. bafybe...).
/// Matches Kubo `ipfs add --cid-version=1` (raw-leaves implied).
pub fn compute_ipfs_cid_from_bytes(bytes: &[u8]) -> Result<String> {
    if bytes.is_empty() {
        anyhow::bail!("cannot compute CID for empty content");
    }
    let chunks: Vec<&[u8]> = bytes.chunks(CHUNK_SIZE).collect();
    let leaves: Vec<(Cid, u64)> = chunks
        .iter()
        .map(|chunk| {
            let cid = raw_leaf_cid(chunk);
            let size = chunk.len() as u64;
            (cid, size)
        })
        .collect();
    let root = if leaves.len() == 1 {
        leaves[0].0
    } else {
        build_balanced_tree(leaves)
    };
    Ok(root.to_string_of_base(Base::Base32Lower)?)
}

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

    #[test]
    fn compute_ipfs_cid_deterministic() {
        let bytes = b"hello world";
        let cid1 = compute_ipfs_cid_from_bytes(bytes).unwrap();
        let cid2 = compute_ipfs_cid_from_bytes(bytes).unwrap();
        assert_eq!(cid1, cid2);
        // CIDv1 Base32: multibase "b" + base32 payload (e.g. bafkr... or bafy...)
        assert!(
            cid1.starts_with('b') && cid1.len() > 40,
            "CIDv1 should be Base32 format (starts with b), got: {}",
            cid1
        );
    }

    #[test]
    fn compute_ipfs_cid_rejects_empty() {
        assert!(compute_ipfs_cid_from_bytes(&[]).is_err());
    }

    #[test]
    fn single_chunk_raw_leaf() {
        let bytes = b"small file";
        let cid = compute_ipfs_cid_from_bytes(bytes).unwrap();
        // CIDv1 Base32 format
        assert!(cid.starts_with('b'), "expected Base32 CID, got: {}", cid);
        assert!(cid.len() > 40);
    }

    /// Run with: cargo test verify_cid_against_kubo -- --nocapture
    /// Then: ipfs add --cid-version=1 assets/test_data/unsigned.jpg
    #[test]
    fn verify_cid_against_kubo() {
        let path = env!("CARGO_MANIFEST_DIR").to_string() + "/assets/test_data/unsigned.jpg";
        let path_exists = std::path::Path::new(&path).exists();
        if !path_exists {
            eprintln!("Skipping: {} not found", path);
            return;
        }
        let bytes = crate::c2pa_core::extract_c2pa_content_bytes(&path).unwrap();
        let cid = compute_ipfs_cid_from_bytes(&bytes).unwrap();
        println!("CIDv1 (rust): {}", cid);
        println!("Verify with: ipfs add --cid-version=1 {}", path);
    }
}
