//! High-performance IP address extraction and tagging engine.
//!
//! `ip-extract` provides a blazingly fast, configurable extractor for finding IPv4 and IPv6
//! addresses in unstructured text. It achieves maximum throughput through:
//!
//! - **Compile-time DFA**: IP patterns are converted to dense Forward DFAs during build,
//!   eliminating runtime regex compilation and heap allocation.
//! - **Zero-overhead scanning**: The DFA scans at O(n) with no backtracking; validation
//!   is performed only on candidates.
//! - **Strict validation**: Deep checks eliminate false positives (e.g., `1.2.3.4.5` is rejected).
//!
//! ## Quick Start
//!
//! By default, **all IP addresses are extracted**:
//!
//! ```no_run
//! use ip_extract::ExtractorBuilder;
//!
//! # fn main() -> anyhow::Result<()> {
//! // Extract all IPs (default: includes private, loopback, broadcast)
//! let extractor = ExtractorBuilder::new().build()?;
//!
//! let input = b"Connect from 192.168.1.1 to 2001:db8::1";
//! for range in extractor.find_iter(input) {
//!     let ip = std::str::from_utf8(&input[range])?;
//!     println!("Found: {}", ip);
//! }
//! # Ok(())
//! # }
//! ```
//!
//! ## Tagging and Output
//!
//! For more structured output (e.g., JSON), use the `Tagged` and `Tag` types:
//!
//! ```no_run
//! use ip_extract::{ExtractorBuilder, Tagged, Tag};
//!
//! # fn main() -> anyhow::Result<()> {
//! let extractor = ExtractorBuilder::new().build()?;
//! let data = b"Server at 8.8.8.8";
//! let mut tagged = Tagged::new(data);
//!
//! for range in extractor.find_iter(data) {
//!     let ip = std::str::from_utf8(&data[range.clone()])?;
//!     let tag = Tag::new(ip, ip).with_range(range);
//!     tagged = tagged.tag(tag);
//! }
//! # Ok(())
//! # }
//! ```
//!
//! ## Configuration
//!
//! Use `ExtractorBuilder` to filter specific IP categories:
//!
//! ```no_run
//! use ip_extract::ExtractorBuilder;
//!
//! # fn main() -> anyhow::Result<()> {
//! // Extract only publicly routable IPs
//! let extractor = ExtractorBuilder::new()
//!     .only_public()
//!     .build()?;
//!
//! // Or use granular control
//! let extractor = ExtractorBuilder::new()
//!     .ipv4(true)            // Extract IPv4 (default: true)
//!     .ipv6(false)           // Skip IPv6
//!     .ignore_private()      // Skip RFC 1918 ranges
//!     .ignore_loopback()     // Skip loopback (127.0.0.1, ::1)
//!     .build()?;
//! # Ok(())
//! # }
//! ```
//!
//! ## Performance
//!
//! Typical throughput on modern hardware:
//! - Dense IPs (mostly IP addresses): **160+ MiB/s**
//! - Sparse logs (IPs mixed with text): **360+ MiB/s**
//! - No IPs (pure scanning): **620+ MiB/s**
//!
//! See `benches/ip_benchmark.rs` for details.

use std::io;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::ops::Range;
use std::sync::OnceLock;

use regex_automata::dfa::dense::DFA;
use regex_automata::dfa::Automaton;
use regex_automata::Input;

mod tag;
pub use tag::{Tag, Tagged, TextData};

/// Whether a validated IP match is IPv4 or IPv6.
///
/// Known at zero cost from the DFA pattern ID — no parsing required.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IpKind {
    V4,
    V6,
}

/// A validated IP address match within a haystack.
///
/// Provides zero-copy access to the matched bytes and their position within
/// the original haystack, plus the IP version. Parsing to [`IpAddr`] is
/// available via [`ip()`][IpMatch::ip] but not cached — callers who look up
/// the same IP repeatedly should cache at a higher level.
#[derive(Debug, Clone)]
pub struct IpMatch<'a> {
    bytes: &'a [u8],
    range: Range<usize>,
    kind: IpKind,
}

