use std::collections::{HashMap, HashSet};

use regex::Regex;
use tracing::{error, info};

use crate::models::{Currency, RansomwareExport, SanctionedAddress};
use crate::repositories::{AddressRepository, CountryRepository};

/// Result of parsing the OFAC SDN Advanced XML.
struct OfacXmlResult {
    addresses: Vec<SanctionedAddress>,
    programs: HashSet<String>,
}

/// OFAC program code → (ISO 3166-1 alpha-2, country/region name).
/// Only includes programs that map to comprehensively sanctioned jurisdictions.
fn program_to_country(program: &str) -> Option<(&'static str, &'static str)> {
    match program {
        "CUBA" => Some(("CU", "Cuba")),
        "IRAN" | "IFSR" | "ISA" | "IRGC" | "CAATSA - IRAN" | "HRIT-IR" => {
            Some(("IR", "Iran"))
        }
        "DPRK" | "DPRK2" | "DPRK3" | "DPRK4" | "DPRK-NKSPEA" => {
            Some(("KP", "North Korea"))
        }
        "SYRIA" | "HRIT-SY" => Some(("SY", "Syria")),
        "RUSSIA-EO14024" | "RUSSIA-EO14065" | "CAATSA - RUSSIA" | "PEESA-EO14039" => {
            Some(("RU", "Russia"))
        }
        "UKRAINE-EO13685" => Some(("UA-CR", "Crimea Region of Ukraine")),
        "UKRAINE-EO13660" | "UKRAINE-EO13661" | "UKRAINE-EO13662" => {
            Some(("UA-DN", "Donetsk/Luhansk Regions of Ukraine"))
        }
        "BELARUS" | "BELARUS-EO14038" => Some(("BY", "Belarus")),
        "VENEZUELA" | "VENEZUELA-EO13850" | "VENEZUELA-EO13884" => {
            Some(("VE", "Venezuela"))
        }
        "NICARAGUA" | "NICARAGUA-NHRAA" => Some(("NI", "Nicaragua")),
        "BURMA-EO14014" => Some(("MM", "Myanmar")),
        "BALKANS" | "BALKANS-EO14033" => Some(("XB", "Western Balkans")),
        "LIBYA" | "LIBYA2" | "LIBYA3" => Some(("LY", "Libya")),
        "SOMALIA" => Some(("SO", "Somalia")),
        "YEMEN" => Some(("YE", "Yemen")),
        "MALI" => Some(("ML", "Mali")),
        "CAR" => Some(("CF", "Central African Republic")),
        "DARFUR" => Some(("SD", "Sudan (Darfur)")),
        "DRCONGO" => Some(("CD", "Democratic Republic of the Congo")),
        "LEBANON" => Some(("LB", "Lebanon")),
        "SOUTH-SUDAN" => Some(("SS", "South Sudan")),
        "ETHIOPIA-EO14046" => Some(("ET", "Ethiopia")),
        _ => None,
    }
}

/// Fetches sanctions data from official sources and upserts into the database.
///
/// Sources:
/// 1. **OFAC SDN Advanced XML** — authoritative U.S. Treasury source with structured
///    crypto addresses (~772) and sanctions program data. Covers 18 currency types.
/// 2. **Ransomware tracker API** — ransomwhe.re export of known ransomware payment addresses.
pub struct IngestionService {
    http_client: reqwest::Client,
    ofac_xml_url: String,
    ransomware_url: String,
}

impl IngestionService {
    pub fn new(ofac_xml_url: String, ransomware_url: String) -> Self {
        let http_client = reqwest::Client::builder()
            .timeout(std::time::Duration::from_secs(120))
            .build()
            .expect("failed to build HTTP client");

        Self {
            http_client,
            ofac_xml_url,
            ransomware_url,
        }
    }

    /// Run a single ingestion cycle: fetch from all sources, normalize, upsert.
    pub async fn run_cycle(
        &self,
        address_repo: &impl AddressRepository,
        country_repo: &impl CountryRepository,
    ) -> crate::error::Result<usize> {
        let mut addresses = Vec::new();
        let mut programs = HashSet::new();

        // 1. Fetch OFAC SDN Advanced XML (addresses + programs).
        match self.fetch_ofac_xml().await {
            Ok(result) => {
                info!(
                    addresses = result.addresses.len(),
                    programs = result.programs.len(),
                    "fetched OFAC SDN Advanced XML"
                );
                addresses.extend(result.addresses);
                programs.extend(result.programs);
            }
            Err(e) => error!(error = %e, "failed to fetch OFAC XML"),
        }

        // 2. Fetch ransomware tracker addresses.
        match self.fetch_ransomware().await {
            Ok(ransom) => {
                info!(count = ransom.len(), "fetched ransomware addresses");
                addresses.extend(ransom);
            }
            Err(e) => error!(error = %e, "failed to fetch ransomware data"),
        }

        // Upsert addresses.
        let total = addresses.len();
        for addr in &addresses {
            if let Err(e) =
                address_repo
                    .upsert(&addr.currency, &addr.address, &addr.source_type)
                    .await
            {
                error!(
                    error = %e,
                    currency = %addr.currency,
                    address = %addr.address,
                    "failed to upsert address"
                );
            }
        }

        // Upsert sanctioned countries derived from OFAC programs.
        let mut country_count = 0usize;
        for program in &programs {
            if let Some((code, name)) = program_to_country(program) {
                if let Err(e) = country_repo.upsert(code, name, program).await {
                    error!(error = %e, program, "failed to upsert country");
                } else {
                    country_count += 1;
                }
            }
        }

        info!(total, country_count, "ingestion cycle complete");
        Ok(total)
    }

