/*
 * Copyright (c) 2026 Jonathan Perkin <jonathan@perkin.org.uk>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

/*!
 * Parse `make pbulk-index` output into package records.
 *
 * The `pbulk-index` make target is part of pkgsrc's infrastructure. It outputs
 * machine-readable information about a package that is used to plan and execute
 * bulk builds across the entire pkgsrc tree.
 *
 * When you run `make pbulk-index` in a package directory, it outputs a series
 * of `KEY=VALUE` pairs containing:
 *
 * - Package name and location in the pkgsrc tree
 * - All dependencies required to build the package
 * - Skip/fail reasons if the package cannot be built
 * - License and distribution restrictions
 * - Build metadata (categories, maintainer, etc.)
 *
 * # Output Format
 *
 * The output consists of multiple records separated by blank lines. Each record
 * contains one package (a single `PKGPATH` may produce multiple records if
 * `MULTI_VERSION` is used):
 *
 * # Example
 *
 * ```no_run
 * use pkgsrc::ScanIndex;
 * use std::io::BufReader;
 * use std::process::{Command, Stdio};
 *
 * // Run pbulk-index on a package
 * let output = Command::new("make")
 *     .current_dir("/usr/pkgsrc/www/nginx")
 *     .arg("pbulk-index")
 *     .output()
 *     .expect("failed to execute make");
 *
 * // Parse the output
 * let reader = BufReader::new(&output.stdout[..]);
 * for result in ScanIndex::from_reader(reader) {
 *     let record = result?;
 *     println!("Package: {}", record.pkgname);
 *     if let Some(deps) = &record.all_depends {
 *         println!("  Dependencies: {}", deps.iter().count());
 *     }
 * }
 * # Ok::<(), std::io::Error>(())
 * ```
 */

use crate::kv::KvWarning;
use crate::{Depend, DependError, PkgName, PkgPath};
use pkgsrc_kv::Kv;
use std::fmt;
use std::io::{self, BufRead};
use std::path::Path;
use std::str::FromStr;

/**
 * Space-separated list of filesystem paths, stored as a single string.
 *
 * `ScanDepends` wraps the raw `SCAN_DEPENDS` value from `pbulk-index` output
 * without splitting or allocating individual path entries.  This avoids
 * millions of [`PathBuf`] allocations when parsing large scan datasets.
 *
 * Individual paths can be accessed lazily via [`iter`](ScanDepends::iter).
 *
 * # Examples
 *
 * ```
 * use pkgsrc::ScanDepends;
 * use std::path::Path;
 *
 * let deps: ScanDepends = "../../devel/gmake ../../lang/rust".into();
 * let paths: Vec<&Path> = deps.iter().collect();
 * assert_eq!(paths.len(), 2);
 * assert_eq!(paths[0], Path::new("../../devel/gmake"));
 * assert_eq!(paths[1], Path::new("../../lang/rust"));
 * ```
 */
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
pub struct ScanDepends(String);

impl ScanDepends {
    fn items(&self) -> std::str::SplitAsciiWhitespace<'_> {
        self.0.split_ascii_whitespace()
    }

    /**
     * Iterate over the individual paths.
     */
    pub fn iter(&self) -> impl Iterator<Item = &Path> {
        self.items().map(Path::new)
    }

    /**
     * Return the raw string contents.
     */
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl AsRef<str> for ScanDepends {
    fn as_ref(&self) -> &str {
        &self.0
    }
}

impl fmt::Display for ScanDepends {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl From<&str> for ScanDepends {
    fn from(s: &str) -> Self {
        ScanDepends(s.to_string())
    }
}

impl crate::kv::FromKv for ScanDepends {
    fn from_kv(value: &str, _span: crate::kv::Span) -> crate::kv::Result<Self> {
        Ok(ScanDepends(value.to_string()))
    }
}

/**
 * Space-separated list of dependency entries, stored as a single string.
 *
 * `AllDepends` wraps the raw `ALL_DEPENDS` value from `pbulk-index` output
 * without parsing individual [`Depend`] entries upfront.  This avoids
 * compiling hundreds of thousands of [`Pattern`] objects during parsing,
 * deferring that cost to when the data is actually accessed.
 *
 * Two iteration modes are available:
 *
 * - [`iter`](AllDepends::iter) yields [`RawDepend`] views with cheap access
 *   to the pattern and pkgpath strings.
 * - [`depends`](AllDepends::depends) yields fully parsed [`Depend`] objects
 *   with compiled [`Pattern`]s.
 *
 * Both yield [`Result`]s, with validation errors surfaced via `?`.
 *
 * # Examples
 *
 * ```
 * use pkgsrc::AllDepends;
 *
 * let deps: AllDepends = "mktool-[0-9]*:../../pkgtools/mktool curl>=7.0:../../www/curl".into();
 *
 * // Cheap access to raw strings
 * for dep in deps.iter() {
 *     let dep = dep?;
 *     println!("{} -> {}", dep.pattern(), dep.pkgpath());
 * }
 *
 * // Full parsing into Depend objects
 * for dep in deps.depends() {
 *     let dep = dep?;
 *     assert!(dep.pattern().matches("mktool-1.4.2") || dep.pattern().matches("curl-8.0"));
 * }
 * # Ok::<(), pkgsrc::DependError>(())
 * ```
 *
 * [`Pattern`]: crate::Pattern
 */
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
pub struct AllDepends(String);

impl AllDepends {
    fn items(&self) -> std::str::SplitAsciiWhitespace<'_> {
        self.0.split_ascii_whitespace()
    }