impl<'a> IpMatch<'a> {
    /// The matched IP address as a byte slice.
    ///
    /// Zero-copy: this is a slice directly into the haystack.
    #[inline]
    pub fn as_bytes(&self) -> &'a [u8] {
        self.bytes
    }

    /// The clean IP address as a string, with any defang brackets removed.
    ///
    /// For normal (fanged) input this is a zero-copy borrow (`Cow::Borrowed`).
    /// For defanged input (e.g. `"192.168.1[.]50"`) this allocates and strips
    /// brackets, returning `Cow::Owned("192.168.1.50")`.
    ///
    /// This is the right default for MMDB lookups, deduplication, output, and
    /// parsing. For the raw matched text (which may contain brackets), use
    /// [`as_matched_str`][Self::as_matched_str].
    pub fn as_str(&self) -> std::borrow::Cow<'a, str> {
        if memchr::memchr(b'[', self.bytes).is_none() {
            // SAFETY: IP characters and brackets are all ASCII.
            std::borrow::Cow::Borrowed(unsafe { std::str::from_utf8_unchecked(self.bytes) })
        } else {
            let cleaned = strip_brackets(self.bytes);
            // SAFETY: strip_brackets retains only IP characters (ASCII).
            std::borrow::Cow::Owned(unsafe { String::from_utf8_unchecked(cleaned) })
        }
    }

    /// The raw matched text as a string slice.
    ///
    /// Returns the exact bytes matched in the haystack — for defanged input,
    /// this may include bracket characters (e.g. `"192.168.1[.]50"`). Use
    /// [`as_str`][Self::as_str] when you need the canonical IP form.
    ///
    /// Zero-copy: this is a slice directly into the haystack. Safe without
    /// UTF-8 validation because all matched characters (digits, hex, `.`, `:`,
    /// `[`, `]`) are ASCII.
    #[inline]
    pub fn as_matched_str(&self) -> &'a str {
        // SAFETY: IP characters and brackets are all ASCII.
        unsafe { std::str::from_utf8_unchecked(self.bytes) }
    }

    /// The byte range of this match within the original haystack.
    #[inline]
    pub fn range(&self) -> Range<usize> {
        self.range.clone()
    }

    /// Whether this match is IPv4 or IPv6.
    #[inline]
    pub fn kind(&self) -> IpKind {
        self.kind
    }

    /// Parse the matched bytes into an [`IpAddr`].
    ///
    /// Automatically strips defang brackets before parsing — safe to call on
    /// both normal and defanged matches. Not cached; callers processing the
    /// same IP repeatedly should cache at a higher level.
    ///
    /// # Panics
    ///
    /// Panics if the validated bytes cannot be parsed as an IP address.
    /// This should not happen in practice because matches are validated by the DFA.
    pub fn ip(&self) -> IpAddr {
        let s = self.as_str();
        match self.kind {
            IpKind::V4 => IpAddr::V4(s.parse::<Ipv4Addr>().expect("validated by DFA")),
            IpKind::V6 => IpAddr::V6(s.parse::<Ipv6Addr>().expect("validated by DFA")),
        }
    }
}

// Alignment wrapper: guarantees u32 alignment for DFA deserialization.
// DFA::from_bytes() requires the byte slice to be u32-aligned; include_bytes!() only
// guarantees byte alignment. Wrapping in repr(C, align(4)) satisfies this at compile time,
// with zero runtime cost: no allocation, no copy, no Box::leak.
#[repr(C, align(4))]
struct AlignedDfa<T: ?Sized>(T);

static IPV4_DFA_BYTES: &AlignedDfa<[u8]> =
    &AlignedDfa(*include_bytes!(concat!(env!("OUT_DIR"), "/ipv4.dfa")));
static IPV6_DFA_BYTES: &AlignedDfa<[u8]> =
    &AlignedDfa(*include_bytes!(concat!(env!("OUT_DIR"), "/ipv6.dfa")));
static BOTH_DFA_BYTES: &AlignedDfa<[u8]> =
    &AlignedDfa(*include_bytes!(concat!(env!("OUT_DIR"), "/both.dfa")));

static DFA_IPV4: OnceLock<DFA<&'static [u32]>> = OnceLock::new();
static DFA_IPV6: OnceLock<DFA<&'static [u32]>> = OnceLock::new();
static DFA_BOTH: OnceLock<DFA<&'static [u32]>> = OnceLock::new();

