//! Progress and cancellation hooks for verifier and scanning consumers.
//!
//! - [`PathVerifyObserver`] — path-keyed callbacks for
//!   [`SqPackPathVerifier`](crate::SqPackPathVerifier).
//! - [`HashScanObserver`] — hash-keyed callbacks for
//!   [`SqPackHashManifestBuilder`](crate::SqPackHashManifestBuilder) and
//!   [`SqPackHashVerifier`](crate::SqPackHashVerifier).
//!
//! Both traits carry a `should_cancel` poll. All methods have no-op
//! defaults so implementors only override what they care about. Composes
//! with [`CancelToken`](crate::CancelToken) — both are polled, either
//! can abort.

use crate::dat::FileKind;
use crate::verify::{HashManifestBuildIssue, HashMismatch, IndexHashKey, PathMismatch};

/// Hook trait for observing path-keyed verify-time progress and
/// signalling cancellation.
///
/// Used by [`SqPackPathVerifier`](crate::SqPackPathVerifier). All
/// methods have no-op defaults so implementors override only what they
/// need.
///
/// # Threading
///
/// An observer is borrowed mutably by the verifier for the lifetime of the
/// [`verify`](crate::SqPackPathVerifier::verify) /
/// [`verify_streaming`](crate::SqPackPathVerifier::verify_streaming) call.
/// There is no internal synchronisation: implementors that need to forward
/// events to another thread should do so via channels they own.
///
/// The trait has a [`Send`] supertrait bound so a boxed observer can be
/// constructed on one thread and driven on another — the typical UI
/// pattern is to construct the observer (often around an
/// [`mpsc::Sender`](std::sync::mpsc::Sender) or an
/// [`AtomicBool`](std::sync::atomic::AtomicBool) cancellation flag) on the
/// UI thread, hand it to an [`SqPackPathVerifier`](crate::SqPackPathVerifier),
/// and ship the verifier to a worker via
/// [`tokio::task::spawn_blocking`](https://docs.rs/tokio/latest/tokio/task/fn.spawn_blocking.html)
/// or `std::thread::spawn`. [`Sync`] is **not** required because the
/// verifier holds the observer by exclusive borrow — multi-reader access
/// is not part of the contract.
///
/// # Async usage
///
/// All methods run inline with the verify loop and are intentionally
/// synchronous — see the crate-level "Async usage" section for the
/// rationale. The cancellation poll in particular is called once per
/// manifest entry and must be cheap (an atomic-bool load is the canonical
/// implementation), which makes it a poor fit for `async` even
/// hypothetically.
///
/// Async consumers wrap the whole verify call in
/// [`tokio::task::spawn_blocking`](https://docs.rs/tokio/latest/tokio/task/fn.spawn_blocking.html)
/// and use a [`CancelToken`](crate::CancelToken) the async side can flip
/// from a `tokio::select!` arm. Per-file events that need to reach an
/// async UI go through a channel whose `Sender` lives inside
/// [`Self::on_file_verified`] / [`Self::on_mismatch`] / [`Self::on_skipped`]
/// and whose `Receiver` is polled from the async task.
///
/// # Example
///
/// ```no_run
/// use sqpack_rs::verify::PathMismatch;
/// use sqpack_rs::{FileKind, SqPack, SqPackPathManifest, PathVerifyObserver, SqPackPathVerifier};
///
/// struct Progress {
///     total: usize,
///     verified: usize,
/// }
///
/// impl PathVerifyObserver for Progress {
///     fn on_file_verified(&mut self, _game_path: &str, _bytes_hashed: u32) {
///         self.verified += 1;
///         println!("progress: {}/{}", self.verified, self.total);
///     }
///
///     fn on_mismatch(&mut self, m: &PathMismatch) {
///         eprintln!("mismatch: {}", m.game_path);
///     }
///
///     fn on_skipped(&mut self, game_path: &str, kind: FileKind) {
///         eprintln!("skipped {game_path}: kind {kind:?}");
///     }
/// }
///
/// let sq = SqPack::open("/opt/ffxiv/game").unwrap();
/// let manifest = SqPackPathManifest::new();
/// let progress = Progress { total: manifest.len(), verified: 0 };
/// let mut verifier = SqPackPathVerifier::new(&sq).with_observer(progress);
/// let _report = verifier.verify(&manifest);
/// ```
pub trait PathVerifyObserver: Send {
    /// Called once per manifest entry that verified cleanly.
    ///
    /// `bytes_hashed` is the on-disk `raw_file_size` of the verified
    /// payload — equivalent to
    /// [`PathManifestEntry::size`](crate::verify::PathManifestEntry::size). Useful for
    /// driving a `bytes_done / bytes_total` progress bar.
    ///
    /// The default implementation does nothing.
    fn on_file_verified(&mut self, game_path: &str, bytes_hashed: u32) {
        let _ = (game_path, bytes_hashed);
    }