    /**
     * Iterate over entries as lightweight borrowed views.
     *
     * Each entry is validated to contain a `:` separator.  Access the
     * pattern and pkgpath strings via [`RawDepend::pattern`] and
     * [`RawDepend::pkgpath`].
     */
    pub fn iter(&self) -> AllDependsIter<'_> {
        AllDependsIter(self.items())
    }

    /**
     * Iterate over entries as fully parsed [`Depend`] objects.
     *
     * Each entry is validated and the [`Pattern`] is compiled.  This is
     * more expensive than [`iter`](AllDepends::iter) but provides access
     * to pattern matching.
     *
     * [`Pattern`]: crate::Pattern
     */
    pub fn depends(
        &self,
    ) -> impl Iterator<Item = Result<Depend, DependError>> + '_ {
        self.items().map(Depend::new)
    }

    /**
     * Return the raw string contents.
     */
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

/**
 * Iterator over [`AllDepends`] entries as [`RawDepend`] views.
 */
pub struct AllDependsIter<'a>(std::str::SplitAsciiWhitespace<'a>);

impl<'a> Iterator for AllDependsIter<'a> {
    type Item = Result<RawDepend<'a>, DependError>;

    fn next(&mut self) -> Option<Self::Item> {
        self.0.next().map(RawDepend::new)
    }
}

impl<'a> IntoIterator for &'a AllDepends {
    type Item = Result<RawDepend<'a>, DependError>;
    type IntoIter = AllDependsIter<'a>;

    fn into_iter(self) -> Self::IntoIter {
        self.iter()
    }
}

impl AsRef<str> for AllDepends {
    fn as_ref(&self) -> &str {
        &self.0
    }
}

impl fmt::Display for AllDepends {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl From<&str> for AllDepends {
    fn from(s: &str) -> Self {
        AllDepends(s.to_string())
    }
}

impl<S: AsRef<str>> FromIterator<S> for AllDepends {
    /**
     * Collect `pattern:pkgpath` entries into an [`AllDepends`].  Entries
     * are joined with a single space; no validation of the `pattern:pkgpath`
     * shape is performed here (use [`AllDepends::iter`] to validate).
     */
    fn from_iter<I: IntoIterator<Item = S>>(iter: I) -> Self {
        let mut out = String::new();
        for item in iter {
            if !out.is_empty() {
                out.push(' ');
            }
            out.push_str(item.as_ref());
        }
        AllDepends(out)
    }
}

impl crate::kv::FromKv for AllDepends {
    fn from_kv(value: &str, _span: crate::kv::Span) -> crate::kv::Result<Self> {
        Ok(AllDepends(value.to_string()))
    }
}

/**
 * A borrowed view of a single `pattern:pkgpath` dependency entry.
 *
 * Provides zero-cost access to the raw string and near-zero-cost access
 * to the pattern and pkgpath components.  For a fully validated [`Depend`]
 * with a compiled [`Pattern`], use [`TryFrom`]:
 *
 * ```
 * use pkgsrc::{AllDepends, Depend};
 *
 * let deps: AllDepends = "mktool-[0-9]*:../../pkgtools/mktool".into();
 * for raw in deps.iter() {
 *     let raw = raw?;
 *     let depend: Depend = raw.try_into()?;
 *     assert!(depend.pattern().matches("mktool-1.4.2"));
 * }
 * # Ok::<(), pkgsrc::DependError>(())
 * ```
 *
 * [`Pattern`]: crate::Pattern
 */
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RawDepend<'a> {
    raw: &'a str,
    colon: usize,
}

impl<'a> RawDepend<'a> {
    fn new(raw: &'a str) -> Result<Self, DependError> {
        let colon = raw.find(':').ok_or(DependError::Invalid)?;
        if raw[colon + 1..].contains(':') {
            return Err(DependError::Invalid);
        }
        Ok(RawDepend { raw, colon })
    }

