//! Load and search in a CTY database
//!
//! licensed under CC BY-SA 4.0,
//! To view a copy of this license, visit <http://creativecommons.org/licenses/by-sa/4.0/>
//!
//! Author: Andreas, <df1asc@darc.de>
//!
//! # Example
//!
//! ```
//! use hamcty::CountryDatabase;
//!
//! fn main() {
//!     let cdb = CountryDatabase::load("test_data/cty.csv").expect("Could not load cty data");
//!     let call = "DF1ASC";
//!     assert_eq!(cdb.country(call).unwrap().code, "DL");
//!     assert_eq!(cdb.prefix(call), Ok("DF".to_string()));
//!  }
//! ```

use regex::Regex;
use std::collections::HashMap;
use std::error;
use std::fmt::{Display, Formatter};
use std::sync::LazyLock;

#[derive(Debug, PartialEq)]
pub enum Error {
    CtyFileError(String),
    CSVError(String),
    ParseError(String),
    PrefixNotFound,
    CountryCodeNotFound,
    CountryNotFound,
    VersionEntityNotFound,
}

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

impl error::Error for Error {}

/// Ham radio country code
pub type CountryCode = String;

/// Continents
#[derive(Debug, Clone, PartialEq)]
pub enum Continent {
    NorthAmerica,
    SouthAmerica,
    Europe,
    Africa,
    Oceania,
    Asia,
    Antarctica,
    Unknown(String),
}

impl From<&str> for Continent {
    /// Converts two letter continent codes to `Continent`
    fn from(value: &str) -> Self {
        match value.to_uppercase().as_str() {
            "NA" => Continent::NorthAmerica,
            "SA" => Continent::SouthAmerica,
            "EU" => Continent::Europe,
            "AF" => Continent::Africa,
            "OC" => Continent::Oceania,
            "AS" => Continent::Asia,
            "AN" => Continent::Antarctica,
            _ => Continent::Unknown(value.to_string()),
        }
    }
}

/// DXCC entity code
pub type DXCCEntity = u16;

/// CQ zone number
pub type CQZone = u8;

/// ITU zone number
pub type ITUZone = u8;

/// Coordinates for a location
#[derive(Debug, Clone, PartialEq)]
pub struct Location {
    pub lat: f32,
    pub lon: f32,
}

/// UTC time offset
pub type TimeOffset = f32;

/// Holds a country information set
#[derive(Debug, Clone, PartialEq)]
pub struct Country {
    /// Ham radio country code
    pub code: CountryCode,
    /// English country name
    pub name: String,
    /// DXCC country code
    pub dxcc: DXCCEntity,
    /// The continent
    pub continent: Continent,
    /// CQ Zone number
    pub cq: CQZone,
    /// ITU Zone number
    pub itu: ITUZone,
    /// Latitude and longitude of country center
    pub location: Location,
    /// Time offset from UTC
    pub time_off: TimeOffset,
}

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

/// Maps the Ham radio country code to the country information
pub type CountryMap = HashMap<CountryCode, Country>;

/// Holds mapping data to mapping prefixes or calls to a country
#[derive(Debug, Clone)]
struct OverrideData {
    /// Ham radio country code
    code: CountryCode,
    /// CQ Zone Override
    cq: Option<CQZone>,
    /// ITU Zone override
    itu: Option<ITUZone>,
}

//noinspection RegExpRedundantEscape
static RE_OVERRIDES: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"([A-Z0-9/=]+)(?:\((\d+)\))?(?:\[(\d+)\])?.*").unwrap());

/// Holds the complete country database
pub struct CountryDatabase {
    /// Mapping for country code to country
    countries: CountryMap,
    /// Mapping for callsign prefixes to country code
    prefix_overrides: HashMap<String, OverrideData>,
    /// Prefixes as sorted list
    prefixes: Vec<String>,
    /// Mapping for callsign overrides to country code
    call_overrides: HashMap<String, OverrideData>,
    /// The data version
    version: String,
}