fn load_dfa(aligned: &'static AlignedDfa<[u8]>) -> DFA<&'static [u32]> {
    let (dfa, _) = DFA::from_bytes(&aligned.0).expect("valid dfa from build.rs");
    dfa
}

fn get_ipv4_dfa() -> &'static DFA<&'static [u32]> {
    DFA_IPV4.get_or_init(|| load_dfa(IPV4_DFA_BYTES))
}
fn get_ipv6_dfa() -> &'static DFA<&'static [u32]> {
    DFA_IPV6.get_or_init(|| load_dfa(IPV6_DFA_BYTES))
}
fn get_both_dfa() -> &'static DFA<&'static [u32]> {
    DFA_BOTH.get_or_init(|| load_dfa(BOTH_DFA_BYTES))
}

#[derive(Clone, Debug)]
enum ValidatorType {
    IPv4 {
        include_private: bool,
        include_loopback: bool,
        include_broadcast: bool,
    },
    IPv6 {
        include_private: bool,
        include_loopback: bool,
    },
}

impl ValidatorType {
    #[inline(always)]
    fn validate(&self, bytes: &[u8]) -> bool {
        match *self {
            ValidatorType::IPv4 {
                include_private,
                include_loopback,
                include_broadcast,
            } => validate_ipv4(bytes, include_private, include_loopback, include_broadcast),
            ValidatorType::IPv6 {
                include_private,
                include_loopback,
            } => validate_ipv6(bytes, include_private, include_loopback),
        }
    }

    #[inline(always)]
    fn kind(&self) -> IpKind {
        match self {
            ValidatorType::IPv4 { .. } => IpKind::V4,
            ValidatorType::IPv6 { .. } => IpKind::V6,
        }
    }
}

/// The main IP address extractor.
///
/// An `Extractor` scans byte slices for IPv4 and/or IPv6 addresses, applying configurable
/// filters to include or exclude certain address classes (private, loopback, broadcast).
///
/// Extractors are best created via [`ExtractorBuilder`] and are designed to be reused
/// across many calls to `find_iter` for maximum efficiency.
///
/// # Bytes vs. Strings
///
/// This extractor works directly on byte slices rather than strings. This avoids UTF-8
/// validation overhead and enables zero-copy scanning of very large inputs.
///
/// # Performance
///
/// The extractor uses a compile-time DFA (Deterministic Finite Automaton) for O(n)
/// scanning with minimal overhead. See the crate-level documentation for throughput benchmarks.
pub struct Extractor {
    dfa: &'static DFA<&'static [u32]>,
    validators: [ValidatorType; 2],
}

impl Extractor {
    /// Find all IP addresses in a byte slice.
    ///
    /// Returns an iterator of byte ranges `[start, end)` pointing to each IP
    /// address found. Ranges are guaranteed to be valid indices into `haystack`.
    ///
    /// For richer match information (IP version, direct string access), use
    /// [`match_iter`][Extractor::match_iter] instead.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use ip_extract::ExtractorBuilder;
    ///
    /// # fn main() -> anyhow::Result<()> {
    /// let extractor = ExtractorBuilder::new().build()?;
    /// let data = b"Connecting from 192.168.1.1";
    ///
    /// for range in extractor.find_iter(data) {
    ///     let ip = std::str::from_utf8(&data[range])?;
    ///     println!("Found: {ip}");
    /// }
    /// # Ok(())
    /// # }
    /// ```
    #[inline]
    pub fn find_iter<'a>(&'a self, haystack: &'a [u8]) -> impl Iterator<Item = Range<usize>> + 'a {
        self.match_iter(haystack).map(|m| m.range())
    }

    /// Find all IP addresses in a byte slice, yielding rich [`IpMatch`] values.
    ///
    /// Like [`find_iter`][Extractor::find_iter], but each match carries the
    /// matched bytes, their position in the haystack, and the IP version —
    /// eliminating the need to re-parse or guess the version at the call site.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use ip_extract::ExtractorBuilder;
    ///
    /// # fn main() -> anyhow::Result<()> {
    /// let extractor = ExtractorBuilder::new().build()?;
    /// let data = b"Log: 192.168.1.1 sent request to 2001:db8::1";
    ///
    /// for m in extractor.match_iter(data) {
    ///     println!("{} ({:?})", m.as_matched_str(), m.kind());
    /// }
    /// # Ok(())
    /// # }
    /// ```
    #[inline]
    pub fn match_iter<'a>(&'a self, haystack: &'a [u8]) -> impl Iterator<Item = IpMatch<'a>> + 'a {
        let mut input = Input::new(haystack);

        std::iter::from_fn(move || loop {
            let Ok(Some(m)) = self.dfa.try_search_fwd(&input) else {
                return None;
            };

            let end = m.offset();
            let pid = m.pattern().as_usize();
            let validator = &self.validators[pid];

            input.set_start(end);

            // Bracket-aware boundary scan (defang always-on: [.] and [:] are valid IP chars).
            let floor = end.saturating_sub(55); // wider for bracket notation:
                                                // max defanged IPv6 ≈ 53 chars
            let raw_start = (floor..end)
                .rev()
                .find(|&i| i == 0 || !is_ip_or_bracket_char(haystack[i - 1]))
                .unwrap_or(floor);

            // A lone `[` at the start of the candidate is a surrounding bracket (e.g. "[3.3.3.3]"),
            // not a defang bracket. Defang brackets always surround a separator character:
            // `[.]`, `[:]`, or `[::]`. Skip a leading `[` that is followed by a digit or hex
            // character (not `.` or `:`), since that pattern is never valid defang notation.
            let start = if raw_start < end
                && haystack[raw_start] == b'['
                && raw_start + 1 < end
                && haystack[raw_start + 1] != b'.'
                && haystack[raw_start + 1] != b':'
            {
                raw_start + 1
            } else {
                raw_start
            };

            let valid_right_boundary = match end.cmp(&haystack.len()) {
                std::cmp::Ordering::Less => {
                    let next = haystack[end];
                    match validator {
                        ValidatorType::IPv4 { .. } => {
                            !(next.is_ascii_digit()
                                || next == b'.'
                                    && end + 1 < haystack.len()
                                    && haystack[end + 1].is_ascii_digit())
                        }
                        ValidatorType::IPv6 { .. } => !is_ip_or_bracket_char(next),
                    }
                }
                _ => true,
            };

            if !valid_right_boundary {
                continue;
            }

            let candidate = &haystack[start..end];

            // Strip brackets before validation (handles both fanged and defanged input).
            // On normal (fanged) input, memchr scans ~7-15 bytes per match and finds
            // nothing — falling straight to the else branch with no allocation. The
            // strip_brackets path only runs when brackets are actually present.
            if memchr::memchr(b'[', candidate).is_some() {
                let cleaned = strip_brackets(candidate);
                if validator.validate(&cleaned) {
                    return Some(IpMatch {
                        bytes: candidate,
                        range: start..end,
                        kind: validator.kind(),
                    });
                }
            } else if validator.validate(candidate) {
                return Some(IpMatch {
                    bytes: candidate,
                    range: start..end,
                    kind: validator.kind(),
                });
            }
        })
    }

    /// Scan `haystack` for IP addresses, writing non-IP text to `wtr` and
    /// calling `replacer` for each match.
    ///
    /// This is the efficient single-pass decoration primitive: the caller
    /// never needs to track byte offsets or manage gap writes. The replacer
    /// writes the substitution directly to `wtr` — no intermediate allocation.
    ///
    /// Returns the number of IP addresses found.
    ///
    /// # Errors
    ///
    /// Returns the first `io::Error` from either a gap write or the replacer.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use ip_extract::ExtractorBuilder;
    /// use std::io::Write;
    ///
    /// # fn main() -> anyhow::Result<()> {
    /// let extractor = ExtractorBuilder::new().build()?;
    /// let data = b"Server 192.168.1.1 is up";
    /// let mut out = Vec::new();
    ///
    /// let count = extractor.replace_iter(data, &mut out, |m, w| {
    ///     write!(w, "[{}]", m.as_matched_str())
    /// })?;
    ///
    /// assert_eq!(count, 1);
    /// assert_eq!(out, b"Server [192.168.1.1] is up");
    /// # Ok(())
    /// # }
    /// ```
    pub fn replace_iter<W, F>(
        &self,
        haystack: &[u8],
        wtr: &mut W,
        mut replacer: F,
    ) -> io::Result<usize>
    where
        W: io::Write,
        F: FnMut(&IpMatch, &mut W) -> io::Result<()>,
    {
        let mut last = 0;
        let mut count = 0;

        for m in self.match_iter(haystack) {
            let range = m.range();
            wtr.write_all(&haystack[last..range.start])?;
            replacer(&m, wtr)?;
            last = range.end;
            count += 1;
        }

        wtr.write_all(&haystack[last..])?;
        Ok(count)
    }
}

