use std::{
    collections::{HashMap, VecDeque},
    fs,
    path::{Path, PathBuf},
};

use serde::{Deserialize, Serialize};

use crate::{variants, Entry, Result, ZipError, SPEC_FUNCTIONS};

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Composition {
    #[serde(default, alias = "variants")]
    pub implementations: HashMap<String, String>,
    #[serde(default)]
    pub composition_id: Option<String>,
}

impl Default for Composition {
    fn default() -> Self {
        Self {
            implementations: default_variants()
                .into_iter()
                .map(|(k, v)| (k.into(), v.into()))
                .collect(),
            composition_id: None,
        }
    }
}

impl Composition {
    pub fn from_json(json: &str) -> Result<Self> {
        let value: serde_json::Value = serde_json::from_str(json)?;
        if value.get("implementations").is_some() || value.get("variants").is_some() {
            Ok(serde_json::from_value(value)?)
        } else {
            Ok(Self {
                implementations: serde_json::from_value(value)?,
                composition_id: None,
            })
        }
    }
    pub fn variant(&self, function: &'static str) -> &str {
        self.implementations
            .get(function)
            .map(String::as_str)
            .unwrap_or_else(|| default_for(function))
    }
    fn dispatch_variant(&self, function: &'static str) -> &str {
        let raw = self.variant(function);
        raw.strip_prefix(function)
            .and_then(|v| v.strip_prefix('.'))
            .unwrap_or(raw)
    }
    pub fn validate(&self) -> Result<()> {
        for function in SPEC_FUNCTIONS {
            validate_variant(function, self.variant(function))?;
        }
        Ok(())
    }
}

#[derive(Clone, Debug, Serialize)]
pub struct VariantInfo {
    pub function: &'static str,
    pub variant: &'static str,
    pub experimental: bool,
}

#[derive(Clone, Debug, Serialize)]
pub struct OperationReport<T> {
    pub value: T,
    pub calls: Vec<String>,
}

pub struct Engine {
    composition: Composition,
    calls: VecDeque<String>,
}

impl Engine {
    pub fn new(composition: Composition) -> Result<Self> {
        composition.validate()?;
        Ok(Self {
            composition,
            calls: VecDeque::with_capacity(32),
        })
    }
    pub fn composition(&self) -> &Composition {
        &self.composition
    }
    pub fn recent_calls(&self) -> Vec<String> {
        self.calls.iter().cloned().collect()
    }
    fn call(&mut self, function: &'static str) {
        if self.calls.len() == 32 {
            self.calls.pop_front();
        }
        self.calls.push_back(format!(
            "{}:{}",
            function,
            self.composition.variant(function)
        ));
    }

    pub fn list_path(&mut self, archive: impl AsRef<Path>) -> Result<OperationReport<Vec<Entry>>> {
        let data = fs::read(archive)?;
        let value = self.list_bytes(&data)?;
        Ok(OperationReport {
            value,
            calls: self.recent_calls(),
        })
    }

    pub fn list_bytes(&mut self, data: &[u8]) -> Result<Vec<Entry>> {
        self.call("list_archive");
        match self.composition.dispatch_variant("list_archive") {
            "central_order" => self.list_central(data, false),
            "preallocated" => self.list_central(data, true),
            "shrink_to_fit" => self.list_central(data, true).map(|mut entries| {
                entries.shrink_to_fit();
                entries
            }),
            "boxed_roundtrip" => self
                .list_central(data, false)
                .map(|entries| entries.into_boxed_slice().into_vec()),
            "deque_roundtrip" => self.list_central(data, true).map(|entries| {
                entries
                    .into_iter()
                    .collect::<VecDeque<_>>()
                    .into_iter()
                    .collect()
            }),
            v => unknown("list_archive", v),
        }
    }