impl CountryDatabase {
    /// Loads a cty.csv (either normal or big) for searching
    pub fn load(path: impl AsRef<str>) -> Result<Self, Error> {
        let mut countries = CountryMap::new();
        let mut prefix_overrides: HashMap<String, OverrideData> = HashMap::new();
        let mut call_overrides: HashMap<String, OverrideData> = HashMap::new();
        let mut version = String::new();

        let mut rdr = csv::ReaderBuilder::new()
            .delimiter(b',')
            .has_headers(false)
            .from_path(path.as_ref())
            .map_err(|e| Error::CtyFileError(e.to_string()))?;
        for res in rdr.records() {
            //1A,Sov Mil Order of Malta,246,EU,15,28,41.90,-12.43,-1.0,1A;
            let record = res.map_err(|e| Error::CSVError(e.to_string()))?;
            let code = record.get(0).unwrap_or_default().replace("*", "");

            countries.insert(
                code.to_string(),
                Country {
                    code: code.to_string(),
                    name: record.get(1).unwrap_or_default().to_string(),
                    dxcc: record.get(2).unwrap_or_default().parse().map_err(|_| {
                        Error::ParseError(format!("DXCC for country code '{code}'"))
                    })?,
                    continent: record.get(3).unwrap_or_default().into(),
                    cq: record.get(4).unwrap_or_default().parse().map_err(|_| {
                        Error::ParseError(format!("CQ zone for country code '{code}'"))
                    })?,
                    itu: record.get(5).unwrap_or_default().parse().map_err(|_| {
                        Error::ParseError(format!("ITU zone for country code '{code}'"))
                    })?,
                    location: Location {
                        lat: record.get(6).unwrap_or_default().parse().map_err(|_| {
                            Error::ParseError(format!("longitude for country code '{code}'"))
                        })?,
                        lon: record.get(7).unwrap_or_default().parse().map_err(|_| {
                            Error::ParseError(format!("latitude for country code '{code}'"))
                        })?,
                    },
                    time_off: record.get(8).unwrap_or_default().parse().map_err(|_| {
                        Error::ParseError(format!("time offset for country code '{code}'"))
                    })?,
                },
            );

            for pfx in record
                .get(9)
                .unwrap_or_default()
                .replace(";", "")
                .split(" ")
            {
                let (pfx, cq, itu) = if let Some(cap) = Regex::captures(&RE_OVERRIDES, pfx) {
                    (
                        cap.get(1).map(|v| v.as_str().to_string()),
                        cap.get(2).map(|v| v.as_str().parse::<CQZone>().unwrap()),
                        cap.get(3).map(|v| v.as_str().parse::<ITUZone>().unwrap()),
                    )
                } else {
                    (None, None, None)
                };

                let Some(pfx) = pfx else { continue };

                if let Some(pfx) = pfx.strip_prefix("=") {
                    if code == "VE" && pfx.starts_with("VER") {
                        version = pfx.to_string();
                        continue;
                    }
                    if !call_overrides.contains_key(pfx) {
                        call_overrides.insert(
                            pfx.to_string(),
                            OverrideData {
                                code: code.to_string(),
                                cq,
                                itu,
                            },
                        );
                    }
                } else {
                    prefix_overrides.insert(
                        pfx,
                        OverrideData {
                            code: code.to_string(),
                            cq,
                            itu,
                        },
                    );
                }
            }
        }

        let mut prefixes = prefix_overrides
            .clone()
            .keys()
            .map(|pfx| pfx.to_string())
            .collect::<Vec<String>>();
        prefixes.sort();

        Ok(Self {
            countries,
            prefix_overrides,
            prefixes,
            call_overrides,
            version,
        })
    }

    /// Returns the database version
    pub fn version(&self) -> &String {
        &self.version
    }

    /// Returns the version entity for verification purpose
    pub fn version_entity(&self) -> Result<(String, String), Error> {
        let cty = self
            .country("VERSION")
            .map_err(|_| Error::VersionEntityNotFound)?;
        Ok((cty.code, cty.name))
    }

