//! Rayon-backed parallel verification and manifest-build helpers.
//!
//! Enabled by the `parallel` feature flag. This module is not part of the
//! public API surface; consumers invoke the parallel entry points through the
//! verifier and builder types in [`crate::verify`]:
//!
//! - [`SqPackPathVerifier::verify_parallel`](crate::verify::SqPackPathVerifier::verify_parallel)
//! - [`SqPackHashVerifier::verify_parallel`](crate::verify::SqPackHashVerifier::verify_parallel)
//! - [`SqPackHashManifestBuilder::build_parallel`](crate::verify::SqPackHashManifestBuilder::build_parallel)
//!
//! The implementation pre-collects all tasks from a sequential archive walk,
//! then uses `rayon::ParallelIterator` to verify or hash each entry concurrently.
//! Results are merged in a single sequential pass after the parallel phase.

use rayon::iter::{IntoParallelIterator, ParallelIterator};
use sha2::{Digest, Sha256};
use tracing::{debug, info, info_span};

use crate::cancel::CancelToken;
use crate::dat::FileKind;
use crate::error::Error;
use crate::header::DatLocation;
use crate::route::{ArchiveTriple, Category, KNOWN_CATEGORIES};
use crate::sqpack::{EnumeratedArchive, SqPack};
use crate::tracing_schema::span_names;
use crate::verify::{
    HashManifestBuildIssue, HashManifestBuildIssueKind, HashManifestBuildReport, HashManifestEntry,
    HashMismatch, HashVerifyReport, IndexHashKey, PathManifestEntry, PathMismatch,
    PathMismatchKind, PathVerifyReport, SqPackHashManifest, SqPackHashManifestBuilder,
    SqPackHashVerifier, SqPackPathManifest, SqPackPathVerifier,
};

enum VerifyOutcome {
    Clean,
    Skipped(PathMismatch),
    PathMismatch(PathMismatch),
    Cancelled,
}

enum HashVerifyOutcome {
    Clean,
    Skipped(HashMismatch),
    PathMismatch(HashMismatch),
    Extra(IndexHashKey),
    Cancelled,
}

enum HashBuildOutcome {
    Entry {
        key: IndexHashKey,
        entry: HashManifestEntry,
    },
    Issue(HashManifestBuildIssue),
    Cancelled,
}

pub(crate) fn verify_parallel(
    verifier: &SqPackPathVerifier<'_>,
    manifest: &SqPackPathManifest,
) -> PathVerifyReport {
    let span = info_span!(span_names::VERIFY_RUN, entries = manifest.entries.len());
    let _enter = span.enter();
    let started_at = std::time::Instant::now();

    let sq = verifier.sq();
    let cancel = verifier.cancel();

    let entries: Vec<(&String, &PathManifestEntry)> = manifest.entries.iter().collect();

    let outcomes: Vec<VerifyOutcome> = entries
        .into_par_iter()
        .map(|(game_path, expected)| verify_one_pure(sq, cancel, game_path, expected))
        .collect();

    let mut mismatches: Vec<PathMismatch> = Vec::new();
    let mut verified_count: usize = 0;
    let mut skipped_count: usize = 0;
    let mut cancelled = false;
    for outcome in outcomes {
        match outcome {
            VerifyOutcome::Clean => verified_count += 1,
            VerifyOutcome::Skipped(m) => {
                skipped_count += 1;
                mismatches.push(m);
            }
            VerifyOutcome::PathMismatch(m) => mismatches.push(m),
            VerifyOutcome::Cancelled => cancelled = true,
        }
    }

    let report = PathVerifyReport {
        mismatches,
        verified_count,
        skipped_count,
        cancelled,
    };

    #[allow(clippy::cast_possible_truncation)]
    let elapsed_ms = started_at.elapsed().as_millis() as u64;
    info!(
        entries = manifest.entries.len(),
        verified = report.verified_count,
        skipped = report.skipped_count,
        mismatches = report.mismatches.len(),
        cancelled = report.cancelled,
        elapsed_ms,
        "verify.run",
    );
    report
}

