//! Fast class loading with validated binary cache format (.jvmc).
//!
//! Caches raw class file bytes keyed by class name for faster loading.
//! The cache format includes source-file checksum and modification time
//! so stale or corrupted cache entries are automatically invalidated.
//!
//! # Cache Format (Version 2)
//!
//! ```text
//! Offset    Size    Field
//! 0         4       Magic: 0x4A564D43 ("JVMC")
//! 4         4       Version: 2
//! 8         4       Source checksum (FNV-1a of source .class bytes)
//! 12        8       Source modification time (seconds since UNIX epoch)
//! 20        4       Source modification time (nanoseconds)
//! 24        4       Data length
//! 28        N       Raw class file bytes
//! ```
//!
//! Version 1 format (no validation) is still readable for backward
//! compatibility, but newly written caches always use Version 2.

use crate::class_file::ClassFile;
use byteorder::{BE, ReadBytesExt, WriteBytesExt};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::time::SystemTime;

const CACHE_MAGIC: u32 = 0x4A564D43; // "JVMC"
const CACHE_VERSION: u32 = 2;

/// Compute a 32-bit FNV-1a hash of the given bytes.
fn fnv1a_32(bytes: &[u8]) -> u32 {
    const FNV_OFFSET_BASIS: u32 = 0x811c_9dc5;
    const FNV_PRIME: u32 = 0x0100_0193;
    let mut hash = FNV_OFFSET_BASIS;
    for &b in bytes {
        hash ^= b as u32;
        hash = hash.wrapping_mul(FNV_PRIME);
    }
    hash
}

/// Metadata stored in the cache header for validation.
#[derive(Debug, Clone, Copy, PartialEq)]
struct CacheHeader {
    version: u32,
    checksum: u32,
    mtime_sec: u64,
    mtime_nano: u32,
    data_len: u32,
}

impl CacheHeader {
    fn write_to<W: Write>(&self, writer: &mut W) -> std::io::Result<()> {
        writer.write_u32::<BE>(CACHE_MAGIC)?;
        writer.write_u32::<BE>(self.version)?;
        writer.write_u32::<BE>(self.checksum)?;
        writer.write_u64::<BE>(self.mtime_sec)?;
        writer.write_u32::<BE>(self.mtime_nano)?;
        writer.write_u32::<BE>(self.data_len)?;
        Ok(())
    }

    fn read_from<R: Read>(reader: &mut R) -> std::io::Result<Option<Self>> {
        let magic = reader.read_u32::<BE>()?;
        if magic != CACHE_MAGIC {
            return Ok(None);
        }
        let version = reader.read_u32::<BE>()?;
        if version == 1 {
            // Version 1: only has data_len after version
            let data_len = reader.read_u32::<BE>()?;
            return Ok(Some(CacheHeader {
                version: 1,
                checksum: 0,
                mtime_sec: 0,
                mtime_nano: 0,
                data_len,
            }));
        }
        if version != 2 {
            return Ok(None); // Unknown version
        }
        let checksum = reader.read_u32::<BE>()?;
        let mtime_sec = reader.read_u64::<BE>()?;
        let mtime_nano = reader.read_u32::<BE>()?;
        let data_len = reader.read_u32::<BE>()?;
        Ok(Some(CacheHeader {
            version: 2,
            checksum,
            mtime_sec,
            mtime_nano,
            data_len,
        }))
    }
}

/// Compute checksum and modification time for a source file.
fn source_metadata(path: &Path) -> std::io::Result<(u32, u64, u32)> {
    let bytes = std::fs::read(path)?;
    let checksum = fnv1a_32(&bytes);
    let meta = std::fs::metadata(path)?;
    let mtime = meta.modified()?;
    let duration = mtime.duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default();
    Ok((checksum, duration.as_secs(), duration.subsec_nanos()))
}

