use std::collections::HashMap;

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

use super::facts::normalize_currency_code;
use super::service::EdgarLookupError;
use super::types::{LatestXbrlFact, ParsedXbrlDocument, UnitFamily};

#[derive(Debug, Clone, Default)]
struct ParsedContext {
    start: Option<String>,
    end: Option<String>,
    instant: Option<String>,
    has_dimensions: bool,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ParsedField {
    Start,
    End,
    Instant,
    Measure,
}

pub(crate) fn pick_instance_document(candidates: &[String]) -> Option<String> {
    let preferred = candidates.iter().find(|name| name.ends_with("_htm.xml"));
    if let Some(value) = preferred {
        return Some(value.clone());
    }

    let preferred = candidates.iter().find(|name| {
        name.ends_with(".xml")
            && !name.contains("_cal")
            && !name.contains("_def")
            && !name.contains("_lab")
            && !name.contains("_pre")
            && !name.contains("schema")
    });
    if let Some(value) = preferred {
        return Some(value.clone());
    }

    candidates
        .iter()
        .find(|name| name.ends_with(".xml"))
        .cloned()
}

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

    let mut contexts = HashMap::<String, ParsedContext>::new();
    let mut units = HashMap::<String, Vec<String>>::new();
    let mut current_context: Option<String> = None;
    let mut current_unit: Option<String> = None;
    let mut current_field: Option<ParsedField> = None;

    let mut facts = Vec::new();
    let mut buffer = Vec::new();

    loop {
        match reader.read_event_into(&mut buffer) {
            Ok(Event::Start(element)) => {
                let name = element.name().as_ref().to_vec();
                let name = String::from_utf8_lossy(&name).to_string();

                if name.ends_with("context") {
                    current_context = attribute_value(&element, b"id");
                    current_field = None;
                } else if name.ends_with("unit") {
                    current_unit = attribute_value(&element, b"id");
                    current_field = None;
                } else if name.ends_with("startDate") {
                    current_field = Some(ParsedField::Start);
                } else if name.ends_with("endDate") {
                    current_field = Some(ParsedField::End);
                } else if name.ends_with("instant") {
                    current_field = Some(ParsedField::Instant);
                } else if name.ends_with("measure") {
                    current_field = Some(ParsedField::Measure);
                } else if name.ends_with("segment") || name.ends_with("scenario") {
                    if let Some(context_id) = current_context.as_ref() {
                        contexts
                            .entry(context_id.clone())
                            .or_default()
                            .has_dimensions = true;
                    }
                } else if is_fact_candidate(&name) {
                    let Some(context_ref) = attribute_value(&element, b"contextRef") else {
                        buffer.clear();
                        continue;
                    };
                    let text = reader
                        .read_text(element.name())
                        .map_err(|source| EdgarLookupError::XbrlParseFailed {
                            detail: source.to_string(),
                        })?
                        .trim()
                        .to_string();

                    if let Ok(value) = text.parse::<f64>() {
                        let context = contexts.get(&context_ref).cloned().unwrap_or_default();
                        let unit_ref = attribute_value(&element, b"unitRef");
                        let (unit_family, currency_code) = unit_ref
                            .as_deref()
                            .and_then(|unit_ref| units.get(unit_ref))
                            .map(|measures| classify_xbrl_unit_measures(measures))
                            .unwrap_or((None, None));
                        facts.push(LatestXbrlFact {
                            concept: name,
                            unit_family,
                            currency_code,
                            value,
                            context_ref,
                            period_start: context.start.clone(),
                            period_end: context.end.clone().or(context.instant.clone()),
                            is_instant: context.instant.is_some(),
                            has_dimensions: context.has_dimensions,
                        });
                    }
                }
            }
            Ok(Event::Text(text)) => {
                let text = String::from_utf8_lossy(text.as_ref()).trim().to_string();
                if text.is_empty() {
                    buffer.clear();
                    continue;
                }

                match current_field {
                    Some(ParsedField::Start) => {
                        if let Some(context_id) = current_context.as_ref() {
                            contexts.entry(context_id.clone()).or_default().start = Some(text);
                        }
                    }
                    Some(ParsedField::End) => {
                        if let Some(context_id) = current_context.as_ref() {
                            contexts.entry(context_id.clone()).or_default().end = Some(text);
                        }
                    }
                    Some(ParsedField::Instant) => {
                        if let Some(context_id) = current_context.as_ref() {
                            contexts.entry(context_id.clone()).or_default().instant = Some(text);
                        }
                    }
                    Some(ParsedField::Measure) => {
                        if let Some(unit_id) = current_unit.as_ref() {
                            units.entry(unit_id.clone()).or_default().push(text);
                        }
                    }
                    _ => {}
                }
            }
            Ok(Event::End(element)) => {
                let name = element.name().as_ref().to_vec();
                let name = String::from_utf8_lossy(&name).to_string();
                if name.ends_with("context") {
                    current_context = None;
                    current_field = None;
                } else if name.ends_with("unit") {
                    current_unit = None;
                    current_field = None;
                } else if name.ends_with("startDate")
                    || name.ends_with("endDate")
                    || name.ends_with("instant")
                    || name.ends_with("measure")
                {
                    current_field = None;
                }
            }
            Ok(Event::Eof) => break,
            Err(source) => {
                return Err(EdgarLookupError::XbrlParseFailed {
                    detail: source.to_string(),
                })
            }
            _ => {}
        }

        buffer.clear();
    }

    Ok(ParsedXbrlDocument { facts })
}

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 classify_xbrl_unit_measures(measures: &[String]) -> (Option<UnitFamily>, Option<String>) {
    let currency_code = measures
        .iter()
        .find_map(|measure| normalize_currency_code(measure));
    let has_currency = currency_code.is_some();
    let has_shares = measures.iter().any(|measure| {
        measure
            .rsplit(':')
            .next()
            .is_some_and(|value| value.eq_ignore_ascii_case("shares"))
    });
    let has_pure = measures.iter().any(|measure| {
        measure
            .rsplit(':')
            .next()
            .is_some_and(|value| value.eq_ignore_ascii_case("pure"))
    });

    let unit_family = if has_currency && has_shares {
        Some(UnitFamily::CurrencyPerShare)
    } else if has_currency {
        Some(UnitFamily::Currency)
    } else if has_shares {
        Some(UnitFamily::Shares)
    } else if has_pure {
        Some(UnitFamily::Pure)
    } else {
        None
    };

    (unit_family, currency_code)
}
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.contains(':')
}