#[allow(clippy::too_many_lines)]
fn verify_one_pure(
    sq: &SqPack,
    cancel: Option<&CancelToken>,
    game_path: &str,
    expected: &PathManifestEntry,
) -> VerifyOutcome {
    if cancel.is_some_and(CancelToken::is_cancelled) {
        return VerifyOutcome::Cancelled;
    }

    debug!(path = %game_path, "verify_parallel entry");

    let category =
        ArchiveTriple::from_game_path(game_path).map_or_else(Category::placeholder, |t| t.category);

    let mut stream = match sq.read_file_streaming(game_path) {
        Ok(s) => s,
        Err(Error::PathNotFound { .. }) => {
            return VerifyOutcome::PathMismatch(PathMismatch::new(
                game_path.to_owned(),
                category,
                PathMismatchKind::Missing,
            ));
        }
        Err(Error::UnsupportedFileKind {
            kind: kind @ (FileKind::Model | FileKind::Texture),
        }) => {
            return verify_one_typed_pure(sq, game_path, category, kind, expected);
        }
        Err(Error::UnsupportedFileKind { kind }) => {
            return VerifyOutcome::Skipped(PathMismatch::new(
                game_path.to_owned(),
                category,
                PathMismatchKind::Unverifiable { kind },
            ));
        }
        Err(err) => {
            let error_kind = err.io_error_kind();
            return VerifyOutcome::PathMismatch(PathMismatch::new(
                game_path.to_owned(),
                category,
                PathMismatchKind::ReadError {
                    message: err.to_string(),
                    error_kind,
                },
            ));
        }
    };

    if stream.file_info().kind() == FileKind::Empty {
        return VerifyOutcome::Skipped(PathMismatch::new(
            game_path.to_owned(),
            category,
            PathMismatchKind::Unverifiable {
                kind: FileKind::Empty,
            },
        ));
    }

    let actual_size = stream.raw_file_size();
    if actual_size != expected.size {
        return VerifyOutcome::PathMismatch(PathMismatch::new(
            game_path.to_owned(),
            category,
            PathMismatchKind::SizeMismatch {
                expected: expected.size,
                actual: actual_size,
            },
        ));
    }

    let mut scratch: Vec<u8> = Vec::with_capacity(16 * 1024);
    let mut hasher = Sha256::new();
    while let Some(res) = stream.read_block_into(&mut scratch) {
        if let Err(err) = res {
            let error_kind = err.io_error_kind();
            return VerifyOutcome::PathMismatch(PathMismatch::new(
                game_path.to_owned(),
                category,
                PathMismatchKind::ReadError {
                    message: err.to_string(),
                    error_kind,
                },
            ));
        }
        hasher.update(&scratch);
    }
    let actual: [u8; 32] = hasher.finalize().into();
    if actual != expected.hash {
        return VerifyOutcome::PathMismatch(PathMismatch::new(
            game_path.to_owned(),
            category,
            PathMismatchKind::HashMismatch {
                expected: expected.hash,
                actual,
            },
        ));
    }

    VerifyOutcome::Clean
}

fn verify_one_typed_pure(
    sq: &SqPack,
    game_path: &str,
    category: Category,
    kind: FileKind,
    expected: &PathManifestEntry,
) -> VerifyOutcome {
    debug!(path = %game_path, kind = ?kind, "verify_parallel typed entry");
    let payload = match sq.read_file_typed(game_path) {
        Ok(p) => p,
        Err(err) => {
            let error_kind = err.io_error_kind();
            return VerifyOutcome::PathMismatch(PathMismatch::new(
                game_path.to_owned(),
                category,
                PathMismatchKind::ReadError {
                    message: err.to_string(),
                    error_kind,
                },
            ));
        }
    };
    let bytes = payload.bytes();
    #[allow(clippy::cast_possible_truncation)]
    let actual_size = bytes.len() as u32;
    if actual_size != expected.size {
        return VerifyOutcome::PathMismatch(PathMismatch::new(
            game_path.to_owned(),
            category,
            PathMismatchKind::SizeMismatch {
                expected: expected.size,
                actual: actual_size,
            },
        ));
    }
    let mut hasher = Sha256::new();
    hasher.update(bytes);
    let actual: [u8; 32] = hasher.finalize().into();
    if actual != expected.hash {
        return VerifyOutcome::PathMismatch(PathMismatch::new(
            game_path.to_owned(),
            category,
            PathMismatchKind::HashMismatch {
                expected: expected.hash,
                actual,
            },
        ));
    }
    VerifyOutcome::Clean
}