/// Write class bytes to cache file with validation metadata.
///
/// `source_path` should be the original `.class` file so the cache can be
/// validated against it on subsequent reads.
pub fn write_to_cache(
    cache_dir: &Path,
    class_name: &str,
    source_path: &Path,
) -> std::io::Result<PathBuf> {
    std::fs::create_dir_all(cache_dir)?;
    let safe_name = class_name.replace('.', "_").replace('/', "_");
    let path = cache_dir.join(format!("{}.jvmc", safe_name));

    let bytes = std::fs::read(source_path)?;
    let (checksum, mtime_sec, mtime_nano) = source_metadata(source_path)?;

    let header = CacheHeader {
        version: CACHE_VERSION,
        checksum,
        mtime_sec,
        mtime_nano,
        data_len: bytes.len() as u32,
    };

    let mut f = std::fs::File::create(&path)?;
    header.write_to(&mut f)?;
    f.write_all(&bytes)?;
    Ok(path)
}

/// Try to read a class from cache, validating against the source file.
///
/// Returns `Ok(None)` if the cache is missing, stale, corrupted, or the
/// source file has been modified since the cache was written.
pub fn read_from_cache(
    cache_dir: &Path,
    class_name: &str,
    source_path: &Path,
) -> std::io::Result<Option<ClassFile>> {
    let safe_name = class_name.replace('.', "_").replace('/', "_");
    let path = cache_dir.join(format!("{}.jvmc", safe_name));
    if !path.exists() {
        return Ok(None);
    }

    let mut f = std::fs::File::open(&path)?;
    let Some(header) = CacheHeader::read_from(&mut f)? else {
        return Ok(None);
    };

    if header.version == 2 {
        // Validate against source file metadata
        if source_path.exists() {
            let expected = source_metadata(source_path)?;
            if header.checksum != expected.0
                || header.mtime_sec != expected.1
                || header.mtime_nano != expected.2
            {
                // Cache is stale
                return Ok(None);
            }
        } else {
            // Source file gone; invalidate cache
            return Ok(None);
        }
    }

    let len = header.data_len as usize;
    let mut buf = vec![0u8; len];
    f.read_exact(&mut buf)?;

    ClassFile::parse(&buf)
        .map(Some)
        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, format!("{:?}", e)))
}

/// Locate the source `.class` file for a given class name in the classpath.
pub fn find_source_file(classpath: &[PathBuf], class_name: &str) -> Option<PathBuf> {
    let class_file_name = format!("{}.class", class_name.replace('.', "/"));
    for dir in classpath {
        let path = dir.join(&class_file_name);
        if path.exists() {
            return Some(path);
        }
    }
    None
}