    /**
     * Return the pattern portion (left of `:`).
     */
    pub fn pattern(&self) -> &'a str {
        &self.raw[..self.colon]
    }

    /**
     * Return the pkgpath portion (right of `:`).
     */
    pub fn pkgpath(&self) -> &'a str {
        &self.raw[self.colon + 1..]
    }

    /**
     * Return the raw `pattern:pkgpath` string.
     */
    pub fn as_str(&self) -> &'a str {
        self.raw
    }
}

impl fmt::Display for RawDepend<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.raw)
    }
}

/**
 * `MAKE_JOBS_SAFE` flag.
 *
 * Per pkgsrc's `mk/build/build.mk`, a package is treated as not parallel-safe
 * only when the value matches `[nN][oO]`; any other value, or the variable
 * being unset, means safe.
 */
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
pub struct MakeJobsSafe(bool);

impl MakeJobsSafe {
    /** Whether the package supports parallel builds (`make -j`). */
    #[must_use]
    pub fn is_safe(&self) -> bool {
        self.0
    }
}

impl crate::kv::FromKv for MakeJobsSafe {
    fn from_kv(value: &str, _span: crate::kv::Span) -> crate::kv::Result<Self> {
        Ok(MakeJobsSafe(!value.eq_ignore_ascii_case("no")))
    }
}

impl From<bool> for MakeJobsSafe {
    fn from(safe: bool) -> Self {
        MakeJobsSafe(safe)
    }
}

/**
 * `BOOTSTRAP_PKG` flag.
 *
 * The value is either `yes` for a package that is part of the pkgsrc
 * bootstrap, or empty for one that is not.
 */
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
pub struct BootstrapPkg(bool);

impl BootstrapPkg {
    /** Whether this package is used during pkgsrc bootstrap. */
    #[must_use]
    pub fn is_bootstrap(&self) -> bool {
        self.0
    }
}

impl crate::kv::FromKv for BootstrapPkg {
    fn from_kv(value: &str, _span: crate::kv::Span) -> crate::kv::Result<Self> {
        Ok(BootstrapPkg(value == "yes"))
    }
}

impl From<bool> for BootstrapPkg {
    fn from(bootstrap: bool) -> Self {
        BootstrapPkg(bootstrap)
    }
}

/**
 * Parse the output of `make pbulk-index` into individual records.
 *
 * See [pbulk-index.mk] and [pbulk-build(1)].
 *
 * While the majority of these fields will always be set even if left empty,
 * they are wrapped in [`Option`] to simplify tests as well as handle cases in
 * the future should they be removed from the default output.
 *
 * [pbulk-index.mk]: https://github.com/NetBSD/pkgsrc/blob/trunk/mk/pbulk/pbulk-index.mk
 * [pbulk-build(1)]: https://github.com/NetBSD/pkgsrc/blob/trunk/pkgtools/pbulk/files/pbulk/pbuild/pbulk-build.1
 *
 * # Example
 *
 * ```no_run
 * use pkgsrc::{PkgName, ScanIndex};
 * use std::io::BufReader;
 * use std::process::{Command, Stdio};
 *
 * let mut cmd = Command::new("make")
 *     .current_dir("/usr/pkgsrc/databases/php-mysql")
 *     .arg("pbulk-index")
 *     .stdout(Stdio::piped())
 *     .spawn()
 *     .expect("Unable to execute make");
 * let stdout = cmd.stdout.take().expect("no stdout");
 * let reader = BufReader::new(stdout);
 * let index: Vec<_> = ScanIndex::from_reader(reader)
 *     .collect::<std::result::Result<_, _>>()?;
 *
 * // Should return 5 results due to MULTI_VERSION
 * assert_eq!(index.len(), 5);
 * assert_eq!(index[0].pkgname, PkgName::new("php56-mysql-5.6.40nb1"));
 * # Ok::<(), std::io::Error>(())
 * ```
 */
