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

use chrono::NaiveDate;

use crate::mappings::{
    CustomConceptMapping, FormulaContext, FormulaEvaluator, FormulaMapping, FormulaParser,
    MappingKind, MappingScope, TargetField, XbrlConceptResolver,
};
use crate::terminal::types::AvailableXbrlConcept;

use super::earnings::facts::{
    classify_fact_unit_family, filing_fact_currency_code, FilingContext, FilingXbrlDocument,
};
use super::facts::matches_requested_currency;
use super::types::ConceptCandidate;

pub(crate) fn select_numeric_value_with_custom(
    document: &FilingXbrlDocument,
    standard_candidates: &[ConceptCandidate],
    target_field: TargetField,
    custom_mappings: &[CustomConceptMapping],
    ticker: &str,
    period_date: &str,
    selected_currency: Option<&str>,
    matches_context: impl Fn(&FilingContext) -> bool + Copy,
) -> Option<f64> {
    select_custom_numeric_value(
        document,
        target_field,
        custom_mappings,
        ticker,
        period_date,
        selected_currency,
        matches_context,
    )
    .or_else(|| {
        select_numeric_value(
            document,
            standard_candidates,
            selected_currency,
            matches_context,
        )
    })
}

pub(crate) fn applicable_custom_mappings<'a>(
    custom_mappings: &'a [CustomConceptMapping],
    target_field: TargetField,
    ticker: &str,
    period_date: &str,
) -> Vec<&'a CustomConceptMapping> {
    let mut applicable = custom_mappings
        .iter()
        .filter(|mapping| mapping.enabled)
        .filter(|mapping| mapping.target_field == target_field)
        .filter(|mapping| scope_matches(&mapping.scope, ticker, period_date))
        .collect::<Vec<_>>();

    applicable.sort_by_key(|mapping| Reverse(mapping_priority_score(mapping)));
    applicable
}

pub(crate) fn collect_formula_mappings<'a>(
    custom_mappings: &'a [CustomConceptMapping],
    ticker: &str,
    period_date: &str,
) -> Vec<(&'a CustomConceptMapping, &'a FormulaMapping)> {
    custom_mappings
        .iter()
        .filter(|mapping| mapping.enabled)
        .filter(|mapping| scope_matches(&mapping.scope, ticker, period_date))
        .filter_map(|mapping| match &mapping.mapping_kind {
            MappingKind::Formula(formula) => Some((mapping, formula)),
            MappingKind::Concept(_) => None,
        })
        .collect()
}

pub(crate) type FormulaCheckTargets = HashSet<TargetField>;

pub(crate) fn formula_check_targets(
    formula_mappings: &[(&CustomConceptMapping, &FormulaMapping)],
) -> FormulaCheckTargets {
    formula_mappings
        .iter()
        .map(|(mapping, _)| mapping.target_field)
        .collect()
}

pub(crate) fn evaluate_formulas(
    formula_mappings: Vec<(&CustomConceptMapping, &FormulaMapping)>,
    field_values: &HashMap<TargetField, f64>,
    concept_resolver: &XbrlConceptResolver,
) -> (HashMap<TargetField, f64>, Vec<String>) {
    if formula_mappings.is_empty() {
        return (HashMap::new(), Vec::new());
    }

    let mut warnings = Vec::new();
    let dependency_graph = build_formula_dependency_graph(&formula_mappings);

    let sorted_fields = match topological_sort_formulas(&dependency_graph) {
        Ok(fields) => fields,
        Err(error) => {
            warnings.push(format!("Formula ordering error: {error}"));
            return (HashMap::new(), warnings);
        }
    };

    let formula_map: HashMap<TargetField, &FormulaMapping> = formula_mappings
        .into_iter()
        .map(|(mapping, formula)| (mapping.target_field, formula))
        .collect();

    let mut calculated_values = field_values.clone();

    for field in sorted_fields {
        let Some(formula) = formula_map.get(&field) else {
            continue;
        };

        let ast = match &formula.cached_ast {
            Some(ast) => ast.clone(),
            None => match FormulaParser::parse(&formula.expression) {
                Ok(ast) => ast,
                Err(error) => {
                    warnings.push(format!("{field:?}: parse error: {error}"));
                    continue;
                }
            },
        };

        let context =
            FormulaContext::new(calculated_values.clone()).with_concept_resolver(concept_resolver);
        let evaluator = FormulaEvaluator::new(&context);

        match evaluator.evaluate(&ast) {
            Ok(result) => {
                calculated_values.insert(field, result);
            }
            Err(error) => {
                warnings.push(format!("{field:?}: {error}"));
            }
        }
    }

    (calculated_values, warnings)
}

pub(crate) fn collect_available_concepts(
    document: &FilingXbrlDocument,
) -> Vec<AvailableXbrlConcept> {
    document
        .facts
        .iter()
        .filter_map(|fact| {
            let taxonomy = fact.prefix.clone()?;
            let unit_family = classify_fact_unit_family(document, fact)?;

            Some(AvailableXbrlConcept {
                taxonomy,
                concept: fact.local_name.clone(),
                unit_family: unit_family.as_str().to_string(),
                label: None,
                period_id: fact
                    .context_ref
                    .clone()
                    .unwrap_or_else(|| "unknown".to_string()),
                value: fact.numeric_value(),
            })
        })
        .collect()
}