enum HashVerifyTask {
    Entry {
        key: IndexHashKey,
        location: DatLocation,
    },
    ArchiveReadError {
        key: IndexHashKey,
        mismatch: HashMismatch,
    },
}

#[allow(clippy::too_many_lines)]
pub(crate) fn hash_verify_parallel(
    verifier: &SqPackHashVerifier<'_>,
    manifest: &SqPackHashManifest,
) -> HashVerifyReport {
    let span = info_span!(
        span_names::HASH_VERIFY_RUN,
        entries = manifest.entries.len()
    );
    let _enter = span.enter();
    let started_at = std::time::Instant::now();

    let sq = verifier.sq();
    let cancel = verifier.cancel();

    let mut cancelled = false;
    let mut tasks: Vec<HashVerifyTask> = Vec::new();

    'walker: for archive_result in sq.enumerate_archives() {
        if cancel.is_some_and(CancelToken::is_cancelled) {
            cancelled = true;
            break 'walker;
        }
        let archive = match archive_result {
            Ok(a) => a,
            Err(err) => {
                let key = IndexHashKey::V1 {
                    triple: ArchiveTriple {
                        category: Category::placeholder(),
                        expansion: 0,
                        chunk: 0,
                    },
                    hash: 0,
                };
                let error_kind = err.io_error_kind();
                tasks.push(HashVerifyTask::ArchiveReadError {
                    key,
                    mismatch: HashMismatch::new(
                        key,
                        PathMismatchKind::ReadError {
                            message: err.to_string(),
                            error_kind,
                        },
                        None,
                    ),
                });
                continue;
            }
        };
        match archive {
            EnumeratedArchive::V1(triple, idx) => {
                for entry in idx.entries() {
                    if entry.is_synonym() {
                        for syn in idx.synonyms_by_hash(entry.hash) {
                            let key = IndexHashKey::V1Synonym {
                                triple,
                                hash: syn.hash(),
                                conflict_index: syn.conflict_index(),
                            };
                            tasks.push(HashVerifyTask::Entry {
                                key,
                                location: syn.dat_location(),
                            });
                        }
                        continue;
                    }
                    let key = IndexHashKey::V1 {
                        triple,
                        hash: entry.hash,
                    };
                    let Some(location) = entry.dat_location() else {
                        continue;
                    };
                    tasks.push(HashVerifyTask::Entry { key, location });
                }
            }
            EnumeratedArchive::V2(triple, idx) => {
                for entry in idx.entries() {
                    let key = IndexHashKey::V2 {
                        triple,
                        hash: entry.hash,
                    };
                    tasks.push(HashVerifyTask::Entry {
                        key,
                        location: entry.dat_location(),
                    });
                }
            }
        }
    }

    let outcomes: Vec<(IndexHashKey, HashVerifyOutcome)> = tasks
        .into_par_iter()
        .map(|task| match task {
            HashVerifyTask::Entry { key, location } => {
                (key, compare_one_pure(sq, cancel, manifest, key, location))
            }
            HashVerifyTask::ArchiveReadError { key, mismatch } => {
                (key, HashVerifyOutcome::PathMismatch(mismatch))
            }
        })
        .collect();

    let mut mismatches: Vec<HashMismatch> = Vec::new();
    let mut extras: Vec<IndexHashKey> = Vec::new();
    let mut verified_count: usize = 0;
    let mut skipped_count: usize = 0;
    let mut seen: std::collections::BTreeSet<IndexHashKey> = std::collections::BTreeSet::new();
    for (key, outcome) in outcomes {
        match outcome {
            HashVerifyOutcome::Clean => {
                verified_count += 1;
                seen.insert(key);
            }
            HashVerifyOutcome::Skipped(m) => {
                skipped_count += 1;
                seen.insert(key);
                mismatches.push(m);
            }
            HashVerifyOutcome::PathMismatch(m) => {
                seen.insert(key);
                mismatches.push(m);
            }
            HashVerifyOutcome::Extra(k) => extras.push(k),
            HashVerifyOutcome::Cancelled => cancelled = true,
        }
    }

    if !cancelled {
        for (key, expected) in &manifest.entries {
            if seen.contains(key) {
                continue;
            }
            mismatches.push(HashMismatch::new(
                *key,
                PathMismatchKind::Missing,
                expected.path.clone(),
            ));
        }
    }

    let report = HashVerifyReport {
        mismatches,
        verified_count,
        skipped_count,
        cancelled,
        extras,
    };

    #[allow(clippy::cast_possible_truncation)]
    let elapsed_ms = started_at.elapsed().as_millis() as u64;
    info!(
        entries = manifest.entries.len(),
        verified = report.verified_count,
        skipped = report.skipped_count,
        mismatches = report.mismatches.len(),
        extras = report.extras.len(),
        cancelled = report.cancelled,
        elapsed_ms,
        "hash_verify.run",
    );
    report
}