    fn list_central(&mut self, data: &[u8], preallocate: bool) -> Result<Vec<Entry>> {
        self.call("locate_end_of_central_directory");
        let eocd = match self
            .composition
            .dispatch_variant("locate_end_of_central_directory")
        {
            "scan_backward" => variants::locate_scan_backward(data),
            "reverse_windows" => variants::locate_rposition(data),
            "forward_latest" => variants::locate_forward_latest(data),
            "trailer_length" => variants::locate_comment_length(data),
            "signature_state" => variants::locate_signature_state(data),
            "faulty_short_comment" => variants::locate_faulty_short_comment(data),
            v => unknown("locate_end_of_central_directory", v),
        }?;
        let mut out = if preallocate {
            Vec::with_capacity(eocd.entries)
        } else {
            Vec::new()
        };
        let mut at = eocd.central_offset;
        let end = eocd.central_offset + eocd.central_size;
        for _ in 0..eocd.entries {
            self.call("parse_central_directory_entry");
            self.call("decode_filename");
            let decoder = match self.composition.dispatch_variant("decode_filename") {
                "utf8_checked" => variants::decode_checked as fn(&[u8], u16) -> Result<String>,
                "owned_utf8" => variants::decode_owned,
                "push_chars" => variants::decode_push_chars,
                "ascii_fast_path" => variants::decode_ascii_fast_path,
                "codepoints" => variants::decode_codepoints,
                v => return unknown("decode_filename", v),
            };
            let raw = match self
                .composition
                .dispatch_variant("parse_central_directory_entry")
            {
                "indexed" => variants::central_indexed(data, at, decoder),
                "bounds_first" => variants::central_cursor(data, at, decoder),
                "fixed_slice" => variants::central_fixed_slice(data, at, decoder),
                "lengths_first" => variants::central_lengths_first(data, at, decoder),
                "signature_guard" => variants::central_signature_guard(data, at, decoder),
                v => return unknown("parse_central_directory_entry", v),
            }?;
            at = raw.next;
            out.push(raw.entry);
        }
        if at != end {
            return Err(ZipError::Invalid("central directory size or entry count"));
        }
        Ok(out)
    }

    pub fn extract_path(
        &mut self,
        archive: impl AsRef<Path>,
        destination: impl AsRef<Path>,
    ) -> Result<OperationReport<Vec<PathBuf>>> {
        let data = fs::read(archive)?;
        let value = self.extract_bytes(&data, destination.as_ref())?;
        Ok(OperationReport {
            value,
            calls: self.recent_calls(),
        })
    }

    pub fn extract_bytes(&mut self, data: &[u8], root: &Path) -> Result<Vec<PathBuf>> {
        let entries = self.list_bytes(data)?;
        let total_size = entries.iter().try_fold(0u64, |total, entry| {
            total
                .checked_add(entry.uncompressed_size)
                .ok_or(ZipError::Invalid("archive size overflow"))
        })?;
        if total_size > 2 * 1024 * 1024 * 1024 {
            return Err(ZipError::Unsupported(
                "archive exceeds extraction size limit",
            ));
        }
        fs::create_dir_all(root)?;
        let canonical_root = fs::canonicalize(root)?;
        let mut written = Vec::new();
        for entry in entries {
            self.call("normalize_extraction_path");
            let path = match self
                .composition
                .dispatch_variant("normalize_extraction_path")
            {
                "component_stack" => variants::normalize_components(root, &entry.name),
                "segment_fold" => variants::normalize_fold(root, &entry.name),
                "segments" => variants::normalize_segments(root, &entry.name),
                "byte_scanner" => variants::normalize_bytes(root, &entry.name),
                "collect_join" => variants::normalize_collect(root, &entry.name),
                "faulty_depth_limit" => variants::normalize_faulty_depth(root, &entry.name),
                v => unknown("normalize_extraction_path", v),
            }?;
            ensure_safe_ancestors(root, &canonical_root, &path)?;
            if fs::symlink_metadata(&path).is_ok_and(|m| m.file_type().is_symlink()) {
                return Err(ZipError::UnsafePath(entry.name));
            }
            if entry.is_directory {
                fs::create_dir(&path).or_else(|e| {
                    if e.kind() == std::io::ErrorKind::AlreadyExists {
                        Ok(())
                    } else {
                        Err(e)
                    }
                })?;
                continue;
            }
            let parent = path
                .parent()
                .ok_or_else(|| ZipError::UnsafePath(entry.name.clone()))?;
            fs::create_dir_all(parent)?;
            let canonical_parent = fs::canonicalize(parent)?;
            if !canonical_parent.starts_with(&canonical_root) {
                return Err(ZipError::UnsafePath(entry.name));
            }
            self.extract_one(data, &entry, &path)?;
            written.push(path);
        }
        Ok(written)
    }