/// Default cache directory
pub fn default_cache_dir() -> PathBuf {
    std::env::temp_dir().join("jvmrs_class_cache")
}

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

    fn make_test_class_bytes() -> Vec<u8> {
        // Minimal invalid-but-parseable class file stub:
        // CAFEBABE + version + constant_pool_count=1 + rest
        let mut bytes = vec![];
        bytes.extend_from_slice(&0xCAFEBABE_u32.to_be_bytes());
        bytes.extend_from_slice(&0u16.to_be_bytes()); // minor
        bytes.extend_from_slice(&0u16.to_be_bytes()); // major
        bytes.extend_from_slice(&1u16.to_be_bytes()); // constant_pool_count=1 (empty)
        bytes.extend_from_slice(&0u16.to_be_bytes()); // access_flags
        bytes.extend_from_slice(&0u16.to_be_bytes()); // this_class
        bytes.extend_from_slice(&0u16.to_be_bytes()); // super_class
        bytes.extend_from_slice(&0u16.to_be_bytes()); // interfaces_count
        bytes.extend_from_slice(&0u16.to_be_bytes()); // fields_count
        bytes.extend_from_slice(&0u16.to_be_bytes()); // methods_count
        bytes.extend_from_slice(&0u16.to_be_bytes()); // attributes_count
        bytes
    }

    #[test]
    fn test_cache_roundtrip_valid() {
        let tmp = tempfile::tempdir().unwrap();
        let cache_dir = tmp.path().join("cache");
        let source_path = tmp.path().join("Test.class");
        let bytes = make_test_class_bytes();
        std::fs::write(&source_path, &bytes).unwrap();

        write_to_cache(&cache_dir, "Test", &source_path).unwrap();
        let result = read_from_cache(&cache_dir, "Test", &source_path).unwrap();
        assert!(result.is_some());
    }

    #[test]
    fn test_cache_stale_after_source_modified() {
        let tmp = tempfile::tempdir().unwrap();
        let cache_dir = tmp.path().join("cache");
        let source_path = tmp.path().join("Test.class");
        let bytes = make_test_class_bytes();
        std::fs::write(&source_path, &bytes).unwrap();

        write_to_cache(&cache_dir, "Test", &source_path).unwrap();

        // Modify source file
        std::thread::sleep(std::time::Duration::from_millis(50));
        let mut bytes2 = bytes.clone();
        bytes2.push(0);
        std::fs::write(&source_path, &bytes2).unwrap();

        let result = read_from_cache(&cache_dir, "Test", &source_path).unwrap();
        assert!(result.is_none(), "cache should be stale after source modification");
    }

    #[test]
    fn test_cache_corrupted_magic() {
        let tmp = tempfile::tempdir().unwrap();
        let cache_dir = tmp.path().join("cache");
        let source_path = tmp.path().join("Test.class");
        let bytes = make_test_class_bytes();
        std::fs::write(&source_path, &bytes).unwrap();

        write_to_cache(&cache_dir, "Test", &source_path).unwrap();

        let cache_path = cache_dir.join("Test.jvmc");
        let mut data = std::fs::read(&cache_path).unwrap();
        data[0] = 0xFF; // corrupt magic
        std::fs::write(&cache_path, &data).unwrap();

        let result = read_from_cache(&cache_dir, "Test", &source_path).unwrap();
        assert!(result.is_none(), "corrupted magic should invalidate cache");
    }

    #[test]
    fn test_cache_missing_source() {
        let tmp = tempfile::tempdir().unwrap();
        let cache_dir = tmp.path().join("cache");
        let source_path = tmp.path().join("Test.class");
        let bytes = make_test_class_bytes();
        std::fs::write(&source_path, &bytes).unwrap();

        write_to_cache(&cache_dir, "Test", &source_path).unwrap();
        std::fs::remove_file(&source_path).unwrap();

        let result = read_from_cache(&cache_dir, "Test", &source_path).unwrap();
        assert!(result.is_none(), "missing source should invalidate cache");
    }

    #[test]
    fn test_fnv1a_32_known_value() {
        // Known test vector: "hello" -> 0x4f9f2cab
        assert_eq!(fnv1a_32(b"hello"), 0x4f9f2cab);
    }

    #[test]
    fn test_cache_v1_backward_compat() {
        let tmp = tempfile::tempdir().unwrap();
        let cache_dir = tmp.path().join("cache");
        let source_path = tmp.path().join("Test.class");
        let bytes = make_test_class_bytes();
        std::fs::write(&source_path, &bytes).unwrap();

        // Manually write a v1 cache file
        std::fs::create_dir_all(&cache_dir).unwrap();
        let cache_path = cache_dir.join("Test.jvmc");
        let mut f = std::fs::File::create(&cache_path).unwrap();
        f.write_u32::<BE>(CACHE_MAGIC).unwrap();
        f.write_u32::<BE>(1).unwrap(); // version 1
        f.write_u32::<BE>(bytes.len() as u32).unwrap();
        f.write_all(&bytes).unwrap();
        drop(f);

        // For v1, validation is skipped (no checksum/mtime), so it should parse
        let result = read_from_cache(&cache_dir, "Test", &source_path).unwrap();
        assert!(result.is_some(), "v1 cache should be readable for backward compat");
    }
}