/// Boundary check for IP characters including defang brackets `[` and `]`.
#[inline(always)]
fn is_ip_or_bracket_char(b: u8) -> bool {
    matches!(b, b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F' | b'.' | b':' | b'[' | b']')
}

/// Strip `[` and `]` from a byte slice, returning a cleaned copy.
///
/// Used by the defang DFA approach to normalize `192[.]168[.]1[.]1` → `192.168.1.1`
/// before feeding to the standard validator.
fn strip_brackets(bytes: &[u8]) -> Vec<u8> {
    let mut out = Vec::with_capacity(bytes.len());
    for &b in bytes {
        if b != b'[' && b != b']' {
            out.push(b);
        }
    }
    out
}

/// A builder for configuring IP extraction behavior.
///
/// Use `ExtractorBuilder` to specify which types of IP addresses should be extracted.
/// By default, it extracts both IPv4 and IPv6 but excludes private, loopback, and
/// broadcast addresses.
///
/// # Example
///
/// ```no_run
/// use ip_extract::ExtractorBuilder;
///
/// # fn main() -> anyhow::Result<()> {
/// let extractor = ExtractorBuilder::new()
///     .ipv4(true)
///     .ipv6(false)  // Only IPv4
///     .private_ips(true)  // Include private ranges
///     .build()?;
/// # Ok(())
/// # }
/// ```
pub struct ExtractorBuilder {
    include_ipv4: bool,
    include_ipv6: bool,
    include_private: bool,
    include_loopback: bool,
    include_broadcast: bool,
}

impl Default for ExtractorBuilder {
    fn default() -> Self {
        Self::new()
    }
}

impl ExtractorBuilder {
    /// Create a new builder with default settings.
    ///
    /// By default, **all IP addresses are extracted** (principle of least surprise).
    /// Use `.only_public()` or `.ignore_*()` methods to filter specific categories.
    ///
    /// Defaults:
    /// - IPv4: enabled
    /// - IPv6: enabled
    /// - Private IPs: **enabled** (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7)
    /// - Loopback IPs: **enabled** (127.0.0.0/8, ::1)
    /// - Broadcast IPs: **enabled** (255.255.255.255, link-local)
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use ip_extract::ExtractorBuilder;
    ///
    /// # fn main() -> anyhow::Result<()> {
    /// // Extract all IPs (default)
    /// let extractor = ExtractorBuilder::new().build()?;
    ///
    /// // Extract only public IPs
    /// let extractor = ExtractorBuilder::new().only_public().build()?;
    ///
    /// // Granular control
    /// let extractor = ExtractorBuilder::new()
    ///     .ignore_private()
    ///     .ignore_loopback()
    ///     .build()?;
    /// # Ok(())
    /// # }
    /// ```
    #[must_use]
    pub fn new() -> Self {
        Self {
            include_ipv4: true,
            include_ipv6: true,
            include_private: true,
            include_loopback: true,
            include_broadcast: true,
        }
    }
    /// Enable or disable IPv4 address extraction.
    ///
    /// Default: `true`
    pub fn ipv4(&mut self, include: bool) -> &mut Self {
        self.include_ipv4 = include;
        self
    }

    /// Enable or disable IPv6 address extraction.
    ///
    /// Default: `true`
    pub fn ipv6(&mut self, include: bool) -> &mut Self {
        self.include_ipv6 = include;
        self
    }

    /// Include private IP addresses (RFC 1918 for IPv4, ULA for IPv6).
    ///
    /// Private ranges include:
    /// - IPv4: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
    /// - IPv6: fc00::/7 (ULA), fe80::/10 (link-local)
    ///
    /// Default: `true`
    pub fn private_ips(&mut self, include: bool) -> &mut Self {
        self.include_private = include;
        self
    }

    /// Include loopback addresses.
    ///
    /// Loopback ranges:
    /// - IPv4: 127.0.0.0/8
    /// - IPv6: ::1
    ///
    /// Default: `true`
    pub fn loopback_ips(&mut self, include: bool) -> &mut Self {
        self.include_loopback = include;
        self
    }

    /// Include broadcast addresses.
    ///
    /// Covers:
    /// - IPv4: 255.255.255.255 and link-local (169.254.0.0/16)
    /// - IPv6: link-local and other special ranges
    ///
    /// Default: `true`
    pub fn broadcast_ips(&mut self, include: bool) -> &mut Self {
        self.include_broadcast = include;
        self
    }

    /// Ignore private IP addresses (convenience for `.private_ips(false)`).
    ///
    /// Excludes:
    /// - IPv4: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
    /// - IPv6: fc00::/7 (ULA), fe80::/10 (link-local)
    pub fn ignore_private(&mut self) -> &mut Self {
        self.include_private = false;
        self
    }

    /// Ignore loopback addresses (convenience for `.loopback_ips(false)`).
    ///
    /// Excludes:
    /// - IPv4: 127.0.0.0/8
    /// - IPv6: ::1
    pub fn ignore_loopback(&mut self) -> &mut Self {
        self.include_loopback = false;
        self
    }

    /// Ignore broadcast addresses (convenience for `.broadcast_ips(false)`).
    ///
    /// Excludes:
    /// - IPv4: 255.255.255.255 and link-local (169.254.0.0/16)
    /// - IPv6: link-local and other special ranges
    pub fn ignore_broadcast(&mut self) -> &mut Self {
        self.include_broadcast = false;
        self
    }

    /// Extract only publicly routable IP addresses.
    ///
    /// This is a convenience method equivalent to:
    /// ```
    /// # use ip_extract::ExtractorBuilder;
    /// # let mut builder = ExtractorBuilder::new();
    /// builder
    ///     .ignore_private()
    ///     .ignore_loopback()
    ///     .ignore_broadcast();
    /// ```
    ///
    /// Excludes:
    /// - Private: RFC 1918 (IPv4), ULA (IPv6)
    /// - Loopback: 127.0.0.0/8, ::1
    /// - Broadcast: 255.255.255.255, link-local ranges
    ///
    /// # Example
    ///
    /// ```no_run
    /// use ip_extract::ExtractorBuilder;
    ///
    /// # fn main() -> anyhow::Result<()> {
    /// let extractor = ExtractorBuilder::new()
    ///     .only_public()
    ///     .build()?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn only_public(&mut self) -> &mut Self {
        self.include_private = false;
        self.include_loopback = false;
        self.include_broadcast = false;
        self
    }

    /// Build and return an `Extractor` with the configured settings.
    ///
    /// # Errors
    ///
    /// Returns an error if no IP version (IPv4 or IPv6) is enabled. At least one
    /// must be selected.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use ip_extract::ExtractorBuilder;
    ///
    /// # fn main() -> anyhow::Result<()> {
    /// let extractor = ExtractorBuilder::new()
    ///     .ipv4(true)
    ///     .ipv6(true)
    ///     .build()?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn build(&self) -> anyhow::Result<Extractor> {
        let ipv4 = ValidatorType::IPv4 {
            include_private: self.include_private,
            include_loopback: self.include_loopback,
            include_broadcast: self.include_broadcast,
        };
        let ipv6 = ValidatorType::IPv6 {
            include_private: self.include_private,
            include_loopback: self.include_loopback,
        };
        // Pattern IDs assigned by build_many order: 0 = IPv4, 1 = IPv6.
        // All DFAs are defang-aware (match both normal and bracket notation).
        // validators[pid] must stay in sync with build.rs build_many order.
        let (dfa, validators) = match (self.include_ipv4, self.include_ipv6) {
            (true, true) => (get_both_dfa(), [ipv4, ipv6]),
            (true, false) => (get_ipv4_dfa(), [ipv4, ipv6]),
            // ipv6_only DFA has a single pattern: pid=0 maps to IPv6
            (false, true) => (get_ipv6_dfa(), [ipv6, ipv4]),
            _ => anyhow::bail!("No IP address patterns selected"),
        };
        Ok(Extractor { dfa, validators })
    }
}