    fn extract_one(&mut self, data: &[u8], entry: &Entry, path: &Path) -> Result<()> {
        self.call("extract_entry");
        self.call("parse_local_file_header");
        let local = match self.composition.dispatch_variant("parse_local_file_header") {
            "indexed" => variants::local_indexed(data, entry.local_offset),
            "sliced" => variants::local_sliced(data, entry.local_offset),
            "fixed_array" => variants::local_fixed_array(data, entry.local_offset),
            "lengths_first" => variants::local_lengths_first(data, entry.local_offset),
            "signature_guard" => variants::local_signature_guard(data, entry.local_offset),
            v => unknown("parse_local_file_header", v),
        }?;
        if local.method != entry.compression || local.flags != entry.flags {
            return Err(ZipError::Invalid("local/central header mismatch"));
        }
        if data.get(local.name_offset..local.name_offset + local.name_len)
            != Some(entry.raw_name.as_slice())
        {
            return Err(ZipError::Invalid("local/central filename mismatch"));
        }
        let size = usize::try_from(entry.compressed_size)
            .map_err(|_| ZipError::Invalid("entry too large"))?;
        if entry.uncompressed_size > 512 * 1024 * 1024 {
            return Err(ZipError::Unsupported("entry exceeds extraction size limit"));
        }
        let compressed = data
            .get(
                local.data_offset
                    ..local
                        .data_offset
                        .checked_add(size)
                        .ok_or(ZipError::Invalid("entry size overflow"))?,
            )
            .ok_or(ZipError::Invalid("truncated entry data"))?;
        self.call("decompress_entry");
        let bytes = match self.composition.dispatch_variant("decompress_entry") {
            "stream_reader" => {
                variants::decompress_reader(entry.compression, compressed, entry.uncompressed_size)
            }
            "buffered_reader" => variants::decompress_buffered(
                entry.compression,
                compressed,
                entry.uncompressed_size,
            ),
            "preallocated" => variants::decompress_preallocated(
                entry.compression,
                compressed,
                entry.uncompressed_size,
            ),
            "chunk_loop" => {
                variants::decompress_chunks(entry.compression, compressed, entry.uncompressed_size)
            }
            "io_copy" => {
                variants::decompress_copy(entry.compression, compressed, entry.uncompressed_size)
            }
            v => unknown("decompress_entry", v),
        }?;
        if bytes.len() as u64 != entry.uncompressed_size {
            return Err(ZipError::SizeMismatch {
                name: entry.name.clone(),
                expected: entry.uncompressed_size,
                actual: bytes.len() as u64,
            });
        }
        self.call("validate_crc");
        let actual = match self.composition.dispatch_variant("validate_crc") {
            "one_shot" => variants::crc_one_shot(&bytes),
            "chunked" => variants::crc_chunked(&bytes),
            "bytewise" => variants::crc_bytewise(&bytes),
            "fixed_blocks" => variants::crc_fixed_blocks(&bytes),
            "halves" => variants::crc_halves(&bytes),
            v => return unknown("validate_crc", v),
        };
        if actual != entry.crc32 {
            return Err(ZipError::CrcMismatch {
                name: entry.name.clone(),
                expected: entry.crc32,
                actual,
            });
        }
        match self.composition.dispatch_variant("extract_entry") {
            "direct" => variants::write_direct(path, &bytes),
            "staged" => variants::write_create_new(path, &bytes),
            "file_handle" => variants::write_file(path, &bytes),
            "buffered" => variants::write_buffered(path, &bytes),
            "chunked" => variants::write_chunked(path, &bytes),
            v => unknown("extract_entry", v),
        }
    }
}

fn ensure_safe_ancestors(root: &Path, canonical_root: &Path, target: &Path) -> Result<()> {
    let relative = target
        .strip_prefix(root)
        .map_err(|_| ZipError::UnsafePath("outside root".into()))?;
    let parent = relative.parent().unwrap_or_else(|| Path::new(""));
    let mut cursor = root.to_path_buf();
    for component in parent.components() {
        cursor.push(component);
        match fs::symlink_metadata(&cursor) {
            Ok(meta) if meta.file_type().is_symlink() || !meta.is_dir() => {
                return Err(ZipError::UnsafePath("unsafe ancestor".into()))
            }
            Ok(_) => {}
            Err(error) if error.kind() == std::io::ErrorKind::NotFound => fs::create_dir(&cursor)?,
            Err(error) => return Err(error.into()),
        }
    }
    if fs::canonicalize(target.parent().unwrap_or(root))?.starts_with(canonical_root) {
        Ok(())
    } else {
        Err(ZipError::UnsafePath("outside root".into()))
    }
}

