mod parse;
mod table;

pub use parse::ParseError;

use std::net::IpAddr;
use std::path::Path;

/// Which RIR issued the record.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Registry {
    Apnic,
    Arin,
    RipeNcc,
    Lacnic,
    Afrinic,
}

/// Allocation status as recorded in the RIR file.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Status {
    Allocated,
    Assigned,
    Available,
    Reserved,
    /// Any status string not recognised by this crate.
    Other,
}

/// A contiguous IP address range (inclusive on both ends).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IpRange {
    pub start: IpAddr,
    pub end: IpAddr,
}

/// A single RIR delegation record returned by [`Database::lookup`].
#[derive(Debug, Clone)]
pub struct Record {
    cc: [u8; 2],
    pub registry: Registry,
    pub status: Status,
    /// Allocation date as `YYYYMMDD` (e.g. `20110811`). `0` means not recorded.
    pub date: u32,
    /// The full allocated range that contains the queried IP.
    pub range: IpRange,
}

impl Record {
    pub(crate) fn new(
        cc: [u8; 2],
        registry: Registry,
        status: Status,
        date: u32,
        range: IpRange,
    ) -> Self {
        Self { cc, registry, status, date, range }
    }

    /// ISO 3166-1 alpha-2 country code (e.g. `"JP"`, `"US"`).
    pub fn cc(&self) -> &str {
        std::str::from_utf8(&self.cc).unwrap_or("??")
    }
}

/// Errors returned by this crate.
#[derive(Debug)]
pub enum Error {
    Io(std::io::Error),
    Parse(ParseError),
}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Error::Io(e) => write!(f, "io error: {e}"),
            Error::Parse(e) => write!(f, "parse error: {e}"),
        }
    }
}

impl std::error::Error for Error {}

impl From<std::io::Error> for Error {
    fn from(e: std::io::Error) -> Self {
        Error::Io(e)
    }
}

impl From<ParseError> for Error {
    fn from(e: ParseError) -> Self {
        Error::Parse(e)
    }
}

/// In-memory IP → RIR record database built from RIR delegated stats files.
///
/// # Usage
///
/// ```no_run
/// use ip_rir::Database;
///
/// let db = Database::init("/var/lib/ip-rir").unwrap();
/// if let Some(rec) = db.lookup("203.0.113.1".parse().unwrap()) {
///     println!("{}", rec.cc()); // "JP"
/// }
/// ```
pub struct Database {
    v4: table::Table4,
    v6: table::Table6,
}

impl Database {
    /// Load all `delegated-*` files found in `data_dir` and build the in-memory table.
    pub fn init(data_dir: impl AsRef<Path>) -> Result<Self, Error> {
        let dir = data_dir.as_ref();
        let mut v4_entries: Vec<table::Entry4> = Vec::new();
        let mut v6_entries: Vec<table::Entry6> = Vec::new();

        for entry in std::fs::read_dir(dir)? {
            let entry = entry?;
            let name = entry.file_name();
            let name = name.to_string_lossy();
            if !name.starts_with("delegated-") {
                continue;
            }
            let content = std::fs::read_to_string(entry.path())?;
            let (mut v4, mut v6) = parse::parse_delegated(&content)?;
            v4_entries.append(&mut v4);
            v6_entries.append(&mut v6);
        }

        Ok(Self {
            v4: table::Table4::build(v4_entries),
            v6: table::Table6::build(v6_entries),
        })
    }

    /// Load a single RIR delegated stats file.
    pub fn init_from_file(path: impl AsRef<Path>) -> Result<Self, Error> {
        let content = std::fs::read_to_string(path)?;
        let (v4_entries, v6_entries) = parse::parse_delegated(&content)?;
        Ok(Self {
            v4: table::Table4::build(v4_entries),
            v6: table::Table6::build(v6_entries),
        })
    }

    /// Load multiple RIR delegated stats files.
    pub fn init_from_files(paths: &[impl AsRef<Path>]) -> Result<Self, Error> {
        let mut v4_entries: Vec<table::Entry4> = Vec::new();
        let mut v6_entries: Vec<table::Entry6> = Vec::new();

        for path in paths {
            let content = std::fs::read_to_string(path)?;
            let (mut v4, mut v6) = parse::parse_delegated(&content)?;
            v4_entries.append(&mut v4);
            v6_entries.append(&mut v6);
        }

        Ok(Self {
            v4: table::Table4::build(v4_entries),
            v6: table::Table6::build(v6_entries),
        })
    }

    /// Look up the RIR record for the given IP address.
    ///
    /// Returns `None` if the address is not found in any RIR record
    /// (unallocated, private, loopback, etc.).
    pub fn lookup(&self, ip: IpAddr) -> Option<Record> {
        match ip {
            IpAddr::V4(v4) => self.v4.lookup(u32::from(v4)),
            IpAddr::V6(v6) => self.v6.lookup(u128::from(v6)),
        }
    }

    /// Total number of IP range entries loaded (IPv4 + IPv6).
    pub fn len(&self) -> usize {
        self.v4.len() + self.v6.len()
    }

    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }
}