/// Validate an IPv4 address from a byte slice, applying filters.
///
/// This function uses `parse_ipv4_bytes` for strict validation and then checks
/// against the provided inclusion filters.
///
/// # Arguments
///
/// * `bytes` - Candidate byte slice to validate.
/// * `include_private` - Whether to include RFC 1918 addresses.
/// * `include_loopback` - Whether to include 127.0.0.0/8 addresses.
/// * `include_broadcast` - Whether to include broadcast and link-local addresses.
#[inline]
fn validate_ipv4(
    bytes: &[u8],
    include_private: bool,
    include_loopback: bool,
    include_broadcast: bool,
) -> bool {
    let Some(ipv4) = parse_ipv4_bytes(bytes) else {
        return false;
    };

    if !include_private && ipv4.is_private() {
        return false;
    }
    if !include_loopback && ipv4.is_loopback() {
        return false;
    }
    if !include_broadcast && (ipv4.is_broadcast() || ipv4.is_link_local()) {
        return false;
    }
    true
}

/// Extract all IPv4 and IPv6 addresses from input, returning them as strings.
///
/// This is a convenience function that uses default settings (all IP types included).
/// For more control, use `ExtractorBuilder` and `Extractor::find_iter()`.
///
/// # Errors
///
/// Returns an error if the builder fails to initialize (e.g., no IP types selected).
///
/// # Example
///
/// ```no_run
/// use ip_extract::extract;
///
/// # fn main() -> anyhow::Result<()> {
/// let ips = extract(b"Server at 192.168.1.1 and 2001:db8::1")?;
/// assert_eq!(ips, vec!["192.168.1.1", "2001:db8::1"]);
/// # Ok(())
/// # }
/// ```
pub fn extract(haystack: &[u8]) -> anyhow::Result<Vec<String>> {
    let extractor = ExtractorBuilder::new().build()?;
    Ok(extractor
        .find_iter(haystack)
        .map(|range| String::from_utf8_lossy(&haystack[range]).to_string())
        .collect())
}