#[allow(clippy::too_many_lines)]
fn compare_one_pure(
    sq: &SqPack,
    cancel: Option<&CancelToken>,
    manifest: &SqPackHashManifest,
    key: IndexHashKey,
    location: DatLocation,
) -> HashVerifyOutcome {
    if cancel.is_some_and(CancelToken::is_cancelled) {
        return HashVerifyOutcome::Cancelled;
    }

    debug!(key = ?key, "hash_verify_parallel entry");

    let Some(expected) = manifest.entries.get(&key) else {
        return HashVerifyOutcome::Extra(key);
    };
    let path_hint = expected.path.clone();
    let triple = key.triple();

    let mut dat = match sq.open_dat_for(triple, location.data_file_id) {
        Ok(d) => d,
        Err(err) => {
            let error_kind = err.io_error_kind();
            return HashVerifyOutcome::PathMismatch(HashMismatch::new(
                key,
                PathMismatchKind::ReadError {
                    message: err.to_string(),
                    error_kind,
                },
                path_hint,
            ));
        }
    };
    let mut stream = match dat.read_file_streaming(location.byte_offset) {
        Ok(s) => s,
        Err(Error::UnsupportedFileKind {
            kind: kind @ (FileKind::Model | FileKind::Texture),
        }) => {
            return compare_one_typed_pure(
                &mut dat,
                key,
                location,
                kind,
                expected.hash,
                expected.size,
                path_hint,
            );
        }
        Err(Error::UnsupportedFileKind { kind }) => {
            return HashVerifyOutcome::Skipped(HashMismatch::new(
                key,
                PathMismatchKind::Unverifiable { kind },
                path_hint,
            ));
        }
        Err(err) => {
            let error_kind = err.io_error_kind();
            return HashVerifyOutcome::PathMismatch(HashMismatch::new(
                key,
                PathMismatchKind::ReadError {
                    message: err.to_string(),
                    error_kind,
                },
                path_hint,
            ));
        }
    };
    if stream.file_info().kind() == FileKind::Empty {
        return HashVerifyOutcome::Skipped(HashMismatch::new(
            key,
            PathMismatchKind::Unverifiable {
                kind: FileKind::Empty,
            },
            path_hint,
        ));
    }
    let actual_size = stream.raw_file_size();
    if actual_size != expected.size {
        return HashVerifyOutcome::PathMismatch(HashMismatch::new(
            key,
            PathMismatchKind::SizeMismatch {
                expected: expected.size,
                actual: actual_size,
            },
            path_hint,
        ));
    }
    let mut scratch: Vec<u8> = Vec::with_capacity(16 * 1024);
    let mut hasher = Sha256::new();
    while let Some(res) = stream.read_block_into(&mut scratch) {
        if let Err(err) = res {
            let error_kind = err.io_error_kind();
            return HashVerifyOutcome::PathMismatch(HashMismatch::new(
                key,
                PathMismatchKind::ReadError {
                    message: err.to_string(),
                    error_kind,
                },
                path_hint,
            ));
        }
        hasher.update(&scratch);
    }
    let actual: [u8; 32] = hasher.finalize().into();
    if actual != expected.hash {
        return HashVerifyOutcome::PathMismatch(HashMismatch::new(
            key,
            PathMismatchKind::HashMismatch {
                expected: expected.hash,
                actual,
            },
            path_hint,
        ));
    }
    HashVerifyOutcome::Clean
}