    /// Called once per manifest entry that failed to verify.
    ///
    /// The supplied [`PathMismatch`] is the same value that the verifier will
    /// also record in [`PathVerifyReport::mismatches`](crate::verify::PathVerifyReport::mismatches)
    /// (eager variant) or yield from
    /// [`PathVerifyStream`](crate::verify::PathVerifyStream) (streaming
    /// variant). The reference is borrowed for the duration of the call —
    /// implementors that need to retain it must clone.
    ///
    /// The default implementation does nothing.
    fn on_mismatch(&mut self, mismatch: &PathMismatch) {
        let _ = mismatch;
    }

    /// Called once per manifest entry whose [`FileKind`] is one the verifier
    /// can't hash (Empty / Unknown).
    ///
    /// Note: an `on_skipped` callback is **always** paired with an
    /// [`on_mismatch`](Self::on_mismatch) callback carrying the same
    /// `game_path` and a [`PathMismatchKind::Unverifiable`](crate::verify::PathMismatchKind::Unverifiable)
    /// variant — skipped entries are accounted in both
    /// [`PathVerifyReport::skipped_count`](crate::verify::PathVerifyReport::skipped_count)
    /// and [`PathVerifyReport::mismatches`](crate::verify::PathVerifyReport::mismatches).
    /// Implementors that only want one of the two should override the
    /// other to a no-op.
    ///
    /// The default implementation does nothing.
    fn on_skipped(&mut self, game_path: &str, kind: FileKind) {
        let _ = (game_path, kind);
    }

    /// Polled between manifest entries to check for user cancellation.
    ///
    /// Implementors should make this method **cheap** — it is called once
    /// per manifest entry. A simple atomic-bool load is the recommended
    /// implementation.
    ///
    /// Returning `true` causes the current verify call to stop walking the
    /// manifest. The eager [`verify`](crate::SqPackPathVerifier::verify) path
    /// returns a partial [`PathVerifyReport`](crate::verify::PathVerifyReport) with
    /// [`cancelled = true`](crate::verify::PathVerifyReport::cancelled) and the
    /// work-done-so-far accumulated in
    /// [`mismatches`](crate::verify::PathVerifyReport::mismatches) /
    /// [`verified_count`](crate::verify::PathVerifyReport::verified_count) /
    /// [`skipped_count`](crate::verify::PathVerifyReport::skipped_count). The streaming
    /// path ([`PathVerifyStream`](crate::verify::PathVerifyStream)) stops yielding events
    /// — `next()` returns `None`. The accumulators reflect work-done-so-far.
    ///
    /// The default implementation always returns `false`.
    fn should_cancel(&mut self) -> bool {
        false
    }
}