/// Extract unique IPv4 and IPv6 addresses from input, returning them as strings.
///
/// Maintains order of first observation (not lexicographic order).
/// This is a convenience function that uses default settings (all IP types included).
/// For more control, use `ExtractorBuilder` and `Extractor::find_iter()`.
///
/// # Errors
///
/// Returns an error if the builder fails to initialize (e.g., no IP types selected).
///
/// # Example
///
/// ```no_run
/// use ip_extract::extract_unique;
///
/// # fn main() -> anyhow::Result<()> {
/// let ips = extract_unique(b"Server at 192.168.1.1, another at 192.168.1.1")?;
/// assert_eq!(ips, vec!["192.168.1.1"]);
/// # Ok(())
/// # }
/// ```
pub fn extract_unique(haystack: &[u8]) -> anyhow::Result<Vec<String>> {
    use std::collections::HashSet;

    let extractor = ExtractorBuilder::new().build()?;
    let mut seen = HashSet::new();
    let mut result = Vec::new();

    for range in extractor.find_iter(haystack) {
        let ip_str = String::from_utf8_lossy(&haystack[range]).to_string();
        if seen.insert(ip_str.clone()) {
            result.push(ip_str);
        }
    }

    Ok(result)
}

/// Extract all IPv4 and IPv6 addresses from input, returning them as parsed `IpAddr` objects.
///
/// This is a convenience function that uses default settings (all IP types included).
/// For more control, use `ExtractorBuilder` and `Extractor::find_iter()`.
///
/// # Errors
///
/// Returns an error if the builder fails to initialize (e.g., no IP types selected),
/// or if an extracted address cannot be parsed (should not happen in practice).
///
/// # Example
///
/// ```no_run
/// use ip_extract::extract_parsed;
///
/// # fn main() -> anyhow::Result<()> {
/// let ips = extract_parsed(b"Server at 192.168.1.1 and 2001:db8::1")?;
/// assert_eq!(ips.len(), 2);
/// assert!(ips[0].is_ipv4());
/// assert!(ips[1].is_ipv6());
/// # Ok(())
/// # }
/// ```
pub fn extract_parsed(haystack: &[u8]) -> anyhow::Result<Vec<IpAddr>> {
    let extractor = ExtractorBuilder::new().build()?;
    extractor
        .find_iter(haystack)
        .map(|range| {
            let s = std::str::from_utf8(&haystack[range])
                .map_err(|e| anyhow::anyhow!("Invalid UTF-8 in IP: {e}"))?;
            s.parse::<IpAddr>()
                .map_err(|e| anyhow::anyhow!("Failed to parse IP '{s}': {e}"))
        })
        .collect()
}