#[derive(Clone, Debug, Default, Eq, Hash, Kv, PartialEq)]
#[cfg_attr(feature = "serde", kv(serde))]
pub struct ScanIndex {
    /// Name of the package including the version number.
    pub pkgname: PkgName,
    /// Path to the package inside pkgsrc.  Should really be PKGPATH.
    pub pkg_location: Option<PkgPath>,
    /// All dependencies of the package as DEPENDS matches.
    pub all_depends: Option<AllDepends>,
    /// A string containing the reason if the package should be skipped.
    pub pkg_skip_reason: Option<String>,
    /// A string containing the reason if the package failed or is broken.
    pub pkg_fail_reason: Option<String>,
    /// A string containing the reason why its binary package may not be
    /// uploaded.
    pub no_bin_on_ftp: Option<String>,
    /// A string containing the reason why its binary package may not be
    /// distributed.
    pub restricted: Option<String>,
    /// Categories to which the package belongs.
    pub categories: Option<String>,
    /// Maintainer of the package.
    pub maintainer: Option<String>,
    /// `DESTDIR` method this package supports (almost always `user-destdir`).
    pub use_destdir: Option<String>,
    /// If this package is used during pkgsrc bootstrap.
    pub bootstrap_pkg: Option<BootstrapPkg>,
    /// The phase of the build process during which the user and/or group
    /// needed by this package need to be available.
    pub usergroup_phase: Option<String>,
    /// List of files read during the dependency scanning step.
    pub scan_depends: Option<ScanDepends>,
    /// Whether this package supports parallel builds (`make -j`). Packages
    /// that do not support parallel builds set `MAKE_JOBS_SAFE=no`.
    pub make_jobs_safe: Option<MakeJobsSafe>,
    /// Numeric build priority of the package. If not set, or if an invalid
    /// value is supplied, a value of 100 is assumed.
    #[kv(lenient)]
    pub pbulk_weight: Option<u32>,
    /// List of variables to be set when building this specific `PKGNAME` from
    /// a common `PKGPATH`.
    pub multi_version: Option<Vec<String>>,
    /// Resolved dependencies as concrete package names.
    ///
    /// This field is not part of the `bmake pbulk-index` output. It is
    /// populated by dependency resolvers (such as pbulk or bob) that take
    /// the patterns in [`all_depends`] and resolve them to specific package
    /// names based on what is available in the package set.
    ///
    /// When parsing presolve files (scan output augmented with resolution
    /// results), this field will contain the resolved dependencies. When
    /// parsing raw scan output, this field will be `None`.  Consumers that
    /// just want a slice can use [`depends`](Self::depends).
    ///
    /// [`all_depends`]: ScanIndex::all_depends
    #[kv(variable = "DEPENDS")]
    pub resolved_depends: Option<Vec<PkgName>>,
}

impl FromStr for ScanIndex {
    type Err = crate::kv::KvError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Self::parse(s)
    }
}

/**
 * Which record format the common writer should emit.  Selected by the
 * [`Pscan`], [`Presolve`], and [`Report`] wrappers.
 */
#[derive(Clone, Copy)]
enum FormatMode {
    /// `bmake pbulk-index` scanner output: includes `PBULK_WEIGHT`, no `DEPENDS`.
    Pscan,
    /// Resolved scan output: scanner output plus resolved `DEPENDS`.
    Presolve,
    /// pbulk report record: like [`Presolve`](Self::Presolve) but without `PBULK_WEIGHT`.
    Report,
}

