use std::collections::HashMap;

use quick_xml::events::Event;
use quick_xml::Reader;

use super::super::facts::normalize_currency_code;
use super::super::service::EdgarLookupError;
use super::super::types::UnitFamily;

#[derive(Debug, Clone, Default)]
pub(crate) struct FilingXbrlDocument {
    pub contexts: HashMap<String, FilingContext>,
    pub units: HashMap<String, FilingUnit>,
    pub facts: Vec<FilingFact>,
}

#[derive(Debug, Clone, Default)]
pub(crate) struct FilingContext {
    pub entity_identifier: Option<String>,
    pub period_start: Option<String>,
    pub period_end: Option<String>,
    pub instant: Option<String>,
    pub has_dimensions: bool,
}

impl FilingContext {
    pub(crate) fn is_duration(&self) -> bool {
        self.period_start.is_some() && self.period_end.is_some()
    }
}

#[derive(Debug, Clone, Default)]
pub(crate) struct FilingUnit {
    pub measures: Vec<String>,
}

#[derive(Debug, Clone)]
pub(crate) struct FilingFact {
    pub prefix: Option<String>,
    pub local_name: String,
    pub context_ref: Option<String>,
    pub unit_ref: Option<String>,
    pub value: FilingFactValue,
    pub is_nil: bool,
}

#[derive(Debug, Clone)]
pub(crate) enum FilingFactValue {
    Numeric(f64),
    Text(String),
}