/// Hook trait for observing hash-keyed scan and verify progress and
/// signalling cancellation.
///
/// Used by [`SqPackHashManifestBuilder`](crate::SqPackHashManifestBuilder)
/// and [`SqPackHashVerifier`](crate::SqPackHashVerifier). All methods
/// have no-op defaults so implementors override only what they need.
///
/// # Threading
///
/// Same [`Send`] + exclusive-borrow contract as [`PathVerifyObserver`].
/// See that trait's threading and async-usage sections — the same rules
/// apply.
pub trait HashScanObserver: Send {
    /// Called once per archive entry that hashed cleanly during a
    /// [`SqPackHashManifestBuilder`](crate::SqPackHashManifestBuilder) run.
    ///
    /// `bytes_hashed` is the on-disk `raw_file_size` of the hashed payload
    /// — equivalent to
    /// [`HashManifestEntry::size`](crate::verify::HashManifestEntry::size). The
    /// per-entry tuple is keyed on [`IndexHashKey`] rather than a game path
    /// because the walker hashes by index entry, not by routed path. A
    /// human-readable path is available on the resulting
    /// [`HashManifestEntry::path`](crate::verify::HashManifestEntry::path) when (and
    /// only when) the caller has populated it after the build.
    ///
    /// The default implementation does nothing.
    fn on_entry_hashed(&mut self, key: &IndexHashKey, bytes_hashed: u32) {
        let _ = (key, bytes_hashed);
    }

    /// Called once per archive entry the hash-keyed verifier compared
    /// against the manifest and found a mismatch (any
    /// [`PathMismatchKind`](crate::verify::PathMismatchKind) variant).
    ///
    /// The supplied [`HashMismatch`] is the same value the verifier records
    /// in [`HashVerifyReport::mismatches`](crate::verify::HashVerifyReport::mismatches).
    /// The reference is borrowed for the duration of the call; implementors
    /// that need to retain it must clone.
    ///
    /// Archive-scope read failures (e.g. a corrupt `.index` whose
    /// `enumerate_archives` step returned `Err`) surface here too, carrying
    /// a synthetic [`IndexHashKey::V1`](crate::verify::IndexHashKey::V1) keyed on
    /// the crate-internal placeholder [`Category`](crate::Category),
    /// expansion `0`, chunk `0`, and hash `0`. Consumers aggregating by
    /// category should treat the placeholder category as "archive-scope
    /// failure, real triple unknown" rather than as a real category
    /// bucket.
    ///
    /// The default implementation does nothing.
    fn on_entry_mismatch(&mut self, mismatch: &HashMismatch) {
        let _ = mismatch;
    }

    /// Called once per archive entry that the hash-keyed builder couldn't
    /// hash because its [`FileKind`] is Empty / Unknown, or because the
    /// per-entry read errored out.
    ///
    /// The supplied [`HashManifestBuildIssue`] is the same value the
    /// builder records in
    /// [`HashManifestBuildReport::issues`](crate::verify::HashManifestBuildReport::issues).
    /// The reference is borrowed for the duration of the call; implementors
    /// that need to retain it must clone.
    ///
    /// Archive-scope read failures (e.g. a corrupt `.index` whose
    /// `enumerate_archives` step returned `Err`) surface here too, carrying
    /// a synthetic [`IndexHashKey::V1`](crate::verify::IndexHashKey::V1) keyed on
    /// the crate-internal placeholder [`Category`](crate::Category),
    /// expansion `0`, chunk `0`, and hash `0`. Consumers aggregating by
    /// category should treat the placeholder category as "archive-scope
    /// failure, real triple unknown" rather than as a real category
    /// bucket.
    ///
    /// The default implementation does nothing.
    fn on_entry_build_issue(&mut self, issue: &HashManifestBuildIssue) {
        let _ = issue;
    }

    /// Called by the hash-keyed verifier once per archive entry that is
    /// present in the live install but not in the manifest — an
    /// "extra" file (typically introduced by a post-baseline game update or
    /// a sideloaded mod).
    ///
    /// Mirrors the [`Self::on_entry_mismatch`] callback in shape; the
    /// `extras` set is accumulated separately on
    /// [`HashVerifyReport::extras`](crate::verify::HashVerifyReport::extras).
    ///
    /// The default implementation does nothing.
    fn on_entry_extra(&mut self, key: &IndexHashKey) {
        let _ = key;
    }

    /// Polled between archive entries to check for user cancellation.
    ///
    /// Same contract as [`PathVerifyObserver::should_cancel`] — must be
    /// cheap, typically an atomic-bool load.
    ///
    /// The default implementation always returns `false`.
    fn should_cancel(&mut self) -> bool {
        false
    }
}