    /// Fetch and parse the OFAC SDN Advanced XML.
    async fn fetch_ofac_xml(&self) -> crate::error::Result<OfacXmlResult> {
        let body = self
            .http_client
            .get(&self.ofac_xml_url)
            .send()
            .await?
            .text()
            .await?;

        Ok(parse_ofac_xml(&body))
    }

    /// Fetch and parse the ransomware tracker export.
    async fn fetch_ransomware(&self) -> crate::error::Result<Vec<SanctionedAddress>> {
        let export: RansomwareExport = self
            .http_client
            .get(&self.ransomware_url)
            .send()
            .await?
            .json()
            .await?;

        Ok(export
            .result
            .into_iter()
            .map(|entry| {
                let currency = Currency::normalize(&entry.blockchain);
                SanctionedAddress {
                    currency: currency.to_string(),
                    address: entry.address,
                    source_type: "RANSOM".into(),
                }
            })
            .collect())
    }
}

/// Parse the OFAC SDN Advanced XML to extract crypto addresses and program codes.
///
/// Uses targeted regex rather than a full XML parser — the XML structure is stable and
/// the patterns are well-defined:
///
/// - FeatureType definitions: `<FeatureType ID="344">Digital Currency Address - XBT</FeatureType>`
/// - Address values: `<Feature ... FeatureTypeID="344">...<VersionDetail ...>ADDRESS</VersionDetail>`
/// - Program codes: `<SanctionsMeasure ... SanctionsTypeID="1"><Comment>PROGRAM</Comment>`
fn parse_ofac_xml(xml: &str) -> OfacXmlResult {
    // Step 1: Build FeatureTypeID → currency map from definitions.
    let feature_type_re = Regex::new(
        r#"<FeatureType ID="(\d+)"[^>]*>Digital Currency Address - (\w+)</FeatureType>"#,
    )
    .expect("invalid regex");

    let mut type_id_to_currency: HashMap<String, String> = HashMap::new();
    for cap in feature_type_re.captures_iter(xml) {
        let id = cap[1].to_string();
        let raw_currency = &cap[2];
        let currency = Currency::normalize(raw_currency).to_string();
        type_id_to_currency.insert(id, currency);
    }

    // Step 2: Extract addresses from Feature elements.
    // Pattern: <Feature ... FeatureTypeID="ID">...<VersionDetail ...>ADDRESS</VersionDetail>
    // Uses (?s) flag so .*? spans newlines within multi-line XML elements.
    let feature_re = Regex::new(
        r#"(?s)<Feature [^>]*FeatureTypeID="(\d+)"[^>]*>.*?<VersionDetail[^>]*>([^<]+)</VersionDetail>"#,
    )
    .expect("invalid regex");

    let mut addresses = Vec::new();
    for cap in feature_re.captures_iter(xml) {
        let type_id = &cap[1];
        let address = cap[2].trim();
        if let Some(currency) = type_id_to_currency.get(type_id) {
            if !address.is_empty() {
                addresses.push(SanctionedAddress {
                    currency: currency.clone(),
                    address: address.to_string(),
                    source_type: "OFAC".into(),
                });
            }
        }
    }

    // Step 3: Extract program codes from SanctionsMeasure elements.
    // Pattern: <SanctionsMeasure ... SanctionsTypeID="1"><Comment>PROGRAM</Comment>
    let program_re = Regex::new(
        r#"<SanctionsMeasure [^>]*SanctionsTypeID="1"[^>]*>\s*<Comment>([^<]+)</Comment>"#,
    )
    .expect("invalid regex");

    let mut programs = HashSet::new();
    for cap in program_re.captures_iter(xml) {
        let program = cap[1].trim();
        if !program.is_empty() {
            programs.insert(program.to_string());
        }
    }

    info!(
        feature_types = type_id_to_currency.len(),
        addresses = addresses.len(),
        programs = programs.len(),
        "parsed OFAC SDN Advanced XML"
    );

    OfacXmlResult {
        addresses,
        programs,
    }
}

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

    #[test]
    fn parse_ofac_xml_extracts_addresses_and_programs() {
        let xml = r#"<?xml version="1.0"?>
<Sanctions>
  <ReferenceValueSets>
    <FeatureTypeValues>
      <FeatureType ID="344" FeatureTypeGroupID="1">Digital Currency Address - XBT</FeatureType>
      <FeatureType ID="345" FeatureTypeGroupID="1">Digital Currency Address - ETH</FeatureType>
      <FeatureType ID="887" FeatureTypeGroupID="1">Digital Currency Address - USDT</FeatureType>
      <FeatureType ID="224" FeatureTypeGroupID="1">Gender</FeatureType>
    </FeatureTypeValues>
  </ReferenceValueSets>
  <SanctionsEntries>
    <SanctionsEntry ID="36" ProfileID="36" ListID="1550">
      <SanctionsMeasure ID="126005" SanctionsTypeID="1">
        <Comment>CUBA</Comment>
        <DatePeriod CalendarTypeID="1" YearFixed="true" MonthFixed="true" DayFixed="true" />
      </SanctionsMeasure>
    </SanctionsEntry>
    <SanctionsEntry ID="28263" ProfileID="28263" ListID="1550">
      <SanctionsMeasure ID="130000" SanctionsTypeID="1">
        <Comment>DPRK3</Comment>
        <DatePeriod CalendarTypeID="1" YearFixed="true" MonthFixed="true" DayFixed="true" />
      </SanctionsMeasure>
      <SanctionsMeasure ID="130001" SanctionsTypeID="1">
        <Comment>CYBER2</Comment>
        <DatePeriod CalendarTypeID="1" YearFixed="true" MonthFixed="true" DayFixed="true" />
      </SanctionsMeasure>
    </SanctionsEntry>
  </SanctionsEntries>
  <Profiles>
    <Profile>
      <Feature ID="31723" FeatureTypeID="344">
        <FeatureVersion ID="29462" ReliabilityID="1">
          <Comment />
          <VersionDetail DetailTypeID="1432">12QtD5BFwRsdNsAZY76UVE1xyCGNTojH9h</VersionDetail>
        </FeatureVersion>
      </Feature>
      <Feature ID="31724" FeatureTypeID="345">
        <FeatureVersion ID="29463" ReliabilityID="1">
          <Comment />
          <VersionDetail DetailTypeID="1432">0x098B716B8Aaf21512996dC57EB0615e2383E2f96</VersionDetail>
        </FeatureVersion>
      </Feature>
      <Feature ID="50000" FeatureTypeID="887">
        <FeatureVersion ID="49000" ReliabilityID="1">
          <Comment />
          <VersionDetail DetailTypeID="1432">TA3941uFAvmVibSkQ6fMJXxmaSNovX86mz</VersionDetail>
        </FeatureVersion>
      </Feature>
      <Feature ID="99999" FeatureTypeID="224">
        <FeatureVersion ID="88888" ReliabilityID="1">
          <Comment />
          <VersionDetail DetailTypeID="1432">Male</VersionDetail>
        </FeatureVersion>
      </Feature>
    </Profile>
  </Profiles>
</Sanctions>"#;

        let result = parse_ofac_xml(xml);

        // Should extract 3 addresses (XBT, ETH, USDT) but not Gender.
        assert_eq!(result.addresses.len(), 3);

        assert_eq!(result.addresses[0].currency, "BTC"); // XBT normalized
        assert_eq!(result.addresses[0].address, "12QtD5BFwRsdNsAZY76UVE1xyCGNTojH9h");
        assert_eq!(result.addresses[0].source_type, "OFAC");

        assert_eq!(result.addresses[1].currency, "ETH");
        assert_eq!(
            result.addresses[1].address,
            "0x098B716B8Aaf21512996dC57EB0615e2383E2f96"
        );

        assert_eq!(result.addresses[2].currency, "USDT");
        assert_eq!(result.addresses[2].address, "TA3941uFAvmVibSkQ6fMJXxmaSNovX86mz");

        // Should extract programs.
        assert!(result.programs.contains("CUBA"));
        assert!(result.programs.contains("DPRK3"));
        assert!(result.programs.contains("CYBER2"));
        assert_eq!(result.programs.len(), 3);
    }

    #[test]
    fn parse_ofac_xml_empty_input() {
        let result = parse_ofac_xml("");
        assert!(result.addresses.is_empty());
        assert!(result.programs.is_empty());
    }

    #[test]
    fn program_to_country_maps_known_programs() {
        assert_eq!(program_to_country("CUBA"), Some(("CU", "Cuba")));
        assert_eq!(program_to_country("IRAN"), Some(("IR", "Iran")));
        assert_eq!(program_to_country("DPRK3"), Some(("KP", "North Korea")));
        assert_eq!(program_to_country("SYRIA"), Some(("SY", "Syria")));
        assert_eq!(
            program_to_country("RUSSIA-EO14024"),
            Some(("RU", "Russia"))
        );
        assert_eq!(program_to_country("SDGT"), None);
        assert_eq!(program_to_country("FTO"), None);
    }
}