fn unknown<T>(function: &'static str, variant: &str) -> Result<T> {
    Err(ZipError::UnknownVariant {
        function,
        variant: variant.into(),
    })
}
fn default_for(function: &str) -> &'static str {
    default_variants()
        .into_iter()
        .find(|(f, _)| *f == function)
        .map(|(_, v)| v)
        .unwrap_or("")
}
fn default_variants() -> [(&'static str, &'static str); 9] {
    [
        ("locate_end_of_central_directory", "scan_backward"),
        ("parse_central_directory_entry", "indexed"),
        ("parse_local_file_header", "indexed"),
        ("decode_filename", "utf8_checked"),
        ("normalize_extraction_path", "component_stack"),
        ("decompress_entry", "stream_reader"),
        ("validate_crc", "one_shot"),
        ("extract_entry", "direct"),
        ("list_archive", "central_order"),
    ]
}

fn validate_variant(function: &'static str, variant: &str) -> Result<()> {
    let short = variant
        .strip_prefix(function)
        .and_then(|v| v.strip_prefix('.'))
        .unwrap_or(variant);
    if variants_catalog()
        .iter()
        .any(|i| i.function == function && i.variant == short)
    {
        Ok(())
    } else {
        unknown(function, variant)
    }
}

pub fn variants_catalog() -> Vec<VariantInfo> {
    vec![
        v("locate_end_of_central_directory", "scan_backward", false),
        v("locate_end_of_central_directory", "reverse_windows", false),
        v("locate_end_of_central_directory", "forward_latest", false),
        v("locate_end_of_central_directory", "trailer_length", false),
        v("locate_end_of_central_directory", "signature_state", false),
        v(
            "locate_end_of_central_directory",
            "faulty_short_comment",
            true,
        ),
        v("parse_central_directory_entry", "indexed", false),
        v("parse_central_directory_entry", "bounds_first", false),
        v("parse_central_directory_entry", "fixed_slice", false),
        v("parse_central_directory_entry", "lengths_first", false),
        v("parse_central_directory_entry", "signature_guard", false),
        v("parse_local_file_header", "indexed", false),
        v("parse_local_file_header", "sliced", false),
        v("parse_local_file_header", "fixed_array", false),
        v("parse_local_file_header", "lengths_first", false),
        v("parse_local_file_header", "signature_guard", false),
        v("decode_filename", "utf8_checked", false),
        v("decode_filename", "owned_utf8", false),
        v("decode_filename", "push_chars", false),
        v("decode_filename", "ascii_fast_path", false),
        v("decode_filename", "codepoints", false),
        v("normalize_extraction_path", "component_stack", false),
        v("normalize_extraction_path", "segment_fold", false),
        v("normalize_extraction_path", "segments", false),
        v("normalize_extraction_path", "byte_scanner", false),
        v("normalize_extraction_path", "collect_join", false),
        v("normalize_extraction_path", "faulty_depth_limit", true),
        v("decompress_entry", "stream_reader", false),
        v("decompress_entry", "buffered_reader", false),
        v("decompress_entry", "preallocated", false),
        v("decompress_entry", "chunk_loop", false),
        v("decompress_entry", "io_copy", false),
        v("validate_crc", "one_shot", false),
        v("validate_crc", "chunked", false),
        v("validate_crc", "bytewise", false),
        v("validate_crc", "fixed_blocks", false),
        v("validate_crc", "halves", false),
        v("extract_entry", "direct", false),
        v("extract_entry", "staged", false),
        v("extract_entry", "file_handle", false),
        v("extract_entry", "buffered", false),
        v("extract_entry", "chunked", false),
        v("list_archive", "central_order", false),
        v("list_archive", "preallocated", false),
        v("list_archive", "shrink_to_fit", false),
        v("list_archive", "boxed_roundtrip", false),
        v("list_archive", "deque_roundtrip", false),
    ]
}
fn v(function: &'static str, variant: &'static str, experimental: bool) -> VariantInfo {
    VariantInfo {
        function,
        variant,
        experimental,
    }
}