#[allow(clippy::too_many_arguments)]
fn compare_one_typed_pure(
    dat: &mut crate::dat::DatFile,
    key: IndexHashKey,
    location: DatLocation,
    kind: FileKind,
    expected_hash: [u8; 32],
    expected_size: u32,
    path_hint: Option<String>,
) -> HashVerifyOutcome {
    let payload = match dat.read_file_typed(location.byte_offset) {
        Ok(p) => p,
        Err(err) => {
            let error_kind = err.io_error_kind();
            return HashVerifyOutcome::PathMismatch(HashMismatch::new(
                key,
                PathMismatchKind::ReadError {
                    message: err.to_string(),
                    error_kind,
                },
                path_hint,
            ));
        }
    };
    let bytes = payload.bytes();
    #[allow(clippy::cast_possible_truncation)]
    let actual_size = bytes.len() as u32;
    if actual_size != expected_size {
        return HashVerifyOutcome::PathMismatch(HashMismatch::new(
            key,
            PathMismatchKind::SizeMismatch {
                expected: expected_size,
                actual: actual_size,
            },
            path_hint,
        ));
    }
    let mut hasher = Sha256::new();
    hasher.update(bytes);
    let actual: [u8; 32] = hasher.finalize().into();
    if actual != expected_hash {
        return HashVerifyOutcome::PathMismatch(HashMismatch::new(
            key,
            PathMismatchKind::HashMismatch {
                expected: expected_hash,
                actual,
            },
            path_hint,
        ));
    }
    debug!(kind = ?kind, "hash_verify_parallel typed entry verified");
    HashVerifyOutcome::Clean
}

enum HashBuildTask {
    Entry {
        triple: ArchiveTriple,
        key: IndexHashKey,
        location: DatLocation,
        path_hint: Option<String>,
    },
    ArchiveReadIssue {
        issue: HashManifestBuildIssue,
    },
}

#[allow(clippy::too_many_lines, clippy::unnecessary_wraps)]
pub(crate) fn hash_build_parallel(
    builder: &SqPackHashManifestBuilder<'_>,
) -> crate::error::Result<HashManifestBuildReport> {
    let span = info_span!(span_names::HASH_MANIFEST_BUILD);
    let _enter = span.enter();
    let started_at = std::time::Instant::now();

    let sq = builder.sq();
    let cancel = builder.cancel();

    let mut cancelled = false;
    let mut tasks: Vec<HashBuildTask> = Vec::new();

    'walker: for archive_result in sq.enumerate_archives() {
        if cancel.is_some_and(CancelToken::is_cancelled) {
            cancelled = true;
            break 'walker;
        }
        let archive = match archive_result {
            Ok(a) => a,
            Err(err) => {
                let key = IndexHashKey::V1 {
                    triple: ArchiveTriple {
                        category: Category::placeholder(),
                        expansion: 0,
                        chunk: 0,
                    },
                    hash: 0,
                };
                tasks.push(HashBuildTask::ArchiveReadIssue {
                    issue: HashManifestBuildIssue {
                        key,
                        kind: HashManifestBuildIssueKind::ReadError {
                            message: err.to_string(),
                        },
                    },
                });
                continue;
            }
        };
        match archive {
            EnumeratedArchive::V1(triple, idx) => {
                for entry in idx.entries() {
                    if entry.is_synonym() {
                        for syn in idx.synonyms_by_hash(entry.hash) {
                            let key = IndexHashKey::V1Synonym {
                                triple,
                                hash: syn.hash(),
                                conflict_index: syn.conflict_index(),
                            };
                            let stored_path = syn.path().into_owned();
                            tasks.push(HashBuildTask::Entry {
                                triple,
                                key,
                                location: syn.dat_location(),
                                path_hint: Some(stored_path),
                            });
                        }
                        continue;
                    }
                    let key = IndexHashKey::V1 {
                        triple,
                        hash: entry.hash,
                    };
                    let Some(location) = entry.dat_location() else {
                        continue;
                    };
                    tasks.push(HashBuildTask::Entry {
                        triple,
                        key,
                        location,
                        path_hint: None,
                    });
                }
            }
            EnumeratedArchive::V2(triple, idx) => {
                for entry in idx.entries() {
                    let key = IndexHashKey::V2 {
                        triple,
                        hash: entry.hash,
                    };
                    tasks.push(HashBuildTask::Entry {
                        triple,
                        key,
                        location: entry.dat_location(),
                        path_hint: None,
                    });
                }
            }
        }
    }

    let outcomes: Vec<HashBuildOutcome> = tasks
        .into_par_iter()
        .map(|task| match task {
            HashBuildTask::Entry {
                triple,
                key,
                location,
                path_hint,
            } => hash_one_pure(sq, cancel, triple, key, location, path_hint),
            HashBuildTask::ArchiveReadIssue { issue } => HashBuildOutcome::Issue(issue),
        })
        .collect();

    let mut manifest = SqPackHashManifest::new();
    let mut issues: Vec<HashManifestBuildIssue> = Vec::new();
    let mut verified_count: usize = 0;
    for outcome in outcomes {
        match outcome {
            HashBuildOutcome::Entry { key, entry } => {
                manifest.entries.insert(key, entry);
                verified_count += 1;
            }
            HashBuildOutcome::Issue(issue) => issues.push(issue),
            HashBuildOutcome::Cancelled => cancelled = true,
        }
    }

    let skipped_count = issues.len();
    let total = verified_count + skipped_count;

    let report = HashManifestBuildReport {
        manifest,
        verified_count,
        skipped_count,
        cancelled,
        issues,
    };

    #[allow(clippy::cast_possible_truncation)]
    let elapsed_ms = started_at.elapsed().as_millis() as u64;
    info!(
        entries = total,
        verified = verified_count,
        skipped = skipped_count,
        cancelled,
        elapsed_ms,
        "hash_manifest.build",
    );
    Ok(report)
}