/// No-op observer used by
/// [`SqPackPathVerifier`](crate::SqPackPathVerifier) and
/// [`SqPackHashVerifier`](crate::SqPackHashVerifier) when none is
/// configured.
///
/// Public because the `with_observer` methods are generic over
/// `impl PathVerifyObserver` / `impl HashScanObserver` and callers
/// occasionally need to name the default in
/// `Box<dyn PathVerifyObserver>`-typed or
/// `Box<dyn HashScanObserver>`-typed fields. All trait methods use
/// the trait defaults.
#[derive(Debug, Default, Clone, Copy)]
pub struct NoopObserver;

impl PathVerifyObserver for NoopObserver {}
impl HashScanObserver for NoopObserver {}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::route::Category;
    use crate::verify::PathMismatchKind;

    // --- Thread-safety bounds ---

    #[test]
    fn boxed_path_verify_observer_is_send() {
        fn assert_send<T: Send + ?Sized>() {}
        assert_send::<dyn PathVerifyObserver>();
        assert_send::<Box<dyn PathVerifyObserver>>();
        let boxed: Box<dyn PathVerifyObserver> = Box::new(NoopObserver);
        std::thread::spawn(move || {
            let _ = boxed;
        })
        .join()
        .unwrap();
    }

    #[test]
    fn boxed_hash_scan_observer_is_send() {
        fn assert_send<T: Send + ?Sized>() {}
        assert_send::<dyn HashScanObserver>();
        assert_send::<Box<dyn HashScanObserver>>();
        let boxed: Box<dyn HashScanObserver> = Box::new(NoopObserver);
        std::thread::spawn(move || {
            let _ = boxed;
        })
        .join()
        .unwrap();
    }

    #[test]
    fn noop_observer_is_send_and_sync() {
        // `NoopObserver` is a unit struct with `#[derive(Clone, Copy)]` —
        // `Send + Sync` is structurally trivial, but this compile-time pin
        // catches any future change that introduces a non-`Send`/non-`Sync`
        // field (e.g. a `Rc` or `RefCell`).
        fn assert_send_sync<T: Send + Sync>() {}
        assert_send_sync::<NoopObserver>();
    }

    // --- NoopObserver ---

    #[test]
    fn noop_observer_should_cancel_always_false() {
        let mut obs = NoopObserver;
        // Call multiple times; must stay false — never flip to true.
        // Test both trait impls.
        for _ in 0..5 {
            assert!(
                !PathVerifyObserver::should_cancel(&mut obs),
                "NoopObserver must never request cancellation (path)",
            );
            assert!(
                !HashScanObserver::should_cancel(&mut obs),
                "NoopObserver must never request cancellation (hash)",
            );
        }
    }

    #[test]
    fn noop_observer_path_callbacks_are_inert() {
        // Sanity: the no-op defaults compile and accept the documented
        // argument shapes without panicking.
        let mut obs = NoopObserver;
        obs.on_file_verified("exd/root.exl", 42);
        obs.on_skipped("bgcommon/foo.mdl", FileKind::Model);
        let exd = Category::try_from_name("exd").expect("exd category");
        let m = crate::verify::PathMismatch::new(
            "exd/missing".to_owned(),
            exd,
            PathMismatchKind::Missing,
        );
        obs.on_mismatch(&m);
    }

    // --- Default impls ---

    /// A custom observer that only overrides `should_cancel` exercises the
    /// no-op defaults for the other methods.
    #[test]
    fn defaulted_path_callbacks_compile() {
        struct OnlyCancel;
        impl PathVerifyObserver for OnlyCancel {
            fn should_cancel(&mut self) -> bool {
                true
            }
        }
        let mut o = OnlyCancel;
        assert!(o.should_cancel());
        // Default no-op methods are still callable.
        o.on_file_verified("p", 0);
        o.on_skipped("p", FileKind::Empty);
    }

    #[test]
    fn defaulted_hash_callbacks_compile() {
        struct OnlyCancel;
        impl HashScanObserver for OnlyCancel {
            fn should_cancel(&mut self) -> bool {
                true
            }
        }
        let mut o = OnlyCancel;
        assert!(o.should_cancel());
    }
}