impl ScanIndex {
    /**
     * Format as `bmake pbulk-index` scanner output.
     *
     * This is the raw pre-resolution format as emitted by the `pbulk-index`
     * target in pkgsrc: all static metadata about a package, including
     * `PBULK_WEIGHT`, but without `DEPENDS` (which is populated later by a
     * resolver).  Use this when producing input for a resolver, or when
     * round-tripping scanner output.
     */
    #[must_use]
    pub fn pscan(&self) -> Pscan<'_> {
        Pscan(self)
    }

    /**
     * Format as resolved scan output.
     *
     * Equivalent to [`pscan`](Self::pscan) plus a `DEPENDS=` line when
     * [`resolved_depends`](Self::resolved_depends) is populated.  This is the
     * format a resolver emits after mapping `ALL_DEPENDS` patterns onto
     * concrete package names.
     */
    #[must_use]
    pub fn presolve(&self) -> Presolve<'_> {
        Presolve(self)
    }

    /**
     * Format as a pbulk report record.
     *
     * Equivalent to [`presolve`](Self::presolve) without the `PBULK_WEIGHT`
     * line.  Matches the per-package block emitted in pbulk's `report` file
     * (the caller appends `PKG_DEPTH` and `BUILD_STATUS` themselves).
     */
    #[must_use]
    pub fn report(&self) -> Report<'_> {
        Report(self)
    }

    /**
     * Resolved dependencies as a slice.  Returns `&[]` when
     * [`resolved_depends`](Self::resolved_depends) is `None`.
     */
    #[must_use]
    pub fn depends(&self) -> &[PkgName] {
        self.resolved_depends.as_deref().unwrap_or(&[])
    }

    fn fmt_record(
        &self,
        f: &mut fmt::Formatter<'_>,
        mode: FormatMode,
    ) -> fmt::Result {
        writeln!(f, "PKGNAME={}", self.pkgname)?;
        if let Some(ref v) = self.pkg_location {
            writeln!(f, "PKG_LOCATION={v}")?;
        }
        write!(f, "ALL_DEPENDS=")?;
        if let Some(ref deps) = self.all_depends {
            write!(f, "{deps}")?;
        }
        writeln!(f)?;
        writeln!(f, "PKG_SKIP_REASON={}", opt_str(&self.pkg_skip_reason))?;
        writeln!(f, "PKG_FAIL_REASON={}", opt_str(&self.pkg_fail_reason))?;
        writeln!(f, "NO_BIN_ON_FTP={}", opt_str(&self.no_bin_on_ftp))?;
        writeln!(f, "RESTRICTED={}", opt_str(&self.restricted))?;
        writeln!(f, "CATEGORIES={}", opt_str(&self.categories))?;
        writeln!(f, "MAINTAINER={}", opt_str(&self.maintainer))?;
        writeln!(f, "USE_DESTDIR={}", opt_str(&self.use_destdir))?;
        let bootstrap_pkg = match &self.bootstrap_pkg {
            Some(b) if b.is_bootstrap() => "yes",
            _ => "",
        };
        writeln!(f, "BOOTSTRAP_PKG={bootstrap_pkg}")?;
        writeln!(f, "USERGROUP_PHASE={}", opt_str(&self.usergroup_phase))?;
        write!(f, "SCAN_DEPENDS=")?;
        if let Some(ref deps) = self.scan_depends {
            write!(f, "{deps}")?;
        }
        writeln!(f)?;
        if let Some(ref v) = self.make_jobs_safe
            && !v.is_safe()
        {
            writeln!(f, "MAKE_JOBS_SAFE=no")?;
        }
        if !matches!(mode, FormatMode::Report)
            && let Some(ref v) = self.pbulk_weight
        {
            writeln!(f, "PBULK_WEIGHT={v}")?;
        }
        if let Some(ref vars) = self.multi_version
            && !vars.is_empty()
        {
            write!(f, "MULTI_VERSION=")?;
            for v in vars {
                write!(f, " {v}")?;
            }
            writeln!(f)?;
        }
        if !matches!(mode, FormatMode::Pscan) {
            let deps = self.depends();
            if !deps.is_empty() {
                write!(f, "DEPENDS=")?;
                for (i, d) in deps.iter().enumerate() {
                    if i > 0 {
                        write!(f, " ")?;
                    }
                    write!(f, "{d}")?;
                }
                writeln!(f)?;
            }
        }
        Ok(())
    }
}

/**
 * [`Display`](fmt::Display) wrapper for `bmake pbulk-index` scanner output.
 *
 * Created by [`ScanIndex::pscan`].
 */
pub struct Pscan<'a>(&'a ScanIndex);

impl fmt::Display for Pscan<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.0.fmt_record(f, FormatMode::Pscan)
    }
}

/**
 * [`Display`](fmt::Display) wrapper for resolved scan output.
 *
 * Created by [`ScanIndex::presolve`].
 */
pub struct Presolve<'a>(&'a ScanIndex);

impl fmt::Display for Presolve<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.0.fmt_record(f, FormatMode::Presolve)
    }
}

/**
 * [`Display`](fmt::Display) wrapper for a pbulk report record.
 *
 * Created by [`ScanIndex::report`].
 */
pub struct Report<'a>(&'a ScanIndex);

impl fmt::Display for Report<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.0.fmt_record(f, FormatMode::Report)
    }
}

fn opt_str(o: &Option<String>) -> &str {
    o.as_deref().unwrap_or("")
}