/// Extract unique IPv4 and IPv6 addresses from input, returning them as parsed `IpAddr` objects.
///
/// Maintains order of first observation (not lexicographic order).
/// This is a convenience function that uses default settings (all IP types included).
/// For more control, use `ExtractorBuilder` and `Extractor::find_iter()`.
///
/// # Errors
///
/// Returns an error if the builder fails to initialize (e.g., no IP types selected),
/// or if an extracted address cannot be parsed (should not happen in practice).
///
/// # Example
///
/// ```no_run
/// use ip_extract::extract_unique_parsed;
///
/// # fn main() -> anyhow::Result<()> {
/// let ips = extract_unique_parsed(b"Server at 192.168.1.1, another at 192.168.1.1")?;
/// assert_eq!(ips.len(), 1);
/// assert!(ips[0].is_ipv4());
/// # Ok(())
/// # }
/// ```
pub fn extract_unique_parsed(haystack: &[u8]) -> anyhow::Result<Vec<IpAddr>> {
    use std::collections::HashSet;

    let extractor = ExtractorBuilder::new().build()?;
    let mut seen = HashSet::new();
    let mut result = Vec::new();

    for range in extractor.find_iter(haystack) {
        let s = std::str::from_utf8(&haystack[range])
            .map_err(|e| anyhow::anyhow!("Invalid UTF-8 in IP: {e}"))?;
        let addr = s
            .parse::<IpAddr>()
            .map_err(|e| anyhow::anyhow!("Failed to parse IP '{s}': {e}"))?;
        if seen.insert(addr) {
            result.push(addr);
        }
    }

    Ok(result)
}