impl FilingFact {
    pub(crate) fn numeric_value(&self) -> Option<f64> {
        match self.value {
            FilingFactValue::Numeric(value) => Some(value),
            FilingFactValue::Text(_) => None,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ContextField {
    Identifier,
    StartDate,
    EndDate,
    Instant,
    Measure,
}

pub(crate) fn parse_filing_xbrl_instance(
    bytes: &[u8],
) -> Result<FilingXbrlDocument, EdgarLookupError> {
    let mut reader = Reader::from_reader(bytes);
    reader.config_mut().trim_text(true);

    let mut document = FilingXbrlDocument::default();
    let mut current_context_id: Option<String> = None;
    let mut current_unit_id: Option<String> = None;
    let mut current_field: Option<ContextField> = None;
    let mut buffer = Vec::new();

    loop {
        match reader.read_event_into(&mut buffer) {
            Ok(Event::Start(element)) => {
                let element_name = decode_name(element.name().as_ref());
                match element_name.as_str() {
                    name if name.ends_with("context") => {
                        current_context_id = attribute_value(&element, b"id");
                        current_field = None;
                    }
                    name if name.ends_with("unit") => {
                        current_unit_id = attribute_value(&element, b"id");
                        current_field = None;
                    }
                    name if name.ends_with("identifier") => {
                        current_field = Some(ContextField::Identifier)
                    }
                    name if name.ends_with("startDate") => {
                        current_field = Some(ContextField::StartDate)
                    }
                    name if name.ends_with("endDate") => {
                        current_field = Some(ContextField::EndDate)
                    }
                    name if name.ends_with("instant") => {
                        current_field = Some(ContextField::Instant)
                    }
                    name if name.ends_with("measure") => {
                        current_field = Some(ContextField::Measure)
                    }
                    name if name.ends_with("segment") || name.ends_with("scenario") => {
                        if let Some(context_id) = current_context_id.as_ref() {
                            document
                                .contexts
                                .entry(context_id.clone())
                                .or_default()
                                .has_dimensions = true;
                        }
                    }
                    _ if is_fact_candidate(&element_name) => {
                        let fact = read_fact(&mut reader, &element, &element_name)?;
                        document.facts.push(fact);
                    }
                    _ => {}
                }
            }
            Ok(Event::Empty(element)) => {
                let element_name = decode_name(element.name().as_ref());
                if is_fact_candidate(&element_name) {
                    document.facts.push(empty_fact(&element, &element_name));
                }
            }
            Ok(Event::Text(text)) => {
                let value = String::from_utf8_lossy(text.as_ref()).trim().to_string();
                if value.is_empty() {
                    buffer.clear();
                    continue;
                }

                match current_field {
                    Some(ContextField::Identifier) => {
                        if let Some(context_id) = current_context_id.as_ref() {
                            document
                                .contexts
                                .entry(context_id.clone())
                                .or_default()
                                .entity_identifier = Some(value);
                        }
                    }
                    Some(ContextField::StartDate) => {
                        if let Some(context_id) = current_context_id.as_ref() {
                            document
                                .contexts
                                .entry(context_id.clone())
                                .or_default()
                                .period_start = Some(value);
                        }
                    }
                    Some(ContextField::EndDate) => {
                        if let Some(context_id) = current_context_id.as_ref() {
                            document
                                .contexts
                                .entry(context_id.clone())
                                .or_default()
                                .period_end = Some(value);
                        }
                    }
                    Some(ContextField::Instant) => {
                        if let Some(context_id) = current_context_id.as_ref() {
                            document
                                .contexts
                                .entry(context_id.clone())
                                .or_default()
                                .instant = Some(value);
                        }
                    }
                    Some(ContextField::Measure) => {
                        if let Some(unit_id) = current_unit_id.as_ref() {
                            document
                                .units
                                .entry(unit_id.clone())
                                .or_default()
                                .measures
                                .push(value);
                        }
                    }
                    None => {}
                }
            }
            Ok(Event::End(element)) => {
                let element_name = decode_name(element.name().as_ref());
                if element_name.ends_with("context") {
                    current_context_id = None;
                    current_field = None;
                } else if element_name.ends_with("unit") {
                    current_unit_id = None;
                    current_field = None;
                } else if matches!(
                    current_field,
                    Some(ContextField::Identifier)
                        | Some(ContextField::StartDate)
                        | Some(ContextField::EndDate)
                        | Some(ContextField::Instant)
                        | Some(ContextField::Measure)
                ) {
                    current_field = None;
                }
            }
            Ok(Event::Eof) => break,
            Err(source) => {
                return Err(EdgarLookupError::XbrlParseFailed {
                    detail: source.to_string(),
                });
            }
            _ => {}
        }

        buffer.clear();
    }

    Ok(document)
}

pub(crate) fn find_text_fact(
    document: &FilingXbrlDocument,
    prefix: &str,
    local_name: &str,
) -> Option<String> {
    document
        .facts
        .iter()
        .find(|fact| {
            !fact.is_nil
                && prefix_matches(fact.prefix.as_deref(), prefix)
                && fact.local_name == local_name
        })
        .and_then(fact_string_value)
}

pub(crate) fn classify_fact_unit_family(
    document: &FilingXbrlDocument,
    fact: &FilingFact,
) -> Option<UnitFamily> {
    let unit_ref = fact.unit_ref.as_deref()?;
    let unit = document.units.get(unit_ref)?;
    classify_unit_measures(&unit.measures)
}

pub(crate) fn filing_fact_currency_code(
    document: &FilingXbrlDocument,
    fact: &FilingFact,
) -> Option<String> {
    let unit_ref = fact.unit_ref.as_deref()?;
    let unit = document.units.get(unit_ref)?;
    extract_currency_code_from_measures(&unit.measures)
}

fn read_fact(
    reader: &mut Reader<&[u8]>,
    element: &quick_xml::events::BytesStart<'_>,
    element_name: &str,
) -> Result<FilingFact, EdgarLookupError> {
    let (prefix, local_name) = split_qualified_name(element_name);
    let is_nil = attribute_value_by_suffix(element, b"nil")
        .map(|value| value.eq_ignore_ascii_case("true"))
        .unwrap_or(false);
    let text = reader
        .read_text(element.name())
        .map_err(|source| EdgarLookupError::XbrlParseFailed {
            detail: source.to_string(),
        })?
        .trim()
        .to_string();
    let unit_ref = attribute_value(element, b"unitRef");

    Ok(FilingFact {
        prefix,
        local_name,
        context_ref: attribute_value(element, b"contextRef"),
        unit_ref: unit_ref.clone(),
        value: parse_fact_value(text, unit_ref.as_deref()),
        is_nil,
    })
}

fn empty_fact(element: &quick_xml::events::BytesStart<'_>, element_name: &str) -> FilingFact {
    let (prefix, local_name) = split_qualified_name(element_name);
    FilingFact {
        prefix,
        local_name,
        context_ref: attribute_value(element, b"contextRef"),
        unit_ref: attribute_value(element, b"unitRef"),
        value: FilingFactValue::Text(String::new()),
        is_nil: attribute_value_by_suffix(element, b"nil")
            .map(|value| value.eq_ignore_ascii_case("true"))
            .unwrap_or(false),
    }
}

fn parse_fact_value(raw: String, unit_ref: Option<&str>) -> FilingFactValue {
    if unit_ref.is_none() {
        return FilingFactValue::Text(raw);
    }

    raw.parse::<f64>()
        .map(FilingFactValue::Numeric)
        .unwrap_or_else(|_| FilingFactValue::Text(raw))
}

fn fact_string_value(fact: &FilingFact) -> Option<String> {
    match &fact.value {
        FilingFactValue::Numeric(value) => Some(value.to_string()),
        FilingFactValue::Text(value) if !value.is_empty() => Some(value.clone()),
        FilingFactValue::Text(_) => None,
    }
}

fn classify_unit_measures(measures: &[String]) -> Option<UnitFamily> {
    if measures.is_empty() {
        return None;
    }

    let has_currency = extract_currency_code_from_measures(measures).is_some();
    let has_shares = measures
        .iter()
        .any(|measure| measure_suffix(measure).eq_ignore_ascii_case("shares"));
    let has_pure = measures
        .iter()
        .any(|measure| measure_suffix(measure).eq_ignore_ascii_case("pure"));

    if has_currency && has_shares {
        return Some(UnitFamily::CurrencyPerShare);
    }
    if has_currency {
        return Some(UnitFamily::Currency);
    }
    if has_shares {
        return Some(UnitFamily::Shares);
    }
    if has_pure {
        return Some(UnitFamily::Pure);
    }

    None
}

fn measure_suffix(measure: &str) -> &str {
    measure.rsplit(':').next().unwrap_or(measure)
}

pub(crate) fn extract_currency_code_from_measures(measures: &[String]) -> Option<String> {
    measures
        .iter()
        .find_map(|measure| normalize_currency_code(measure))
}

fn attribute_value(element: &quick_xml::events::BytesStart<'_>, key: &[u8]) -> Option<String> {
    element
        .attributes()
        .flatten()
        .find(|attribute| attribute.key.as_ref() == key)
        .map(|attribute| String::from_utf8_lossy(attribute.value.as_ref()).to_string())
}

fn attribute_value_by_suffix(
    element: &quick_xml::events::BytesStart<'_>,
    key_suffix: &[u8],
) -> Option<String> {
    element
        .attributes()
        .flatten()
        .find(|attribute| attribute.key.as_ref().ends_with(key_suffix))
        .map(|attribute| String::from_utf8_lossy(attribute.value.as_ref()).to_string())
}

fn decode_name(value: &[u8]) -> String {
    String::from_utf8_lossy(value).to_string()
}

fn split_qualified_name(value: &str) -> (Option<String>, String) {
    match value.split_once(':') {
        Some((prefix, local_name)) => (Some(prefix.to_string()), local_name.to_string()),
        None => (None, value.to_string()),
    }
}

fn prefix_matches(actual: Option<&str>, expected: &str) -> bool {
    actual.is_none_or(|value| value == expected)
}

fn is_fact_candidate(name: &str) -> bool {
    !name.ends_with("context")
        && !name.ends_with("unit")
        && !name.ends_with("measure")
        && !name.ends_with("identifier")
        && !name.ends_with("segment")
        && !name.ends_with("entity")
        && !name.ends_with("period")
        && !name.ends_with("xbrl")
        && !name.contains("schemaRef")
        && name.contains(':')
}

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

    #[test]
    fn parse_filing_xbrl_instance_should_capture_text_and_numeric_facts() {
        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<xbrli:xbrl xmlns:xbrli="http://www.xbrl.org/2003/instance" xmlns:dei="http://xbrl.sec.gov/dei/2024" xmlns:us-gaap="http://fasb.org/us-gaap/2024" xmlns:iso4217="http://www.xbrl.org/2003/iso4217">
  <xbrli:context id="c1"><xbrli:entity><xbrli:identifier scheme="http://www.sec.gov/CIK">1</xbrli:identifier></xbrli:entity><xbrli:period><xbrli:startDate>2024-01-01</xbrli:startDate><xbrli:endDate>2024-12-31</xbrli:endDate></xbrli:period></xbrli:context>
  <xbrli:unit id="usd"><xbrli:measure>iso4217:USD</xbrli:measure></xbrli:unit>
  <dei:DocumentFiscalYearFocus contextRef="c1">2024</dei:DocumentFiscalYearFocus>
  <us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax contextRef="c1" unitRef="usd">100</us-gaap:RevenueFromContractWithCustomerExcludingAssessedTax>
</xbrli:xbrl>"#;

        let document = parse_filing_xbrl_instance(xml.as_bytes()).expect("xml should parse");
        assert_eq!(
            find_text_fact(&document, "dei", "DocumentFiscalYearFocus"),
            Some("2024".to_string())
        );
        assert_eq!(
            document
                .facts
                .iter()
                .filter_map(FilingFact::numeric_value)
                .next(),
            Some(100.0)
        );
    }

    #[test]
    fn classify_unit_measures_should_detect_unprefixed_shares() {
        assert_eq!(
            classify_unit_measures(&["shares".to_string()]),
            Some(UnitFamily::Shares)
        );
    }

    #[test]
    fn classify_unit_measures_should_detect_unprefixed_pure() {
        assert_eq!(
            classify_unit_measures(&["pure".to_string()]),
            Some(UnitFamily::Pure)
        );
    }

    #[test]
    fn classify_unit_measures_should_detect_currency_per_share_with_unprefixed_shares() {
        assert_eq!(
            classify_unit_measures(&["iso4217:USD".to_string(), "shares".to_string()]),
            Some(UnitFamily::CurrencyPerShare)
        );
    }

    #[test]
    fn extracts_currency_codes_from_prefixed_xbrl_measures() {
        assert_eq!(
            extract_currency_code_from_measures(&["iso4217:USD".to_string()]),
            Some("USD".to_string())
        );
        assert_eq!(
            extract_currency_code_from_measures(&["iso4217:EUR".to_string()]),
            Some("EUR".to_string())
        );
    }

    #[test]
    fn extracts_currency_codes_from_divided_eps_units() {
        assert_eq!(
            extract_currency_code_from_measures(&[
                "iso4217:EUR".to_string(),
                "xbrli:shares".to_string(),
            ]),
            Some("EUR".to_string())
        );
    }
}