impl ScanIndex {
    /**
     * Create an iterator that parses [`ScanIndex`] entries from a reader.
     *
     * Records are delimited by lines starting with `PKGNAME=`.  Each item is
     * an [`io::Result`], matching the convention of [`BufRead::lines`].
     *
     * Warnings from `#[kv(lenient)]` fields (such as an invalid
     * `PBULK_WEIGHT`) do not fail a record; they accumulate as the iterator is
     * consumed and can be read afterwards with
     * [`warnings`](ScanIndexIter::warnings) or
     * [`into_warnings`](ScanIndexIter::into_warnings).
     *
     * # Examples
     *
     * The warnings live on the iterator, so a plain `collect` that consumes it
     * drops them.  To keep them, drive the iterator through [`by_ref`] (or a
     * `for … in &mut iter` loop) so it outlives iteration:
     *
     * ```
     * use pkgsrc::ScanIndex;
     *
     * let input = b"PKGNAME=foo-1.0\nPBULK_WEIGHT=oops\n" as &[u8];
     * let mut scan = ScanIndex::from_reader(input);
     * let records: Vec<_> = scan.by_ref().collect::<Result<_, _>>()?;
     * assert_eq!(records.len(), 1);
     * assert_eq!(scan.warnings().len(), 1); /* still available */
     * # Ok::<(), std::io::Error>(())
     * ```
     *
     * [`by_ref`]: Iterator::by_ref
     */
    pub fn from_reader<R: BufRead>(reader: R) -> ScanIndexIter<R> {
        ScanIndexIter {
            reader,
            line_buf: String::new(),
            buffer: String::new(),
            done: false,
            warnings: Vec::new(),
        }
    }
}

/**
 * Iterator that parses [`ScanIndex`] entries from a [`BufRead`] source.
 *
 * Created by [`ScanIndex::from_reader`].  The iterator owns the warnings
 * accumulated from `#[kv(lenient)]` fields; read them once iteration is
 * complete via [`warnings`](Self::warnings) or [`into_warnings`](Self::into_warnings).
 */
pub struct ScanIndexIter<R> {
    reader: R,
    line_buf: String,
    buffer: String,
    done: bool,
    warnings: Vec<KvWarning>,
}

impl<R> ScanIndexIter<R> {
    /**
     * The warnings accumulated so far, in the order encountered.
     *
     * A record's warnings are appended as it is yielded, so this is complete
     * only once the iterator has been fully consumed.
     */
    #[must_use]
    pub fn warnings(&self) -> &[KvWarning] {
        &self.warnings
    }

    /**
     * Consume the iterator and return the accumulated warnings.
     */
    #[must_use]
    pub fn into_warnings(self) -> Vec<KvWarning> {
        self.warnings
    }
}

impl<R: BufRead> Iterator for ScanIndexIter<R> {
    type Item = io::Result<ScanIndex>;

