//! Minimal read-only ISO9660 lookup. Given an uploaded ISO file and an
//! in-ISO path (e.g. `/casper/vmlinuz`), locate the file and return a
//! `(start_byte, length_bytes)` pair so the HTTP handler can stream just
//! that range from the on-disk ISO without full extraction.
//!
//! We only implement what we need: the Primary Volume Descriptor and Rock
//! Ridge / Joliet extensions are ignored. Paths are matched case-insensitive
//! against plain ISO9660 filenames (uppercase, `;1` version suffix stripped).
//! This is sufficient for the kernel/initrd and wimboot files we serve;
//! if a requested path isn't found, the handler returns 404 and the user
//! can still download the whole ISO via `/iso/<id>.iso`.

use std::io::{Read, Seek, SeekFrom};
use std::path::Path;

const SECTOR: u64 = 2048;

#[derive(Debug, Clone)]
pub struct FileLocation {
    pub offset: u64,
    pub length: u64,
}

/// Look up `in_iso_path` (leading slash optional, case-insensitive) in the
/// ISO at `iso_path`. Returns None on any parsing or IO failure.
pub fn lookup(iso_path: &Path, in_iso_path: &str) -> Option<FileLocation> {
    let mut f = std::fs::File::open(iso_path).ok()?;
    let root = read_root_directory(&mut f)?;
    let components: Vec<&str> = in_iso_path
        .trim_start_matches('/')
        .split('/')
        .filter(|c| !c.is_empty())
        .collect();
    if components.is_empty() {
        return None;
    }
    walk(&mut f, root.offset, root.length, &components)
}

fn read_root_directory(f: &mut std::fs::File) -> Option<FileLocation> {
    // Primary Volume Descriptor at LBA 16.
    let mut pvd = [0u8; 2048];
    f.seek(SeekFrom::Start(16 * SECTOR)).ok()?;
    f.read_exact(&mut pvd).ok()?;
    if pvd[0] != 0x01 || &pvd[1..6] != b"CD001" {
        return None;
    }
    // Root directory record is at offset 156, length 34.
    let rec = &pvd[156..156 + 34];
    let (offset, length) = parse_dir_record_ext(rec)?;
    Some(FileLocation {
        offset: offset * SECTOR,
        length,
    })
}

/// Walk components down the directory tree starting at `dir_offset`.
fn walk(
    f: &mut std::fs::File,
    dir_offset: u64,
    dir_len: u64,
    components: &[&str],
) -> Option<FileLocation> {
    let mut dir = vec![0u8; dir_len as usize];
    f.seek(SeekFrom::Start(dir_offset)).ok()?;
    f.read_exact(&mut dir).ok()?;

    let target = components[0];
    let rest = &components[1..];
    let mut i = 0;
    while i < dir.len() {
        let len = dir[i] as usize;
        if len == 0 {
            // Padding to sector boundary.
            let next = (i / SECTOR as usize + 1) * SECTOR as usize;
            if next <= i {
                break;
            }
            i = next;
            continue;
        }
        if i + len > dir.len() {
            break;
        }
        let rec = &dir[i..i + len];
        let name = dir_record_name(rec);
        let is_dir = (rec.get(25).copied().unwrap_or(0) & 0x02) != 0;
        // Skip "." (0x00) and ".." (0x01) pseudo-entries.
        let is_pseudo = matches!(rec.get(32).copied(), Some(1))
            && rec.get(33).copied() == Some(0x00)
            || matches!(rec.get(32).copied(), Some(1)) && rec.get(33).copied() == Some(0x01);
        if !is_pseudo && name.eq_ignore_ascii_case(target) {
            let (child_off, child_len) = parse_dir_record_ext(rec)?;
            if rest.is_empty() && !is_dir {
                return Some(FileLocation {
                    offset: child_off * SECTOR,
                    length: child_len,
                });
            } else if !rest.is_empty() && is_dir {
                return walk(f, child_off * SECTOR, child_len, rest);
            }
        }
        i += len;
    }
    None
}

/// Extract (extent LBA, data length in bytes) from a directory record.
/// Layout per ISO9660: bytes 2..10 extent LBA (LE+BE duplicate), 10..18
/// data length (LE+BE duplicate). We trust the little-endian copy.
fn parse_dir_record_ext(rec: &[u8]) -> Option<(u64, u64)> {
    if rec.len() < 34 {
        return None;
    }
    let lba = u32::from_le_bytes(rec[2..6].try_into().ok()?) as u64;
    let len = u32::from_le_bytes(rec[10..14].try_into().ok()?) as u64;
    Some((lba, len))
}

/// Extract the identifier from a directory record, stripping ISO9660's
/// `;1` version suffix.
fn dir_record_name(rec: &[u8]) -> String {
    let name_len = *rec.get(32).unwrap_or(&0) as usize;
    if name_len == 0 || rec.len() < 33 + name_len {
        return String::new();
    }
    let raw = &rec[33..33 + name_len];
    let s = String::from_utf8_lossy(raw).to_string();
    // Strip `;N` version suffix.
    if let Some(i) = s.rfind(';') {
        s[..i].to_string()
    } else {
        s
    }
}