fn select_custom_numeric_value(
    document: &FilingXbrlDocument,
    target_field: TargetField,
    custom_mappings: &[CustomConceptMapping],
    ticker: &str,
    period_date: &str,
    selected_currency: Option<&str>,
    matches_context: impl Fn(&FilingContext) -> bool + Copy,
) -> Option<f64> {
    applicable_custom_mappings(custom_mappings, target_field, ticker, period_date)
        .into_iter()
        .find_map(|mapping| {
            let custom_concept = mapping.custom_concept()?;

            document.facts.iter().find_map(|fact| {
                let context_ref = fact.context_ref.as_deref()?;
                let context = document.contexts.get(context_ref)?;

                (matches_context(context)
                    && prefix_matches(fact.prefix.as_deref(), custom_concept.taxonomy.as_str())
                    && fact.local_name == custom_concept.concept.as_str()
                    && classify_fact_unit_family(document, fact)
                        == Some(custom_concept.unit_family)
                    && matches_requested_currency(
                        custom_concept.unit_family,
                        filing_fact_currency_code(document, fact).as_deref(),
                        selected_currency,
                    ))
                .then(|| fact.numeric_value())
                .flatten()
            })
        })
}

fn select_numeric_value(
    document: &FilingXbrlDocument,
    candidates: &[ConceptCandidate],
    selected_currency: Option<&str>,
    matches_context: impl Fn(&FilingContext) -> bool + Copy,
) -> Option<f64> {
    candidates.iter().find_map(|candidate| {
        document.facts.iter().find_map(|fact| {
            let context_ref = fact.context_ref.as_deref()?;
            let context = document.contexts.get(context_ref)?;

            (matches_context(context)
                && prefix_matches(fact.prefix.as_deref(), candidate.prefix)
                && fact.local_name == candidate.local_name
                && classify_fact_unit_family(document, fact) == Some(candidate.unit_family)
                && matches_requested_currency(
                    candidate.unit_family,
                    filing_fact_currency_code(document, fact).as_deref(),
                    selected_currency,
                ))
            .then(|| fact.numeric_value())
            .flatten()
        })
    })
}

fn mapping_priority_score(mapping: &CustomConceptMapping) -> u8 {
    let mut score = 0u8;

    if mapping.scope.ticker.is_some() {
        score += 100;
    }
    if mapping.scope.period_start.is_some() {
        score += 10;
    }
    if mapping.scope.period_end.is_some() {
        score += 10;
    }

    score
}

fn scope_matches(scope: &MappingScope, ticker: &str, period_date: &str) -> bool {
    if let Some(scope_ticker) = scope.ticker.as_deref() {
        if !scope_ticker.eq_ignore_ascii_case(ticker) {
            return false;
        }
    }

    let parse_bound = |value: &str| NaiveDate::parse_from_str(value, "%Y-%m-%d").ok();
    let parsed_period_date = match (scope.period_start.as_deref(), scope.period_end.as_deref()) {
        (Some(_), _) | (_, Some(_)) => match parse_bound(period_date) {
            Some(date) => Some(date),
            None => return false,
        },
        (None, None) => None,
    };

    if let Some(start) = scope.period_start.as_deref() {
        let Some(start_date) = parse_bound(start) else {
            return false;
        };

        if parsed_period_date.is_some_and(|date| date < start_date) {
            return false;
        }
    }

    if let Some(end) = scope.period_end.as_deref() {
        let Some(end_date) = parse_bound(end) else {
            return false;
        };

        if parsed_period_date.is_some_and(|date| date > end_date) {
            return false;
        }
    }

    true
}

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

fn build_formula_dependency_graph(
    formula_mappings: &[(&CustomConceptMapping, &FormulaMapping)],
) -> HashMap<TargetField, Vec<TargetField>> {
    let mut graph = HashMap::new();

    for (mapping, formula) in formula_mappings {
        let dependencies = if let Some(ast) = &formula.cached_ast {
            ast.collect_field_refs()
        } else {
            continue;
        };

        graph.insert(mapping.target_field, dependencies);
    }

    graph
}

fn topological_sort_formulas(
    dependency_graph: &HashMap<TargetField, Vec<TargetField>>,
) -> Result<Vec<TargetField>, String> {
    let mut in_degree: HashMap<TargetField, usize> = HashMap::new();
    let mut all_fields: HashSet<TargetField> = HashSet::new();

    for (target, dependencies) in dependency_graph {
        all_fields.insert(*target);
        in_degree.entry(*target).or_insert(0);

        for dependency in dependencies {
            all_fields.insert(*dependency);
            in_degree.entry(*dependency).or_insert(0);
            *in_degree.entry(*target).or_insert(0) += 1;
        }
    }

    let mut queue: Vec<TargetField> = all_fields
        .iter()
        .filter(|field| *in_degree.get(field).unwrap_or(&0) == 0)
        .copied()
        .collect();
    let mut sorted = Vec::new();

    while let Some(field) = queue.pop() {
        sorted.push(field);

        for (target, dependencies) in dependency_graph {
            if dependencies.contains(&field) {
                let entry = in_degree.get_mut(target).expect("target should exist");
                *entry -= 1;
                if *entry == 0 {
                    queue.push(*target);
                }
            }
        }
    }

    if sorted.len() != all_fields.len() {
        return Err("Circular reference detected in formulas".to_string());
    }

    Ok(sorted)
}