fn hash_one_pure(
    sq: &SqPack,
    cancel: Option<&CancelToken>,
    triple: ArchiveTriple,
    key: IndexHashKey,
    location: DatLocation,
    path_hint: Option<String>,
) -> HashBuildOutcome {
    if cancel.is_some_and(CancelToken::is_cancelled) {
        return HashBuildOutcome::Cancelled;
    }
    debug!(key = ?key, "hash_manifest.build_parallel entry");
    let mut dat = match sq.open_dat_for(triple, location.data_file_id) {
        Ok(d) => d,
        Err(err) => {
            return HashBuildOutcome::Issue(HashManifestBuildIssue {
                key,
                kind: HashManifestBuildIssueKind::ReadError {
                    message: err.to_string(),
                },
            });
        }
    };
    let mut stream = match dat.read_file_streaming(location.byte_offset) {
        Ok(s) => s,
        Err(Error::UnsupportedFileKind {
            kind: kind @ (FileKind::Model | FileKind::Texture),
        }) => {
            return hash_one_typed_pure(&mut dat, key, location, kind, path_hint);
        }
        Err(Error::UnsupportedFileKind { kind }) => {
            return HashBuildOutcome::Issue(HashManifestBuildIssue {
                key,
                kind: HashManifestBuildIssueKind::Unverifiable { kind },
            });
        }
        Err(err) => {
            return HashBuildOutcome::Issue(HashManifestBuildIssue {
                key,
                kind: HashManifestBuildIssueKind::ReadError {
                    message: err.to_string(),
                },
            });
        }
    };
    if stream.file_info().kind() == FileKind::Empty {
        return HashBuildOutcome::Issue(HashManifestBuildIssue {
            key,
            kind: HashManifestBuildIssueKind::Unverifiable {
                kind: FileKind::Empty,
            },
        });
    }
    let raw_file_size = stream.raw_file_size();
    let mut scratch: Vec<u8> = Vec::with_capacity(16 * 1024);
    let mut hasher = Sha256::new();
    while let Some(res) = stream.read_block_into(&mut scratch) {
        if let Err(err) = res {
            return HashBuildOutcome::Issue(HashManifestBuildIssue {
                key,
                kind: HashManifestBuildIssueKind::ReadError {
                    message: err.to_string(),
                },
            });
        }
        hasher.update(scratch.as_slice());
    }
    let hash: [u8; 32] = hasher.finalize().into();
    let mut entry = HashManifestEntry::new(hash, raw_file_size);
    entry.path = path_hint;
    HashBuildOutcome::Entry { key, entry }
}