    /// Finds the prefix for a callsign
    pub fn prefix(&self, call: impl AsRef<str>) -> Result<String, Error> {
        let mut part = String::new();
        let idx: usize = 0;
        let mut pfx = String::new();

        for c in call.as_ref().to_uppercase().chars() {
            part.push(c);
            if let Some(idx) = self.prefixes[idx..].iter().position(|r| r == &part) {
                pfx = self.prefixes[idx].to_string()
            } else {
                continue;
            }
        }

        if !pfx.is_empty() {
            Ok(pfx)
        } else {
            Err(Error::PrefixNotFound)
        }
    }

    /// Searches the country code and override data for a callsign
    /// If data is not found in callsigns, the data is searched in prefixes
    fn country_code(&self, call: impl AsRef<str>) -> Result<&OverrideData, Error> {
        if let Some(ord) = self.call_overrides.get(&call.as_ref().to_uppercase()) {
            return Ok(ord);
        }

        if let Some(ord) = self
            .prefix_overrides
            .get(&self.prefix(&call).map_err(|_| Error::CountryCodeNotFound)?)
        {
            return Ok(ord);
        }

        // Can never be reached: If prefix is found it is also in prefix overrides
        Err(Error::CountryCodeNotFound)
    }

    /// Fetches the `Country` data for a callsign
    pub fn country(&self, call: impl AsRef<str>) -> Result<Country, Error> {
        let data = self.country_code(&call)?;

        if let Some(cty) = self.countries.get(&data.code) {
            Ok(Country {
                cq: if let Some(cq) = data.cq { cq } else { cty.cq },
                itu: if let Some(itu) = data.itu {
                    itu
                } else {
                    cty.itu
                },
                ..cty.clone()
            })
        } else {
            Err(Error::CountryNotFound)
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    // The tests will only work with cty.csv from 3 June 2026 (CTY-3620)
    // VER20260603, Version entity is Kingdom of Eswatini, 3DA
    fn setup() -> CountryDatabase {
        CountryDatabase::load("test_data/cty.csv").expect("error loading test data")
    }

    #[test]
    fn test_100_version() {
        let cdb = setup();

        assert_eq!(cdb.version(), "VER20260603");
        assert_eq!(
            cdb.version_entity(),
            Ok(("3DA".to_string(), "Kingdom of Eswatini".to_string()))
        );
    }

    #[test]
    fn test_110_prefix() {
        let cdb = setup();

        assert_eq!(cdb.prefix("DF1ASC"), Ok("DF".to_string()));
        assert_eq!(cdb.prefix("DK50K32MR"), Ok("DK".to_string()));
        assert_eq!(cdb.prefix("XX1XX"), Err(Error::PrefixNotFound));
    }

    #[test]
    fn test_120_search() {
        let cdb = setup();

        let cty = cdb.country("DF1ASC").unwrap();
        assert_eq!(cty.code, "DL");
        assert_eq!(cty.continent, Continent::Europe);

        assert_eq!(cdb.country("XX1XX"), Err(Error::CountryCodeNotFound));
    }

    #[test]
    fn test_130_search_w_override() {
        let cdb = setup();

        // Override country for call
        let cty = cdb.country("LU1ZA").unwrap();
        assert_eq!(cty.code, "VP8/o");
        assert_eq!(cty.continent, Continent::SouthAmerica);

        // Override ITU and CQ for call
        let cty = cdb.country("4U1WB").unwrap();
        assert_eq!(cty.code, "K");
        assert_eq!(cty.continent, Continent::NorthAmerica);
        assert_eq!(cty.cq, 5);
        assert_eq!(cty.itu, 8);

        // Override ITU for prefix
        let cty = cdb.country("PP6XX").unwrap();
        assert_eq!(cty.code, "PY");
        assert_eq!(cty.continent, Continent::SouthAmerica);
        assert_eq!(cty.itu, 13);
        assert_eq!(cty.cq, 11);
    }
}