/// Parse an IPv4 address from a byte slice.
///
/// Performs strict validation of dotted-quad notation (e.g., `192.168.1.1`).
/// Rejects:
/// - Octet values > 255
/// - Leading zeros (e.g., `192.168.001.1`)
/// - Invalid formats
///
/// # Example
///
/// ```
/// use ip_extract::parse_ipv4_bytes;
///
/// assert_eq!(parse_ipv4_bytes(b"192.168.1.1"), Some("192.168.1.1".parse().unwrap()));
/// assert_eq!(parse_ipv4_bytes(b"256.1.1.1"), None);  // Out of range
/// assert_eq!(parse_ipv4_bytes(b"192.168.01.1"), None);  // Leading zero
/// ```
#[must_use]
#[inline]
pub fn parse_ipv4_bytes(bytes: &[u8]) -> Option<Ipv4Addr> {
    if bytes.len() < 7 || bytes.len() > 15 {
        return None;
    }
    let mut octets = [0u8; 4];
    let mut octet_idx = 0;
    let mut current_val = 0u16;
    let mut digits_in_octet = 0;
    for &b in bytes {
        match b {
            b'.' => {
                if digits_in_octet == 0 || octet_idx == 3 {
                    return None;
                }
                #[allow(clippy::cast_possible_truncation)]
                {
                    octets[octet_idx] = current_val as u8;
                }
                octet_idx += 1;
                current_val = 0;
                digits_in_octet = 0;
            }
            b'0'..=b'9' => {
                let digit = u16::from(b - b'0');
                if digits_in_octet > 0 && current_val == 0 {
                    return None;
                }
                current_val = current_val * 10 + digit;
                if current_val > 255 {
                    return None;
                }
                digits_in_octet += 1;
            }
            _ => return None,
        }
    }
    if octet_idx != 3 || digits_in_octet == 0 {
        return None;
    }
    #[allow(clippy::cast_possible_truncation)]
    {
        octets[3] = current_val as u8;
    }
    Some(Ipv4Addr::new(octets[0], octets[1], octets[2], octets[3]))
}

/// Check if an IPv6 address is a Unique Local Address (ULA) per RFC 4193.
/// ULA addresses are in the fc00::/7 range (fc00:: to fdff::).
#[inline]
fn is_unique_local(ip: &Ipv6Addr) -> bool {
    matches!(ip.octets()[0], 0xfc | 0xfd)
}

/// Validate an IPv6 address from a byte slice, applying filters.
///
/// This function performs parsing and category-based filtering. It uses
/// `unsafe` `from_utf8_unchecked` for performance, as the candidates are
/// already filtered by the DFA for IP-like characters.
///
/// # Arguments
///
/// * `bytes` - Candidate byte slice to validate.
/// * `include_private` - Whether to include ULA and link-local addresses.
/// * `include_loopback` - Whether to include the loopback address (`::1`).
#[inline]
fn validate_ipv6(bytes: &[u8], include_private: bool, include_loopback: bool) -> bool {
    if bytes.len() < 2 {
        return false;
    }
    let s = unsafe { std::str::from_utf8_unchecked(bytes) };
    let Ok(ip) = s.parse::<IpAddr>() else {
        return false;
    };

    match ip {
        IpAddr::V6(ipv6) => {
            if !include_private && (ipv6.is_unicast_link_local() || is_unique_local(&ipv6)) {
                return false;
            }
            if !include_loopback && ipv6.is_loopback() {
                return false;
            }
            true
        }
        IpAddr::V4(_) => false,
    }
}

impl std::fmt::Debug for Extractor {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Extractor")
            .field("validators", &self.validators)
            .finish()
    }
}