    fn next(&mut self) -> Option<Self::Item> {
        if self.done {
            return None;
        }

        loop {
            self.line_buf.clear();
            match self.reader.read_line(&mut self.line_buf) {
                Ok(0) => {
                    self.done = true;
                    if self.buffer.is_empty() {
                        return None;
                    }
                    let record = std::mem::take(&mut self.buffer);
                    return Some(parse_record(&record, &mut self.warnings));
                }
                Ok(_) => {
                    if self.line_buf.starts_with("PKGNAME=")
                        && !self.buffer.is_empty()
                    {
                        let record = std::mem::take(&mut self.buffer);
                        self.buffer.push_str(&self.line_buf);
                        return Some(parse_record(&record, &mut self.warnings));
                    }
                    self.buffer.push_str(&self.line_buf);
                }
                Err(e) => return Some(Err(e)),
            }
        }
    }
}

fn parse_record(
    s: &str,
    warnings: &mut Vec<KvWarning>,
) -> io::Result<ScanIndex> {
    ScanIndex::parse_with_warnings(s, warnings)
        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::kv::KvError;
    use anyhow::Context;
    use std::fs::File;
    use std::io::BufReader;
    use std::path::PathBuf;

    /*
     * Real-world test input generated using 'bmake pbulk-index' inside
     * databases/py-mysqlclient using pbulkmulti patches, so that there are
     * a total of 40 packages built from a single PKGPATH.
     */
    #[test]
    fn multi_input() -> anyhow::Result<()> {
        let mut scanfile = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        scanfile.push("tests/data/scanindex/pbulk-index.txt");
        let file = File::open(&scanfile)?;
        let reader = BufReader::new(file);
        let index: Vec<_> =
            ScanIndex::from_reader(reader).collect::<Result<_, _>>()?;
        assert_eq!(index.len(), 40);
        let all_depends = index[0]
            .all_depends
            .as_ref()
            .context("missing all_depends")?;
        assert_eq!(all_depends.iter().count(), 11);
        let scan_depends = index[0]
            .scan_depends
            .as_ref()
            .context("missing scan_depends")?;
        assert_eq!(scan_depends.iter().count(), 155);
        let multi_version = index[0]
            .multi_version
            .as_ref()
            .context("missing multi_version")?;
        assert_eq!(multi_version.len(), 2);
        Ok(())
    }

    #[test]
    fn duplicate_pkgname() -> Result<(), io::Error> {
        // We do not check for unique PKGNAME, two entries will be created.
        let input = "PKGNAME=foo\nPKGNAME=foo\n";
        let index: Vec<_> = ScanIndex::from_reader(input.as_bytes())
            .collect::<Result<_, _>>()?;
        assert_eq!(index.len(), 2);
        Ok(())
    }

    #[test]
    fn no_input() -> Result<(), io::Error> {
        // No valid input should just result in an empty index.
        let input = "";
        let index: Vec<_> = ScanIndex::from_reader(input.as_bytes())
            .collect::<Result<_, _>>()?;
        assert_eq!(index.len(), 0);
        Ok(())
    }

    #[test]
    fn empty_input() -> Result<(), io::Error> {
        // A single PKGNAME, even if invalid, will create an index
        // entry but it should be empty.
        let input = "PKGNAME=";
        let index: Vec<_> = ScanIndex::from_reader(input.as_bytes())
            .collect::<Result<_, _>>()?;
        assert_eq!(index.len(), 1);
        assert_eq!(index[0].pkgname.pkgname(), "");
        Ok(())
    }

    #[test]
    fn input_error() {
        // If we see any valid field but no PKGNAME (required) then we should
        // generate an error.
        let input = "ALL_DEPENDS=";
        let result: Result<Vec<_>, _> =
            ScanIndex::from_reader(input.as_bytes()).collect();
        assert!(result.is_err());

        // ALL_DEPENDS stores the raw string; invalid entries are caught
        // when iterating, not during parsing.
        let input = "PKGNAME=\nALL_DEPENDS=hello\n";
        let index: Vec<_> = ScanIndex::from_reader(input.as_bytes())
            .collect::<Result<_, io::Error>>()
            .expect("lazy parse should succeed");
        let deps = index[0]
            .all_depends
            .as_ref()
            .expect("should have all_depends");
        assert!(deps.iter().any(|r| r.is_err()));
        assert!(deps.depends().any(|r| r.is_err()));
    }

    #[test]
    fn from_str() -> Result<(), KvError> {
        use std::str::FromStr;

        let input = "PKGNAME=test-1.0\nMAINTAINER=test@example.com\n";
        let index = ScanIndex::from_str(input)?;
        assert_eq!(index.pkgname.pkgname(), "test-1.0");
        assert_eq!(index.maintainer.as_deref(), Some("test@example.com"));
        Ok(())
    }

    #[test]
    fn error_unknown_variable() -> Result<(), KvError> {
        use std::str::FromStr;

        let input = "PKGNAME=test-1.0\nUNKNOWN=value\n";
        let err = ScanIndex::from_str(input)
            .err()
            .ok_or(KvError::Incomplete("expected error".to_string()))?;
        match err {
            KvError::UnknownVariable { variable, span } => {
                assert_eq!(variable, "UNKNOWN");
                assert_eq!(span.offset, 17);
                assert_eq!(span.len, 7);
                assert_eq!(
                    &input[span.offset..span.offset + span.len],
                    "UNKNOWN"
                );
            }
            _ => panic!("expected UnknownVariable error, got {err:?}"),
        }
        Ok(())
    }

    #[test]
    fn error_invalid_depend() -> Result<(), KvError> {
        use std::str::FromStr;

        // "invalid" is not a valid Depend (missing ":" separator).
        // With AllDepends the raw string is stored at parse time; errors
        // surface when iterating.
        let input = "PKGNAME=test-1.0\nALL_DEPENDS=invalid\n";
        let index = ScanIndex::from_str(input)?;
        let deps = index
            .all_depends
            .as_ref()
            .ok_or(KvError::Incomplete("all_depends".to_string()))?;
        assert_eq!(deps.as_str(), "invalid");
        let results: Vec<_> = deps.iter().collect();
        assert_eq!(results.len(), 1);
        assert!(results[0].is_err());
        let results: Vec<_> = deps.depends().collect();
        assert!(results[0].is_err());
        Ok(())
    }

    #[test]
    fn error_invalid_pkgpath() -> Result<(), KvError> {
        use std::str::FromStr;

        // "bad" is not a valid PkgPath (missing category/package structure)
        let input = "PKGNAME=test-1.0\nPKG_LOCATION=bad\n";
        let err = ScanIndex::from_str(input)
            .err()
            .ok_or(KvError::Incomplete("expected error".to_string()))?;
        match err {
            KvError::Parse { message, span } => {
                assert!(message.contains("Invalid path"));
                assert_eq!(span.offset, 30);
                assert_eq!(span.len, 3);
                assert_eq!(&input[span.offset..span.offset + span.len], "bad");
            }
            _ => panic!("expected Parse error, got {err:?}"),
        }
        Ok(())
    }

    #[test]
    fn error_missing_pkgname() -> Result<(), KvError> {
        use std::str::FromStr;

        let input = "MAINTAINER=test@example.com\n";
        let err = ScanIndex::from_str(input)
            .err()
            .ok_or(KvError::Incomplete("expected error".to_string()))?;
        match err {
            KvError::Incomplete(field) => {
                assert_eq!(field, "PKGNAME");
            }
            _ => panic!("expected Incomplete error, got {err:?}"),
        }
        Ok(())
    }

    #[test]
    fn error_bad_line_format() -> Result<(), KvError> {
        use std::str::FromStr;

        let input = "PKGNAME=test-1.0\nbadline\n";
        let err = ScanIndex::from_str(input)
            .err()
            .ok_or(KvError::Incomplete("expected error".to_string()))?;
        match err {
            KvError::ParseLine(span) => {
                assert_eq!(span.offset, 17);
                assert_eq!(span.len, 7);
                assert_eq!(
                    &input[span.offset..span.offset + span.len],
                    "badline"
                );
            }
            _ => panic!("expected ParseLine error, got {err:?}"),
        }
        Ok(())
    }

    #[test]
    fn error_span_accessor() -> Result<(), KvError> {
        use std::str::FromStr;

        let input = "PKGNAME=test-1.0\nUNKNOWN=value\n";
        let err = ScanIndex::from_str(input)
            .err()
            .ok_or(KvError::Incomplete("expected error".to_string()))?;
        let span = err
            .span()
            .ok_or(KvError::Incomplete("expected span".to_string()))?;
        assert_eq!(&input[span.offset..span.offset + span.len], "UNKNOWN");
        Ok(())
    }

    #[test]
    fn pscan_roundtrip() -> anyhow::Result<()> {
        let mut scanfile = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        scanfile.push("tests/data/scanindex/pbulk-index.txt");
        let file = File::open(&scanfile)?;
        let reader = BufReader::new(file);
        let original: Vec<_> =
            ScanIndex::from_reader(reader).collect::<Result<_, _>>()?;

        let output: String =
            original.iter().map(|s| s.pscan().to_string()).collect();
        let reparsed: Vec<_> = ScanIndex::from_reader(output.as_bytes())
            .collect::<Result<_, _>>()?;

        assert_eq!(original, reparsed);
        Ok(())
    }

    #[test]
    fn resolved_depends_none() -> Result<(), KvError> {
        use std::str::FromStr;

        let input = "PKGNAME=test-1.0\n";
        let index = ScanIndex::from_str(input)?;
        assert!(index.resolved_depends.is_none());
        assert!(index.depends().is_empty());
        assert!(!index.presolve().to_string().contains("\nDEPENDS="));
        assert!(!index.pscan().to_string().contains("\nDEPENDS="));
        Ok(())
    }

    #[test]
    fn resolved_depends_some() -> Result<(), KvError> {
        use std::str::FromStr;

        let input = "PKGNAME=test-1.0\nDEPENDS=foo-1.0 bar-2.0\n";
        let index = ScanIndex::from_str(input)?;
        let deps = index.depends();
        assert_eq!(deps.len(), 2);
        assert_eq!(deps[0].pkgname(), "foo-1.0");
        assert_eq!(deps[1].pkgname(), "bar-2.0");
        assert!(
            index
                .presolve()
                .to_string()
                .contains("DEPENDS=foo-1.0 bar-2.0")
        );
        // pscan omits DEPENDS even when resolved_depends is populated.
        assert!(!index.pscan().to_string().contains("\nDEPENDS="));
        Ok(())
    }

    #[test]
    fn resolved_depends_roundtrip() -> Result<(), KvError> {
        use std::str::FromStr;

        let input = "PKGNAME=test-1.0\nDEPENDS=foo-1.0 bar-2.0\n";
        let index = ScanIndex::from_str(input)?;
        let output = index.presolve().to_string();
        let reparsed = ScanIndex::from_str(&output)?;
        assert_eq!(index.resolved_depends, reparsed.resolved_depends);
        Ok(())
    }

    #[test]
    fn from_reader_accumulates_warnings() -> Result<(), io::Error> {
        /*
         * An invalid PBULK_WEIGHT is dropped to None without failing the
         * record; the bad value accumulates on the iterator instead.
         */
        let input = "PKGNAME=foo-1.0\nPBULK_WEIGHT=bad\n";
        let mut iter = ScanIndex::from_reader(input.as_bytes());
        let index: Vec<_> = iter.by_ref().collect::<Result<_, _>>()?;
        assert_eq!(index.len(), 1);
        assert_eq!(index[0].pbulk_weight, None);

        let warnings = iter.into_warnings();
        assert_eq!(warnings.len(), 1);
        assert_eq!(warnings[0].variable, "PBULK_WEIGHT");
        assert_eq!(warnings[0].value, "bad");
        Ok(())
    }
}
