//! XISO / XDVDFS disc image reading.
//!
//! Xbox 360 game discs use the XDVDFS filesystem (also called XISO).
//! This module wraps the `xdvdfs` crate to extract game files into memory
//! or onto disk for further processing.
//!
//! ## Reference projects
//! - xdvdfs (Rust): https://github.com/antangelo/xdvdfs
//! - extract-xiso (C): https://github.com/XboxDev/extract-xiso
//! - Xenia emulator: https://github.com/xenia-canary/xenia-canary

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

use anyhow::{Context, Result};
use tracing::{debug, info};
use xdvdfs::blockdev::OffsetWrapper;
use xdvdfs::layout::VolumeDescriptor;

/// A handle to an open XISO image.
///
/// The inner file is wrapped in an [`OffsetWrapper`] which probes the four
/// known XGD partition offsets (raw XISO, XGD1, XGD2, XGD3) at open time,
/// so both single-layer raw dumps **and** full dual-layer disc images are
/// supported transparently.
pub struct XisoReader<F: Read + Seek + Send + Sync> {
    volume: VolumeDescriptor,
    /// Offset-aware block device — all sector reads are shifted by the
    /// detected partition start offset automatically.
    file: OffsetWrapper<F, std::io::Error>,
}

impl<F: Read + Seek + Send + Sync + 'static> XisoReader<F> {
    pub async fn open(file: F) -> Result<Self> {
        // OffsetWrapper::new probes four known partition offsets:
        //   0x00000000  — raw XISO (trimmed, sector 0 = XDVDFS start)
        //   0x183E0000  — XGD1 (original Xbox)
        //   0x0FD90000  — XGD2 (Xbox 360, most retail titles)
        //   0x02080000  — XGD3 (Xbox 360, later dual-layer titles)
        let mut wrapper = OffsetWrapper::new(file).await.map_err(|e| {
            anyhow::anyhow!(
                "No valid XDVDFS partition found in this disc image. \
                 Tried raw XISO, XGD1, XGD2, and XGD3 offsets. \
                 Is this a valid Xbox 360 (or original Xbox) disc image? \
                 (internal error: {e:?})"
            )
        })?;

        // Re-read the volume descriptor via the wrapper (now at the correct offset).
        let volume = xdvdfs::read::read_volume(&mut wrapper)
            .await
            .context("Found XDVDFS partition but failed to parse volume descriptor")?;

        info!(
            "Opened XISO: root directory table at sector {}",
            { let s = volume.root_table.region.sector; s }
        );
        Ok(Self { volume, file: wrapper })
    }

    /// List all files in the disc image, recursively (directories excluded).
    pub async fn list_all_files(&mut self) -> Result<Vec<String>> {
        let entries = self
            .volume
            .root_table
            .file_tree(&mut self.file)
            .await
            .context("Failed to walk XISO file tree")?;

        let mut paths = Vec::new();
        for (parent, entry) in &entries {
            if entry.node.dirent.is_directory() {
                continue;
            }
            let name = entry
                .name_str::<std::io::Error>()
                .unwrap_or(std::borrow::Cow::Borrowed("<invalid>"));
            // file_tree builds parent paths with a leading slash (e.g. "/dat").
            // Trim it so we get "dat/filename" instead of "/dat/filename".
            let full_path = format!("{}/{}", parent, name);
            paths.push(full_path.trim_start_matches('/').to_string());
        }
        Ok(paths)
    }

    /// Read the raw bytes of a file by its path inside the ISO.
    pub async fn read_file(&mut self, path: &str) -> Result<Vec<u8>> {
        let dirent = self
            .volume
            .root_table
            .walk_path(&mut self.file, path)
            .await
            .with_context(|| format!("File not found in ISO: {}", path))?;

        let data = dirent
            .node
            .dirent
            .read_data_all(&mut self.file)
            .await
            .with_context(|| format!("Failed to read file data: {}", path))?;

        debug!("Read {} bytes from {}", data.len(), path);
        Ok(data.into_vec())
    }

    /// Extract all files from the ISO into a directory on disk.
    pub async fn extract_all(&mut self, output_dir: &Path) -> Result<ExtractStats> {
        info!("Extracting ISO to {}", output_dir.display());
        std::fs::create_dir_all(output_dir).context("Failed to create output directory")?;

        let entries = self
            .volume
            .root_table
            .file_tree(&mut self.file)
            .await
            .context("Failed to walk XISO file tree")?;

        let mut stats = ExtractStats::default();
        for (parent, entry) in &entries {
            if entry.node.dirent.is_directory() {
                continue;
            }
            let name = entry
                .name_str::<std::io::Error>()
                .unwrap_or(std::borrow::Cow::Borrowed("<invalid>"));
            let rel_path = format!("{}/{}", parent, name);
            let rel_path = rel_path.trim_start_matches('/');

            let disk_path =
                output_dir.join(rel_path.replace('/', std::path::MAIN_SEPARATOR_STR));

            if let Some(parent_dir) = disk_path.parent() {
                std::fs::create_dir_all(parent_dir).with_context(|| {
                    format!("Failed to create dir: {}", parent_dir.display())
                })?;
            }

            let data = entry
                .node
                .dirent
                .read_data_all(&mut self.file)
                .await
                .with_context(|| format!("Failed to read: {}", rel_path))?;

            std::fs::write(&disk_path, &*data)
                .with_context(|| format!("Failed to write: {}", disk_path.display()))?;

            stats.files_extracted += 1;
            stats.bytes_extracted += data.len() as u64;
            debug!("Extracted {}", rel_path);
        }

        info!(
            "Extraction complete: {} files, {} bytes",
            stats.files_extracted, stats.bytes_extracted
        );
        Ok(stats)
    }
}

/// Open a disc image from a file path.
///
/// Accepts raw XISO dumps and full XGD1/XGD2/XGD3 disc images — the correct
/// partition offset is detected automatically.
pub async fn open_iso(path: &Path) -> Result<XisoReader<std::fs::File>> {
    let file = std::fs::File::open(path)
        .with_context(|| format!("Cannot open disc image: {}", path.display()))?;
    XisoReader::open(file).await
}

/// Statistics from an extraction operation.
#[derive(Default, Debug)]
pub struct ExtractStats {
    pub files_extracted: usize,
    pub bytes_extracted: u64,
}

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

    #[tokio::test]
    #[ignore = "requires a real ISO image — set SYLPHEED_ISO env var"]
    async fn test_list_iso() {
        let iso_path =
            std::env::var("SYLPHEED_ISO").expect("Set SYLPHEED_ISO to your ISO path");
        let mut reader = open_iso(Path::new(&iso_path)).await.unwrap();
        let files = reader.list_all_files().await.unwrap();
        for f in &files {
            println!("{}", f);
        }
        println!("Total: {} files", files.len());
    }
}