fn hash_one_typed_pure(
    dat: &mut crate::dat::DatFile,
    key: IndexHashKey,
    location: DatLocation,
    kind: FileKind,
    path_hint: Option<String>,
) -> HashBuildOutcome {
    let payload = match dat.read_file_typed(location.byte_offset) {
        Ok(p) => p,
        Err(err) => {
            return HashBuildOutcome::Issue(HashManifestBuildIssue {
                key,
                kind: HashManifestBuildIssueKind::ReadError {
                    message: err.to_string(),
                },
            });
        }
    };
    let bytes = payload.bytes();
    #[allow(clippy::cast_possible_truncation)]
    let raw_file_size = bytes.len() as u32;
    let mut hasher = Sha256::new();
    hasher.update(bytes);
    let hash: [u8; 32] = hasher.finalize().into();
    let mut entry = HashManifestEntry::new(hash, raw_file_size);
    entry.path = path_hint;
    debug!(
        kind = ?kind,
        raw_file_size,
        "hash_manifest.build_parallel typed entry",
    );
    HashBuildOutcome::Entry { key, entry }
}

#[derive(Debug, Copy, Clone)]
enum IndexVariant {
    V1,
    V2,
}

pub(crate) fn enumerate_archives_parallel(
    sq: &SqPack,
) -> Vec<crate::error::Result<EnumeratedArchive>> {
    let mut slots: Vec<(ArchiveTriple, IndexVariant)> = Vec::new();
    for (_, raw_id) in KNOWN_CATEGORIES {
        let category = Category::from_known_id(*raw_id);
        for expansion in 0u8..=15 {
            for chunk in 0u8..=7 {
                let triple = ArchiveTriple {
                    category,
                    expansion,
                    chunk,
                };
                let Some(index_path) = triple.index_path(sq.install_root(), sq.platform()) else {
                    continue;
                };
                let Some(index2_path) = triple.index2_path(sq.install_root(), sq.platform()) else {
                    continue;
                };
                let v1 = sq.vfs_ref().exists(&index_path);
                let v2 = sq.vfs_ref().exists(&index2_path);
                if !v1 && !v2 {
                    break;
                }
                if v1 {
                    slots.push((triple, IndexVariant::V1));
                }
                if v2 {
                    slots.push((triple, IndexVariant::V2));
                }
            }
        }
    }

    slots
        .into_par_iter()
        .map(|(triple, variant)| match variant {
            IndexVariant::V1 => {
                let path = triple
                    .index_path(sq.install_root(), sq.platform())
                    .expect("path-builder invariant — non-Unknown platform");
                crate::index::Index::open_in(&path, sq.vfs_ref())
                    .map(|idx| EnumeratedArchive::V1(triple, idx))
            }
            IndexVariant::V2 => {
                let path = triple
                    .index2_path(sq.install_root(), sq.platform())
                    .expect("path-builder invariant — non-Unknown platform");
                crate::index2::Index2::open_in(&path, sq.vfs_ref())
                    .map(|idx| EnumeratedArchive::V2(triple, idx))
            }
        })
        .collect()
}

impl<'a> SqPackPathVerifier<'a> {
    pub(crate) fn sq(&self) -> &'a SqPack {
        self.sq_internal()
    }
    pub(crate) fn cancel(&self) -> Option<&CancelToken> {
        self.cancel_internal()
    }
}

impl<'a> SqPackHashVerifier<'a> {
    pub(crate) fn sq(&self) -> &'a SqPack {
        self.sq_internal()
    }
    pub(crate) fn cancel(&self) -> Option<&CancelToken> {
        self.cancel_internal()
    }
}

impl<'a> SqPackHashManifestBuilder<'a> {
    pub(crate) fn sq(&self) -> &'a SqPack {
        self.sq_internal()
    }
    pub(crate) fn cancel(&self) -> Option<&CancelToken> {
        self.cancel_internal()
    }
}

#[cfg(test)]
mod tests {
    use super::{
        HashBuildOutcome, HashBuildTask, HashVerifyOutcome, HashVerifyTask, VerifyOutcome,
    };

    fn assert_send<T: Send>() {}

    #[test]
    fn internal_outcome_enums_are_send() {
        assert_send::<VerifyOutcome>();
        assert_send::<HashVerifyOutcome>();
        assert_send::<HashBuildOutcome>();
        assert_send::<HashVerifyTask>();
        assert_send::<HashBuildTask>();
    }
}
