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

use chrono::NaiveDate;

use crate::terminal::{
    CashFlowPeriod, DividendEvent, FilingRef, Frequency, SourceStatus, StatementPeriod,
};

use super::service::EdgarLookupError;
use super::types::{
    CompanyConceptFacts, CompanyFactRecord, CompanyFactsResponse, ConceptCandidate, NormalizedFact,
    ParsedXbrlDocument, PeriodRow, ResolvedCompany, UnitFamily,
};

pub(crate) const DEFAULT_ANNUAL_EARNINGS_PERIODS: usize = 4;
pub(crate) const DEFAULT_QUARTERLY_EARNINGS_PERIODS: usize = 8;

const ANNUAL_FORMS: &[&str] = &["10-K", "20-F", "40-F", "10-K/A", "20-F/A", "40-F/A"];
const QUARTERLY_FORMS: &[&str] = &["10-Q", "6-K", "10-Q/A", "6-K/A"];

pub(crate) const REVENUE_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "RevenueFromContractWithCustomerExcludingAssessedTax",
        UnitFamily::Currency,
    ),
    candidate("us-gaap", "SalesRevenueNet", UnitFamily::Currency),
    candidate("us-gaap", "Revenues", UnitFamily::Currency),
    candidate("ifrs-full", "Revenue", UnitFamily::Currency),
];
pub(crate) const GROSS_PROFIT_CONCEPTS: &[ConceptCandidate] =
    &[candidate("us-gaap", "GrossProfit", UnitFamily::Currency)];
pub(crate) const OPERATING_INCOME_CONCEPTS: &[ConceptCandidate] = &[
    candidate("us-gaap", "OperatingIncomeLoss", UnitFamily::Currency),
    candidate(
        "ifrs-full",
        "ProfitLossFromOperatingActivities",
        UnitFamily::Currency,
    ),
];
pub(crate) const NET_INCOME_CONCEPTS: &[ConceptCandidate] = &[
    candidate("us-gaap", "NetIncomeLoss", UnitFamily::Currency),
    candidate("ifrs-full", "ProfitLoss", UnitFamily::Currency),
];
pub(crate) const DILUTED_EPS_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "EarningsPerShareDiluted",
        UnitFamily::CurrencyPerShare,
    ),
    candidate(
        "ifrs-full",
        "BasicAndDilutedEarningsLossPerShare",
        UnitFamily::CurrencyPerShare,
    ),
    candidate(
        "ifrs-full",
        "DilutedEarningsLossPerShare",
        UnitFamily::CurrencyPerShare,
    ),
];
pub(crate) const CASH_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "CashAndCashEquivalentsAtCarryingValue",
        UnitFamily::Currency,
    ),
    candidate("ifrs-full", "CashAndCashEquivalents", UnitFamily::Currency),
];
pub(crate) const SHORT_TERM_INVESTMENTS_CONCEPTS: &[ConceptCandidate] = &[
    candidate("us-gaap", "ShortTermInvestments", UnitFamily::Currency),
    candidate(
        "us-gaap",
        "AvailableForSaleSecuritiesCurrent",
        UnitFamily::Currency,
    ),
    candidate(
        "us-gaap",
        "MarketableSecuritiesCurrent",
        UnitFamily::Currency,
    ),
    candidate(
        "ifrs-full",
        "OtherCurrentFinancialAssets",
        UnitFamily::Currency,
    ),
];
pub(crate) const ACCOUNTS_RECEIVABLE_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "AccountsReceivableNetCurrent",
        UnitFamily::Currency,
    ),
    candidate("us-gaap", "ReceivablesNetCurrent", UnitFamily::Currency),
    candidate("ifrs-full", "CurrentTradeReceivables", UnitFamily::Currency),
];
pub(crate) const INVENTORY_CONCEPTS: &[ConceptCandidate] = &[
    candidate("us-gaap", "InventoryNet", UnitFamily::Currency),
    candidate("ifrs-full", "Inventories", UnitFamily::Currency),
];
pub(crate) const OTHER_CURRENT_ASSETS_CONCEPTS: &[ConceptCandidate] = &[
    candidate("us-gaap", "OtherAssetsCurrent", UnitFamily::Currency),
    candidate(
        "us-gaap",
        "PrepaidExpenseAndOtherAssetsCurrent",
        UnitFamily::Currency,
    ),
    candidate(
        "ifrs-full",
        "OtherCurrentNonfinancialAssets",
        UnitFamily::Currency,
    ),
];
pub(crate) const CURRENT_ASSETS_CONCEPTS: &[ConceptCandidate] = &[
    candidate("us-gaap", "AssetsCurrent", UnitFamily::Currency),
    candidate("ifrs-full", "CurrentAssets", UnitFamily::Currency),
];
pub(crate) const ASSET_CONCEPTS: &[ConceptCandidate] = &[
    candidate("us-gaap", "Assets", UnitFamily::Currency),
    candidate("ifrs-full", "Assets", UnitFamily::Currency),
];
pub(crate) const PPE_NET_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "PropertyPlantAndEquipmentNet",
        UnitFamily::Currency,
    ),
    candidate(
        "ifrs-full",
        "PropertyPlantAndEquipment",
        UnitFamily::Currency,
    ),
];
pub(crate) const GOODWILL_CONCEPTS: &[ConceptCandidate] = &[
    candidate("us-gaap", "Goodwill", UnitFamily::Currency),
    candidate("ifrs-full", "Goodwill", UnitFamily::Currency),
];
pub(crate) const INTANGIBLE_ASSETS_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "FiniteLivedIntangibleAssetsNet",
        UnitFamily::Currency,
    ),
    candidate(
        "us-gaap",
        "IntangibleAssetsNetExcludingGoodwill",
        UnitFamily::Currency,
    ),
    candidate(
        "ifrs-full",
        "IntangibleAssetsOtherThanGoodwill",
        UnitFamily::Currency,
    ),
];
pub(crate) const OTHER_NON_CURRENT_ASSETS_CONCEPTS: &[ConceptCandidate] = &[
    candidate("us-gaap", "OtherAssetsNoncurrent", UnitFamily::Currency),
    candidate("ifrs-full", "OtherNoncurrentAssets", UnitFamily::Currency),
];
pub(crate) const ACCOUNTS_PAYABLE_CONCEPTS: &[ConceptCandidate] = &[
    candidate("us-gaap", "AccountsPayableCurrent", UnitFamily::Currency),
    candidate(
        "ifrs-full",
        "TradeAndOtherCurrentPayables",
        UnitFamily::Currency,
    ),
];
pub(crate) const ACCRUED_CURRENT_LIABILITIES_CONCEPTS: &[ConceptCandidate] = &[
    candidate("us-gaap", "AccruedLiabilitiesCurrent", UnitFamily::Currency),
    candidate("us-gaap", "OtherLiabilitiesCurrent", UnitFamily::Currency),
    candidate("ifrs-full", "OtherCurrentProvisions", UnitFamily::Currency),
];
pub(crate) const SHORT_TERM_DEBT_CONCEPTS: &[ConceptCandidate] = &[
    candidate("us-gaap", "ShortTermBorrowings", UnitFamily::Currency),
    candidate("us-gaap", "LongTermDebtCurrent", UnitFamily::Currency),
    candidate("us-gaap", "CommercialPaper", UnitFamily::Currency),
    candidate("ifrs-full", "CurrentBorrowings", UnitFamily::Currency),
];
pub(crate) const CURRENT_LIABILITIES_CONCEPTS: &[ConceptCandidate] = &[
    candidate("us-gaap", "LiabilitiesCurrent", UnitFamily::Currency),
    candidate("ifrs-full", "CurrentLiabilities", UnitFamily::Currency),
];
pub(crate) const LIABILITY_CONCEPTS: &[ConceptCandidate] = &[
    candidate("us-gaap", "Liabilities", UnitFamily::Currency),
    candidate("ifrs-full", "Liabilities", UnitFamily::Currency),
];
pub(crate) const LONG_TERM_DEBT_CONCEPTS: &[ConceptCandidate] = &[
    candidate("us-gaap", "LongTermDebtNoncurrent", UnitFamily::Currency),
    candidate(
        "us-gaap",
        "LongTermDebtAndCapitalLeaseObligations",
        UnitFamily::Currency,
    ),
    candidate("ifrs-full", "NoncurrentBorrowings", UnitFamily::Currency),
];
pub(crate) const OTHER_NON_CURRENT_LIABILITIES_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "OtherLiabilitiesNoncurrent",
        UnitFamily::Currency,
    ),
    candidate(
        "ifrs-full",
        "OtherNoncurrentLiabilities",
        UnitFamily::Currency,
    ),
];
pub(crate) const EQUITY_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "StockholdersEquityIncludingPortionAttributableToNoncontrollingInterest",
        UnitFamily::Currency,
    ),
    candidate("us-gaap", "StockholdersEquity", UnitFamily::Currency),
    candidate("ifrs-full", "Equity", UnitFamily::Currency),
];
pub(crate) const RETAINED_EARNINGS_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "RetainedEarningsAccumulatedDeficit",
        UnitFamily::Currency,
    ),
    candidate("ifrs-full", "RetainedEarnings", UnitFamily::Currency),
];
pub(crate) const AOCI_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "AccumulatedOtherComprehensiveIncomeLossNetOfTax",
        UnitFamily::Currency,
    ),
    candidate("ifrs-full", "OtherReserves", UnitFamily::Currency),
];
pub(crate) const SHARES_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "dei",
        "EntityCommonStockSharesOutstanding",
        UnitFamily::Shares,
    ),
    candidate(
        "us-gaap",
        "CommonStockSharesOutstanding",
        UnitFamily::Shares,
    ),
    candidate("ifrs-full", "NumberOfSharesOutstanding", UnitFamily::Shares),
];
pub(crate) const CFO_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "NetCashProvidedByUsedInOperatingActivities",
        UnitFamily::Currency,
    ),
    candidate(
        "ifrs-full",
        "CashFlowsFromUsedInOperatingActivities",
        UnitFamily::Currency,
    ),
];
pub(crate) const CFI_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "NetCashProvidedByUsedInInvestingActivities",
        UnitFamily::Currency,
    ),
    candidate(
        "ifrs-full",
        "CashFlowsFromUsedInInvestingActivities",
        UnitFamily::Currency,
    ),
];
pub(crate) const CFF_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "NetCashProvidedByUsedInFinancingActivities",
        UnitFamily::Currency,
    ),
    candidate(
        "ifrs-full",
        "CashFlowsFromUsedInFinancingActivities",
        UnitFamily::Currency,
    ),
];
pub(crate) const CAPEX_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "PaymentsToAcquirePropertyPlantAndEquipment",
        UnitFamily::Currency,
    ),
    candidate(
        "us-gaap",
        "PropertyPlantAndEquipmentAdditions",
        UnitFamily::Currency,
    ),
    candidate(
        "ifrs-full",
        "PurchaseOfPropertyPlantAndEquipmentClassifiedAsInvestingActivities",
        UnitFamily::Currency,
    ),
];
pub(crate) const NET_INCOME_CF_CONCEPTS: &[ConceptCandidate] = &[
    candidate("us-gaap", "NetIncomeLoss", UnitFamily::Currency),
    candidate("ifrs-full", "ProfitLoss", UnitFamily::Currency),
];
pub(crate) const DEPRECIATION_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "DepreciationDepletionAndAmortization",
        UnitFamily::Currency,
    ),
    candidate(
        "us-gaap",
        "DepreciationAndAmortization",
        UnitFamily::Currency,
    ),
    candidate(
        "ifrs-full",
        "DepreciationAndAmortisation",
        UnitFamily::Currency,
    ),
];
pub(crate) const STOCK_BASED_COMPENSATION_CONCEPTS: &[ConceptCandidate] = &[
    candidate("us-gaap", "ShareBasedCompensation", UnitFamily::Currency),
    candidate("us-gaap", "StockCompensationExpense", UnitFamily::Currency),
];
pub(crate) const DEFERRED_INCOME_TAXES_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "DeferredIncomeTaxExpenseBenefit",
        UnitFamily::Currency,
    ),
    candidate(
        "us-gaap",
        "DeferredIncomeTaxesAndTaxCredits",
        UnitFamily::Currency,
    ),
    candidate(
        "us-gaap",
        "IncreaseDecreaseInDeferredIncomeTaxes",
        UnitFamily::Currency,
    ),
];
pub(crate) const IMPAIRMENT_CHARGES_CONCEPTS: &[ConceptCandidate] = &[
    candidate("us-gaap", "AssetImpairmentCharges", UnitFamily::Currency),
    candidate("us-gaap", "GoodwillImpairmentLosses", UnitFamily::Currency),
    candidate(
        "us-gaap",
        "ImpairmentOfGoodwillAndIndefiniteLivedIntangibleAssets",
        UnitFamily::Currency,
    ),
];
pub(crate) const LOSS_GAIN_ON_SALE_OF_ASSETS_CONCEPTS: &[ConceptCandidate] = &[
    candidate("us-gaap", "LossGainOnSaleOfAssets", UnitFamily::Currency),
    candidate(
        "us-gaap",
        "GainLossOnSaleOfPropertyPlantEquipment",
        UnitFamily::Currency,
    ),
    candidate(
        "us-gaap",
        "GainLossOnDispositionOfAssets",
        UnitFamily::Currency,
    ),
];
pub(crate) const OTHER_NON_CASH_ITEMS_CONCEPTS: &[ConceptCandidate] = &[
    candidate("us-gaap", "OtherNoncashIncomeExpense", UnitFamily::Currency),
    candidate("us-gaap", "OtherNoncashExpense", UnitFamily::Currency),
    candidate(
        "us-gaap",
        "OtherNoncashItemsIncludedInNetIncomeLoss",
        UnitFamily::Currency,
    ),
];
pub(crate) const CASH_FLOW_ACCOUNTS_RECEIVABLE_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "IncreaseDecreaseInAccountsReceivable",
        UnitFamily::Currency,
    ),
    candidate(
        "us-gaap",
        "ChangesInAccountReceivables",
        UnitFamily::Currency,
    ),
];
pub(crate) const CASH_FLOW_INVENTORY_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "IncreaseDecreaseInInventories",
        UnitFamily::Currency,
    ),
    candidate("us-gaap", "ChangesInInventories", UnitFamily::Currency),
];
pub(crate) const PREPAIDS_AND_OTHER_CURRENT_ASSETS_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "IncreaseDecreaseInPrepaidDeferredExpenseAndOtherAssets",
        UnitFamily::Currency,
    ),
    candidate(
        "us-gaap",
        "IncreaseDecreaseInPrepaidExpenseAndOtherAssets",
        UnitFamily::Currency,
    ),
    candidate(
        "us-gaap",
        "IncreaseDecreaseInOtherCurrentAssets",
        UnitFamily::Currency,
    ),
];
pub(crate) const CASH_FLOW_ACCOUNTS_PAYABLE_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "IncreaseDecreaseInAccountsPayable",
        UnitFamily::Currency,
    ),
    candidate("us-gaap", "ChangesInAccountPayables", UnitFamily::Currency),
];
pub(crate) const ACCRUED_EXPENSES_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "IncreaseDecreaseInAccruedLiabilities",
        UnitFamily::Currency,
    ),
    candidate(
        "us-gaap",
        "IncreaseDecreaseInAccruedExpenses",
        UnitFamily::Currency,
    ),
    candidate(
        "us-gaap",
        "IncreaseDecreaseInAccruedIncomeTaxesPayable",
        UnitFamily::Currency,
    ),
];
pub(crate) const DEFERRED_REVENUE_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "IncreaseDecreaseInContractWithCustomerLiability",
        UnitFamily::Currency,
    ),
    candidate(
        "us-gaap",
        "IncreaseDecreaseInDeferredRevenue",
        UnitFamily::Currency,
    ),
];
pub(crate) const OTHER_WORKING_CAPITAL_CHANGES_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "IncreaseDecreaseInOperatingCapital",
        UnitFamily::Currency,
    ),
    candidate(
        "us-gaap",
        "IncreaseDecreaseInOperatingAssetsAndLiabilitiesNet",
        UnitFamily::Currency,
    ),
    candidate(
        "us-gaap",
        "OtherAssetsAndLiabilitiesNet",
        UnitFamily::Currency,
    ),
];
pub(crate) const CHANGE_IN_OPERATING_ASSETS_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "IncreaseDecreaseInAccountsReceivable",
        UnitFamily::Currency,
    ),
    candidate(
        "us-gaap",
        "ChangesInAccountReceivables",
        UnitFamily::Currency,
    ),
];
pub(crate) const CHANGE_IN_OPERATING_LIABILITIES_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "IncreaseDecreaseInAccountsPayable",
        UnitFamily::Currency,
    ),
    candidate("us-gaap", "ChangesInAccountPayables", UnitFamily::Currency),
];
pub(crate) const OTHER_OPERATING_ADJUSTMENTS_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "OtherAdjustmentsToReconcileNetIncomeLossToCashProvidedByUsedInOperatingActivities",
        UnitFamily::Currency,
    ),
    candidate("us-gaap", "OtherNoncashIncomeExpense", UnitFamily::Currency),
];
pub(crate) const PROCEEDS_FROM_SALE_OF_PPE_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "ProceedsFromSaleOfPropertyPlantAndEquipment",
        UnitFamily::Currency,
    ),
    candidate(
        "us-gaap",
        "ProceedsFromSaleOfPropertyPlantAndEquipmentAndIntangibleAssets",
        UnitFamily::Currency,
    ),
];
pub(crate) const PURCHASES_OF_INVESTMENTS_CONCEPTS: &[ConceptCandidate] = &[
    candidate("us-gaap", "PaymentsForInvestments", UnitFamily::Currency),
    candidate("us-gaap", "PurchaseOfInvestments", UnitFamily::Currency),
    candidate(
        "us-gaap",
        "PaymentsToAcquireInvestments",
        UnitFamily::Currency,
    ),
];
pub(crate) const SALES_OF_INVESTMENTS_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "ProceedsFromSaleOfInvestments",
        UnitFamily::Currency,
    ),
    candidate("us-gaap", "MaturitiesOfInvestments", UnitFamily::Currency),
];
pub(crate) const ACQUISITIONS_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "PaymentsToAcquireBusinessesNetOfCashAcquired",
        UnitFamily::Currency,
    ),
    candidate(
        "us-gaap",
        "PaymentsToAcquireBusinesses",
        UnitFamily::Currency,
    ),
    candidate(
        "us-gaap",
        "PaymentsMadeForAcquisitionOfBusiness",
        UnitFamily::Currency,
    ),
];
pub(crate) const OTHER_INVESTING_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "OtherCashPaymentsForInvestingActivities",
        UnitFamily::Currency,
    ),
    candidate(
        "us-gaap",
        "OtherInvestingActivitiesNetCashFlow",
        UnitFamily::Currency,
    ),
];
pub(crate) const DEBT_ISSUANCE_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "ProceedsFromIssuanceOfDebt",
        UnitFamily::Currency,
    ),
    candidate(
        "us-gaap",
        "ProceedsFromIssuanceOfLongTermDebt",
        UnitFamily::Currency,
    ),
];
pub(crate) const DEBT_REPAYMENT_CONCEPTS: &[ConceptCandidate] = &[
    candidate("us-gaap", "PaymentsOfLongTermDebt", UnitFamily::Currency),
    candidate("us-gaap", "RepaymentsOfLongTermDebt", UnitFamily::Currency),
    candidate("us-gaap", "RepaymentsOfDebt", UnitFamily::Currency),
];
pub(crate) const EQUITY_ISSUANCE_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "ProceedsFromIssuanceOfCommonStock",
        UnitFamily::Currency,
    ),
    candidate(
        "us-gaap",
        "ProceedsFromStockOptionsExercised",
        UnitFamily::Currency,
    ),
    candidate(
        "us-gaap",
        "ProceedsFromIssuanceOfSharesUnderIncentiveAndShareBasedCompensationPlansIncludingStockOptions",
        UnitFamily::Currency,
    ),
];
pub(crate) const STOCK_REPURCHASES_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "PaymentsForRepurchaseOfCommonStock",
        UnitFamily::Currency,
    ),
    candidate("us-gaap", "RepurchaseOfCommonStock", UnitFamily::Currency),
    candidate(
        "us-gaap",
        "PaymentsForRepurchaseOfTreasuryStock",
        UnitFamily::Currency,
    ),
];
pub(crate) const DIVIDENDS_PAID_CONCEPTS: &[ConceptCandidate] = &[
    candidate("us-gaap", "PaymentsOfDividends", UnitFamily::Currency),
    candidate(
        "us-gaap",
        "PaymentsOfDividendsCommonStock",
        UnitFamily::Currency,
    ),
];
pub(crate) const FINANCE_LEASE_OBLIGATIONS_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "PaymentsOfFinanceLeaseObligations",
        UnitFamily::Currency,
    ),
    candidate(
        "us-gaap",
        "PaymentsOfCapitalLeaseObligations",
        UnitFamily::Currency,
    ),
];
pub(crate) const OTHER_FINANCING_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "OtherCashPaymentsForFinancingActivities",
        UnitFamily::Currency,
    ),
    candidate(
        "us-gaap",
        "OtherFinancingActivitiesNetCashFlow",
        UnitFamily::Currency,
    ),
];
pub(crate) const EXCHANGE_RATE_EFFECTS_CONCEPTS: &[ConceptCandidate] = &[candidate(
    "us-gaap",
    "EffectOfExchangeRateOnCashAndCashEquivalents",
    UnitFamily::Currency,
)];
pub(crate) const NET_INCREASE_DECREASE_CASH_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "CashCashEquivalentsRestrictedCashAndRestrictedCashEquivalentsPeriodIncreaseDecreaseIncludingExchangeRateEffect",
        UnitFamily::Currency,
    ),
    candidate(
        "us-gaap",
        "CashAndCashEquivalentsPeriodIncreaseDecrease",
        UnitFamily::Currency,
    ),
    candidate(
        "ifrs-full",
        "IncreaseDecreaseInCashAndCashEquivalents",
        UnitFamily::Currency,
    ),
];
pub(crate) const BEGINNING_CASH_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "CashCashEquivalentsRestrictedCashAndRestrictedCashEquivalentsAtBeginningOfPeriod",
        UnitFamily::Currency,
    ),
    candidate(
        "us-gaap",
        "CashAndCashEquivalentsAtBeginningOfPeriod",
        UnitFamily::Currency,
    ),
    candidate(
        "us-gaap",
        "CashAndCashEquivalentsAtCarryingValue",
        UnitFamily::Currency,
    ),
    candidate("ifrs-full", "CashAndCashEquivalents", UnitFamily::Currency),
];
pub(crate) const ENDING_CASH_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "CashCashEquivalentsRestrictedCashAndRestrictedCashEquivalents",
        UnitFamily::Currency,
    ),
    candidate(
        "us-gaap",
        "CashAndCashEquivalentsAtCarryingValue",
        UnitFamily::Currency,
    ),
    candidate("ifrs-full", "CashAndCashEquivalents", UnitFamily::Currency),
];
pub(crate) const CASH_PAID_INTEREST_CONCEPTS: &[ConceptCandidate] = &[
    candidate("us-gaap", "InterestPaidNet", UnitFamily::Currency),
    candidate("us-gaap", "InterestPaid", UnitFamily::Currency),
    candidate("us-gaap", "CashPaidForInterest", UnitFamily::Currency),
    candidate("ifrs-full", "InterestPaid", UnitFamily::Currency),
];
pub(crate) const CASH_PAID_INCOME_TAXES_CONCEPTS: &[ConceptCandidate] = &[
    candidate("us-gaap", "IncomeTaxesPaidNet", UnitFamily::Currency),
    candidate("us-gaap", "IncomeTaxesPaid", UnitFamily::Currency),
    candidate("us-gaap", "CashPaidForIncomeTaxes", UnitFamily::Currency),
    candidate("ifrs-full", "IncomeTaxesPaid", UnitFamily::Currency),
];
pub(crate) const DIVIDEND_PER_SHARE_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "CommonStockDividendsPerShareDeclared",
        UnitFamily::CurrencyPerShare,
    ),
    candidate(
        "us-gaap",
        "CommonStockDividendsPerShareCashPaid",
        UnitFamily::CurrencyPerShare,
    ),
];
pub(crate) const DIVIDEND_CASH_CONCEPTS: &[ConceptCandidate] = &[
    candidate(
        "us-gaap",
        "PaymentsOfDividendsCommonStock",
        UnitFamily::Currency,
    ),
    candidate("us-gaap", "DividendsCash", UnitFamily::Currency),
    candidate(
        "ifrs-full",
        "DividendsPaidClassifiedAsFinancingActivities",
        UnitFamily::Currency,
    ),
];

const fn candidate(
    prefix: &'static str,
    local_name: &'static str,
    unit_family: UnitFamily,
) -> ConceptCandidate {
    ConceptCandidate {
        prefix,
        local_name,
        unit_family,
    }
}

pub(crate) fn select_latest_filing(
    filings: &[FilingRef],
    frequency: Frequency,
) -> Option<FilingRef> {
    filings
        .iter()
        .filter(|filing| matches_frequency_form(&filing.form, frequency))
        .cloned()
        .max_by(compare_filing_priority)
}

pub(crate) fn normalize_all_facts(
    company_facts: &CompanyFactsResponse,
) -> Result<Vec<NormalizedFact>, EdgarLookupError> {
    let mut normalized = Vec::new();

    for concept in flatten_concepts(company_facts) {
        for (unit, records) in concept.units {
            let unit_family = classify_unit(unit);
            let currency_code = extract_companyfacts_currency_code(unit);
            for record in records {
                let Some(value) = json_number(&record.val) else {
                    continue;
                };

                normalized.push(NormalizedFact {
                    taxonomy: concept.taxonomy,
                    concept: concept.concept_name.clone(),
                    unit_family,
                    currency_code: currency_code.clone(),
                    value,
                    filed: record.filed.clone(),
                    form: record.form.clone(),
                    fiscal_year: record.fy.as_ref().map(fiscal_year_to_string),
                    fiscal_period: record.fp.clone(),
                    period_start: record.start.clone(),
                    period_end: record.end.clone(),
                    accession_number: record.accn.clone(),
                });
            }
        }
    }

    if normalized.is_empty() {
        return Err(EdgarLookupError::NoFactsAvailable);
    }

    Ok(normalized)
}

pub(crate) fn build_statement_rows(
    facts: &[NormalizedFact],
    frequency: Frequency,
    period_limit: usize,
) -> Vec<PeriodRow> {
    build_period_rows(facts, frequency, REVENUE_CONCEPTS, period_limit)
}

pub(crate) fn build_statement_period(
    facts: &[NormalizedFact],
    row: &PeriodRow,
    selected_currency: Option<&str>,
    latest_xbrl: Option<&ParsedXbrlDocument>,
) -> StatementPeriod {
    StatementPeriod {
        label: row.label.clone(),
        fiscal_year: row.fiscal_year.clone(),
        fiscal_period: row.fiscal_period.clone(),
        period_start: row.period_start.clone(),
        period_end: row.period_end.clone(),
        filed_date: row.filed_date.clone(),
        form: row.form.clone(),
        revenue: value_for_period(facts, row, REVENUE_CONCEPTS, selected_currency, latest_xbrl),
        gross_profit: value_for_period(
            facts,
            row,
            GROSS_PROFIT_CONCEPTS,
            selected_currency,
            latest_xbrl,
        ),
        operating_income: value_for_period(
            facts,
            row,
            OPERATING_INCOME_CONCEPTS,
            selected_currency,
            latest_xbrl,
        ),
        net_income: value_for_period(
            facts,
            row,
            NET_INCOME_CONCEPTS,
            selected_currency,
            latest_xbrl,
        ),
        diluted_eps: value_for_period(
            facts,
            row,
            DILUTED_EPS_CONCEPTS,
            selected_currency,
            latest_xbrl,
        ),
        cash_and_equivalents: value_for_period(
            facts,
            row,
            CASH_CONCEPTS,
            selected_currency,
            latest_xbrl,
        ),
        short_term_investments: value_for_period(
            facts,
            row,
            SHORT_TERM_INVESTMENTS_CONCEPTS,
            selected_currency,
            latest_xbrl,
        ),
        accounts_receivable_net: value_for_period(
            facts,
            row,
            ACCOUNTS_RECEIVABLE_CONCEPTS,
            selected_currency,
            latest_xbrl,
        ),
        inventory: value_for_period(
            facts,
            row,
            INVENTORY_CONCEPTS,
            selected_currency,
            latest_xbrl,
        ),
        other_current_assets: value_for_period(
            facts,
            row,
            OTHER_CURRENT_ASSETS_CONCEPTS,
            selected_currency,
            latest_xbrl,
        ),
        total_current_assets: value_for_period(
            facts,
            row,
            CURRENT_ASSETS_CONCEPTS,
            selected_currency,
            latest_xbrl,
        ),
        property_plant_and_equipment_net: value_for_period(
            facts,
            row,
            PPE_NET_CONCEPTS,
            selected_currency,
            latest_xbrl,
        ),
        goodwill: value_for_period(
            facts,
            row,
            GOODWILL_CONCEPTS,
            selected_currency,
            latest_xbrl,
        ),
        intangible_assets: value_for_period(
            facts,
            row,
            INTANGIBLE_ASSETS_CONCEPTS,
            selected_currency,
            latest_xbrl,
        ),
        other_non_current_assets: value_for_period(
            facts,
            row,
            OTHER_NON_CURRENT_ASSETS_CONCEPTS,
            selected_currency,
            latest_xbrl,
        ),
        total_assets: value_for_period(facts, row, ASSET_CONCEPTS, selected_currency, latest_xbrl),
        accounts_payable: value_for_period(
            facts,
            row,
            ACCOUNTS_PAYABLE_CONCEPTS,
            selected_currency,
            latest_xbrl,
        ),
        accrued_expenses_and_other_current_liabilities: value_for_period(
            facts,
            row,
            ACCRUED_CURRENT_LIABILITIES_CONCEPTS,
            selected_currency,
            latest_xbrl,
        ),
        short_term_debt: value_for_period(
            facts,
            row,
            SHORT_TERM_DEBT_CONCEPTS,
            selected_currency,
            latest_xbrl,
        ),
        total_current_liabilities: value_for_period(
            facts,
            row,
            CURRENT_LIABILITIES_CONCEPTS,
            selected_currency,
            latest_xbrl,
        ),
        long_term_debt: value_for_period(
            facts,
            row,
            LONG_TERM_DEBT_CONCEPTS,
            selected_currency,
            latest_xbrl,
        ),
        other_non_current_liabilities: value_for_period(
            facts,
            row,
            OTHER_NON_CURRENT_LIABILITIES_CONCEPTS,
            selected_currency,
            latest_xbrl,
        ),
        total_liabilities: value_for_period(
            facts,
            row,
            LIABILITY_CONCEPTS,
            selected_currency,
            latest_xbrl,
        ),
        retained_earnings: value_for_period(
            facts,
            row,
            RETAINED_EARNINGS_CONCEPTS,
            selected_currency,
            latest_xbrl,
        ),
        accumulated_other_comprehensive_income_loss: value_for_period(
            facts,
            row,
            AOCI_CONCEPTS,
            selected_currency,
            latest_xbrl,
        ),
        total_equity: value_for_period(facts, row, EQUITY_CONCEPTS, selected_currency, latest_xbrl),
        shares_outstanding: value_for_period(
            facts,
            row,
            SHARES_CONCEPTS,
            selected_currency,
            latest_xbrl,
        ),
        book_value_per_share: value_for_period(
            facts,
            row,
            EQUITY_CONCEPTS,
            selected_currency,
            latest_xbrl,
        )
        .zip(value_for_period(
            facts,
            row,
            SHARES_CONCEPTS,
            selected_currency,
            latest_xbrl,
        ))
        .and_then(|(equity, shares)| (shares != 0.0).then_some(equity / shares)),
    }
}

#[allow(dead_code)]
pub(crate) fn build_cash_flow_periods(
    facts: &[NormalizedFact],
    frequency: Frequency,
    latest_xbrl: Option<&ParsedXbrlDocument>,
) -> Vec<CashFlowPeriod> {
    build_cash_flow_periods_from_rows_inner(
        facts,
        build_period_rows(facts, frequency, CFO_CONCEPTS, 4),
        latest_xbrl,
    )
}

#[allow(dead_code)]
pub(crate) fn build_cash_flow_periods_with_limit(
    facts: &[NormalizedFact],
    frequency: Frequency,
    latest_xbrl: Option<&ParsedXbrlDocument>,
    period_limit: usize,
) -> Vec<CashFlowPeriod> {
    build_cash_flow_periods_from_rows_inner(
        facts,
        build_period_rows(facts, frequency, CFO_CONCEPTS, period_limit),
        latest_xbrl,
    )
}

pub(crate) fn build_cash_flow_periods_from_rows(
    facts: &[NormalizedFact],
    rows: Vec<PeriodRow>,
    latest_xbrl: Option<&ParsedXbrlDocument>,
) -> Vec<CashFlowPeriod> {
    build_cash_flow_periods_from_rows_inner(facts, rows, latest_xbrl)
}

fn build_cash_flow_periods_from_rows_inner(
    facts: &[NormalizedFact],
    rows: Vec<PeriodRow>,
    latest_xbrl: Option<&ParsedXbrlDocument>,
) -> Vec<CashFlowPeriod> {
    rows.into_iter()
        .enumerate()
        .map(|(index, row)| {
            let latest_xbrl = (index == 0).then_some(latest_xbrl).flatten();
            let capex = value_for_period(facts, &row, CAPEX_CONCEPTS, None, latest_xbrl);
            let operating_cash_flow =
                value_for_period(facts, &row, CFO_CONCEPTS, None, latest_xbrl);
            CashFlowPeriod {
                label: row.label.clone(),
                fiscal_year: row.fiscal_year.clone(),
                fiscal_period: row.fiscal_period.clone(),
                period_start: row.period_start.clone(),
                period_end: row.period_end.clone(),
                filed_date: row.filed_date.clone(),
                form: row.form.clone(),
                net_income: value_for_period(
                    facts,
                    &row,
                    NET_INCOME_CF_CONCEPTS,
                    None,
                    latest_xbrl,
                ),
                depreciation_and_amortization: value_for_period(
                    facts,
                    &row,
                    DEPRECIATION_CONCEPTS,
                    None,
                    latest_xbrl,
                ),
                stock_based_compensation: value_for_period(
                    facts,
                    &row,
                    STOCK_BASED_COMPENSATION_CONCEPTS,
                    None,
                    latest_xbrl,
                ),
                deferred_income_taxes: value_for_period(
                    facts,
                    &row,
                    DEFERRED_INCOME_TAXES_CONCEPTS,
                    None,
                    latest_xbrl,
                ),
                impairment_charges: value_for_period(
                    facts,
                    &row,
                    IMPAIRMENT_CHARGES_CONCEPTS,
                    None,
                    latest_xbrl,
                ),
                loss_gain_on_sale_of_assets: value_for_period(
                    facts,
                    &row,
                    LOSS_GAIN_ON_SALE_OF_ASSETS_CONCEPTS,
                    None,
                    latest_xbrl,
                ),
                other_non_cash_items: value_for_period(
                    facts,
                    &row,
                    OTHER_NON_CASH_ITEMS_CONCEPTS,
                    None,
                    latest_xbrl,
                ),
                accounts_receivable: value_for_period(
                    facts,
                    &row,
                    CASH_FLOW_ACCOUNTS_RECEIVABLE_CONCEPTS,
                    None,
                    latest_xbrl,
                ),
                inventory: value_for_period(
                    facts,
                    &row,
                    CASH_FLOW_INVENTORY_CONCEPTS,
                    None,
                    latest_xbrl,
                ),
                prepaid_expenses_and_other_current_assets: value_for_period(
                    facts,
                    &row,
                    PREPAIDS_AND_OTHER_CURRENT_ASSETS_CONCEPTS,
                    None,
                    latest_xbrl,
                ),
                accounts_payable: value_for_period(
                    facts,
                    &row,
                    CASH_FLOW_ACCOUNTS_PAYABLE_CONCEPTS,
                    None,
                    latest_xbrl,
                ),
                accrued_expenses: value_for_period(
                    facts,
                    &row,
                    ACCRUED_EXPENSES_CONCEPTS,
                    None,
                    latest_xbrl,
                ),
                deferred_revenue: value_for_period(
                    facts,
                    &row,
                    DEFERRED_REVENUE_CONCEPTS,
                    None,
                    latest_xbrl,
                ),
                other_working_capital_changes: value_for_period(
                    facts,
                    &row,
                    OTHER_WORKING_CAPITAL_CHANGES_CONCEPTS,
                    None,
                    latest_xbrl,
                ),
                change_in_operating_assets: value_for_period(
                    facts,
                    &row,
                    CHANGE_IN_OPERATING_ASSETS_CONCEPTS,
                    None,
                    latest_xbrl,
                ),
                change_in_operating_liabilities: value_for_period(
                    facts,
                    &row,
                    CHANGE_IN_OPERATING_LIABILITIES_CONCEPTS,
                    None,
                    latest_xbrl,
                ),
                other_operating_adjustments: value_for_period(
                    facts,
                    &row,
                    OTHER_OPERATING_ADJUSTMENTS_CONCEPTS,
                    None,
                    latest_xbrl,
                ),
                operating_cash_flow,
                capex,
                proceeds_from_sale_of_ppe: value_for_period(
                    facts,
                    &row,
                    PROCEEDS_FROM_SALE_OF_PPE_CONCEPTS,
                    None,
                    latest_xbrl,
                ),
                purchases_of_investments: value_for_period(
                    facts,
                    &row,
                    PURCHASES_OF_INVESTMENTS_CONCEPTS,
                    None,
                    latest_xbrl,
                ),
                sales_of_investments: value_for_period(
                    facts,
                    &row,
                    SALES_OF_INVESTMENTS_CONCEPTS,
                    None,
                    latest_xbrl,
                ),
                acquisitions: value_for_period(
                    facts,
                    &row,
                    ACQUISITIONS_CONCEPTS,
                    None,
                    latest_xbrl,
                ),
                other_investing_activities: value_for_period(
                    facts,
                    &row,
                    OTHER_INVESTING_CONCEPTS,
                    None,
                    latest_xbrl,
                ),
                investing_cash_flow: value_for_period(facts, &row, CFI_CONCEPTS, None, latest_xbrl),
                debt_issuance: value_for_period(
                    facts,
                    &row,
                    DEBT_ISSUANCE_CONCEPTS,
                    None,
                    latest_xbrl,
                ),
                debt_repayment: value_for_period(
                    facts,
                    &row,
                    DEBT_REPAYMENT_CONCEPTS,
                    None,
                    latest_xbrl,
                ),
                equity_issuance: value_for_period(
                    facts,
                    &row,
                    EQUITY_ISSUANCE_CONCEPTS,
                    None,
                    latest_xbrl,
                ),
                stock_repurchases: value_for_period(
                    facts,
                    &row,
                    STOCK_REPURCHASES_CONCEPTS,
                    None,
                    latest_xbrl,
                ),
                dividends_paid: value_for_period(
                    facts,
                    &row,
                    DIVIDENDS_PAID_CONCEPTS,
                    None,
                    latest_xbrl,
                ),
                finance_lease_obligations: value_for_period(
                    facts,
                    &row,
                    FINANCE_LEASE_OBLIGATIONS_CONCEPTS,
                    None,
                    latest_xbrl,
                ),
                other_financing_activities: value_for_period(
                    facts,
                    &row,
                    OTHER_FINANCING_CONCEPTS,
                    None,
                    latest_xbrl,
                ),
                financing_cash_flow: value_for_period(facts, &row, CFF_CONCEPTS, None, latest_xbrl),
                free_cash_flow: match (operating_cash_flow, capex) {
                    (Some(cfo), Some(capex)) => Some(cfo - capex.abs()),
                    _ => None,
                },
                exchange_rate_effects: value_for_period(
                    facts,
                    &row,
                    EXCHANGE_RATE_EFFECTS_CONCEPTS,
                    None,
                    latest_xbrl,
                ),
                net_increase_decrease_cash: value_for_period(
                    facts,
                    &row,
                    NET_INCREASE_DECREASE_CASH_CONCEPTS,
                    None,
                    latest_xbrl,
                ),
                beginning_cash: value_for_period(
                    facts,
                    &row,
                    BEGINNING_CASH_CONCEPTS,
                    None,
                    latest_xbrl,
                ),
                ending_cash: value_for_period(facts, &row, ENDING_CASH_CONCEPTS, None, latest_xbrl),
                cash_paid_interest: value_for_period(
                    facts,
                    &row,
                    CASH_PAID_INTEREST_CONCEPTS,
                    None,
                    latest_xbrl,
                ),
                cash_paid_income_taxes: value_for_period(
                    facts,
                    &row,
                    CASH_PAID_INCOME_TAXES_CONCEPTS,
                    None,
                    latest_xbrl,
                ),
            }
        })
        .collect()
}

pub(crate) fn build_dividend_events(
    facts: &[NormalizedFact],
    latest_xbrl: Option<&ParsedXbrlDocument>,
) -> Vec<DividendEvent> {
    let rows = build_period_rows(facts, Frequency::Quarterly, DIVIDEND_PER_SHARE_CONCEPTS, 8);

    let mut events = rows
        .into_iter()
        .enumerate()
        .map(|(index, row)| {
            let latest_xbrl = (index == 0).then_some(latest_xbrl).flatten();
            DividendEvent {
                end_date: row.period_end.clone(),
                filed_date: row.filed_date.clone(),
                form: row.form.clone(),
                frequency_guess: "unknown".to_string(),
                dividend_per_share: value_for_period(
                    facts,
                    &row,
                    DIVIDEND_PER_SHARE_CONCEPTS,
                    None,
                    latest_xbrl,
                ),
                total_cash_dividends: value_for_period(
                    facts,
                    &row,
                    DIVIDEND_CASH_CONCEPTS,
                    None,
                    latest_xbrl,
                ),
            }
        })
        .collect::<Vec<_>>();

    for index in 0..events.len() {
        let frequency_guess = classify_dividend_frequency(
            events.get(index).map(|value| value.end_date.as_str()),
            events.get(index + 1).map(|value| value.end_date.as_str()),
        );
        if let Some(event) = events.get_mut(index) {
            event.frequency_guess = frequency_guess;
        }
    }

    events
}

pub(crate) fn default_earnings_period_limit(frequency: Frequency) -> usize {
    match frequency {
        Frequency::Annual => DEFAULT_ANNUAL_EARNINGS_PERIODS,
        Frequency::Quarterly => DEFAULT_QUARTERLY_EARNINGS_PERIODS,
    }
}

pub(crate) fn build_source_status(
    latest_xbrl: Result<Option<&ParsedXbrlDocument>, &EdgarLookupError>,
) -> SourceStatus {
    match latest_xbrl {
        Ok(Some(_)) => SourceStatus {
            companyfacts_used: true,
            degraded_reason: None,
        },
        Ok(None) => SourceStatus {
            companyfacts_used: true,
            degraded_reason: Some("Latest filing XBRL instance was unavailable.".to_string()),
        },
        Err(error) => SourceStatus {
            companyfacts_used: true,
            degraded_reason: Some(error.to_string()),
        },
    }
}

pub(crate) fn company_name<'a>(
    resolved_company: &'a ResolvedCompany,
    filings_name: Option<&'a str>,
    companyfacts_name: Option<&'a str>,
) -> String {
    filings_name
        .filter(|value| !value.trim().is_empty())
        .or(companyfacts_name.filter(|value| !value.trim().is_empty()))
        .unwrap_or(&resolved_company.company_name)
        .to_string()
}

pub(crate) fn limit_dividend_ttm(events: &[DividendEvent]) -> (Option<f64>, Option<f64>) {
    let total_per_share = events
        .iter()
        .take(4)
        .filter_map(|event| event.dividend_per_share)
        .reduce(|acc, value| acc + value);
    let total_cash = events
        .iter()
        .take(4)
        .filter_map(|event| event.total_cash_dividends)
        .reduce(|acc, value| acc + value);

    (total_per_share, total_cash)
}

fn flatten_concepts(company_facts: &CompanyFactsResponse) -> Vec<FlattenedConcept<'_>> {
    let mut concepts = Vec::new();
    concepts.extend(flatten_taxonomy("us-gaap", &company_facts.facts.us_gaap));
    concepts.extend(flatten_taxonomy(
        "ifrs-full",
        &company_facts.facts.ifrs_full,
    ));
    concepts.extend(flatten_taxonomy("dei", &company_facts.facts.dei));
    concepts
}

fn flatten_taxonomy<'a>(
    taxonomy: &'static str,
    facts: &'a HashMap<String, CompanyConceptFacts>,
) -> Vec<FlattenedConcept<'a>> {
    facts
        .iter()
        .map(|(concept_name, concept)| FlattenedConcept {
            taxonomy,
            concept_name: concept_name.clone(),
            units: &concept.units,
        })
        .collect()
}

struct FlattenedConcept<'a> {
    taxonomy: &'static str,
    concept_name: String,
    units: &'a HashMap<String, Vec<CompanyFactRecord>>,
}

fn classify_unit(unit: &str) -> UnitFamily {
    let normalized = unit.to_ascii_uppercase();
    if extract_companyfacts_currency_code(unit).is_some()
        && (normalized.contains("/SHARE") || normalized.contains("-PER-SHARE"))
    {
        UnitFamily::CurrencyPerShare
    } else if normalized == "SHARES" || normalized.ends_with(":SHARES") {
        UnitFamily::Shares
    } else if normalized == "PURE" {
        UnitFamily::Pure
    } else {
        UnitFamily::Currency
    }
}

pub(crate) fn extract_companyfacts_currency_code(unit: &str) -> Option<String> {
    let candidate = unit.trim().split('/').next().unwrap_or(unit.trim());
    normalize_currency_code(candidate)
}

pub(crate) fn normalize_currency_code(value: &str) -> Option<String> {
    let candidate = value.trim().rsplit(':').next().unwrap_or(value.trim());
    (candidate.len() == 3
        && candidate
            .chars()
            .all(|character| character.is_ascii_alphabetic()))
    .then(|| candidate.to_ascii_uppercase())
}

pub(crate) fn matches_requested_currency(
    unit_family: UnitFamily,
    currency_code: Option<&str>,
    selected_currency: Option<&str>,
) -> bool {
    if !unit_family.uses_currency() {
        return true;
    }

    match selected_currency {
        Some(selected_currency) => currency_code.is_some_and(|code| code == selected_currency),
        None => true,
    }
}

fn json_number(value: &serde_json::Value) -> Option<f64> {
    match value {
        serde_json::Value::Number(number) => number.as_f64(),
        serde_json::Value::String(string) => string.parse::<f64>().ok(),
        _ => None,
    }
}

fn fiscal_year_to_string(value: &serde_json::Value) -> String {
    match value {
        serde_json::Value::Number(number) => number.to_string(),
        serde_json::Value::String(value) => value.clone(),
        _ => String::new(),
    }
}

fn matches_frequency_form(form: &str, frequency: Frequency) -> bool {
    match frequency {
        Frequency::Annual => ANNUAL_FORMS.contains(&form),
        Frequency::Quarterly => QUARTERLY_FORMS.contains(&form),
    }
}

fn compare_filing_priority(left: &FilingRef, right: &FilingRef) -> std::cmp::Ordering {
    left.filing_date
        .cmp(&right.filing_date)
        .then_with(|| amended_rank(&right.form).cmp(&amended_rank(&left.form)))
}

fn amended_rank(form: &str) -> usize {
    usize::from(form.contains("/A"))
}

fn build_period_rows(
    facts: &[NormalizedFact],
    frequency: Frequency,
    anchor_concepts: &[ConceptCandidate],
    period_limit: usize,
) -> Vec<PeriodRow> {
    let anchored = facts
        .iter()
        .filter(|fact| is_fact_for_frequency(fact, frequency))
        .filter(|fact| concept_match(anchor_concepts, fact))
        .map(|fact| PeriodRow::from_fact(fact, period_label(fact, frequency)))
        .collect::<Vec<_>>();

    if !anchored.is_empty() {
        return dedupe_rows(anchored, frequency, period_limit);
    }

    dedupe_rows(
        facts
            .iter()
            .filter(|fact| is_fact_for_frequency(fact, frequency))
            .map(|fact| PeriodRow::from_fact(fact, period_label(fact, frequency)))
            .collect(),
        frequency,
        period_limit,
    )
}

fn dedupe_rows(rows: Vec<PeriodRow>, frequency: Frequency, period_limit: usize) -> Vec<PeriodRow> {
    let mut grouped = BTreeMap::<String, PeriodRow>::new();

    for row in rows {
        grouped
            .entry(period_identity_key(&row, frequency))
            .and_modify(|existing| {
                if row.filed_date > existing.filed_date {
                    *existing = row.clone();
                }
            })
            .or_insert(row);
    }

    let mut values = grouped.into_values().collect::<Vec<_>>();
    values.sort_by(|left, right| right.period_end.cmp(&left.period_end));
    values.truncate(period_limit);
    values
}

pub(crate) fn is_fact_for_frequency(fact: &NormalizedFact, frequency: Frequency) -> bool {
    if !matches_frequency_form(&fact.form, frequency) {
        return false;
    }

    match frequency {
        Frequency::Annual => {
            fact.fiscal_period.as_deref() == Some("FY")
                || duration_days(fact).is_none_or(|days| days >= 250)
                || fact.period_start.is_none()
        }
        Frequency::Quarterly => {
            fact.fiscal_period
                .as_deref()
                .is_some_and(|period| period.starts_with('Q'))
                || duration_days(fact).is_none_or(|days| days <= 120)
                || fact.period_start.is_none()
        }
    }
}

fn duration_days(fact: &NormalizedFact) -> Option<i64> {
    let start = fact.period_start.as_ref()?;
    let start = NaiveDate::parse_from_str(start, "%Y-%m-%d").ok()?;
    let end = NaiveDate::parse_from_str(&fact.period_end, "%Y-%m-%d").ok()?;
    Some((end - start).num_days().abs())
}

fn concept_match(candidates: &[ConceptCandidate], fact: &NormalizedFact) -> bool {
    candidates.iter().any(|candidate| {
        candidate.prefix == fact.taxonomy
            && candidate.local_name == fact.concept
            && candidate.unit_family == fact.unit_family
    })
}

pub(crate) fn value_for_period(
    facts: &[NormalizedFact],
    row: &PeriodRow,
    concepts: &[ConceptCandidate],
    selected_currency: Option<&str>,
    latest_xbrl: Option<&ParsedXbrlDocument>,
) -> Option<f64> {
    let mut best = facts
        .iter()
        .filter(|fact| concept_match(concepts, fact))
        .filter(|fact| {
            matches_requested_currency(
                fact.unit_family,
                fact.currency_code.as_deref(),
                selected_currency,
            )
        })
        .filter(|fact| fact_matches_period_row(fact, row))
        .max_by(|left, right| {
            (left.accession_number.as_deref() == row.accession_number.as_deref())
                .cmp(&(right.accession_number.as_deref() == row.accession_number.as_deref()))
                .then_with(|| left.filed.cmp(&right.filed))
                .then_with(|| {
                    period_start_specificity(
                        left.period_start.as_deref(),
                        row.period_start.as_deref(),
                    )
                    .cmp(&period_start_specificity(
                        right.period_start.as_deref(),
                        row.period_start.as_deref(),
                    ))
                })
                .then_with(|| {
                    optional_match_score(
                        left.fiscal_period.as_deref(),
                        row.fiscal_period.as_deref(),
                    )
                    .cmp(&optional_match_score(
                        right.fiscal_period.as_deref(),
                        row.fiscal_period.as_deref(),
                    ))
                })
                .then_with(|| {
                    optional_match_score(left.fiscal_year.as_deref(), row.fiscal_year.as_deref())
                        .cmp(&optional_match_score(
                            right.fiscal_year.as_deref(),
                            row.fiscal_year.as_deref(),
                        ))
                })
                .then_with(|| left.filed.cmp(&right.filed))
                .then_with(|| left.accession_number.cmp(&right.accession_number))
        })
        .map(|fact| fact.value);

    if let Some(latest_xbrl) = latest_xbrl {
        if let Some(value) = overlay_xbrl_value(latest_xbrl, row, concepts, selected_currency) {
            best = Some(value);
        }
    }

    best
}

fn overlay_xbrl_value(
    latest_xbrl: &ParsedXbrlDocument,
    row: &PeriodRow,
    concepts: &[ConceptCandidate],
    selected_currency: Option<&str>,
) -> Option<f64> {
    latest_xbrl
        .facts
        .iter()
        .filter(|fact| !fact.has_dimensions)
        .filter(|fact| fact.period_end.as_deref() == Some(row.period_end.as_str()))
        .find(|fact| {
            concepts.iter().any(|candidate| {
                fact.unit_family
                    .is_none_or(|unit_family| unit_family == candidate.unit_family)
                    && matches_requested_currency(
                        candidate.unit_family,
                        fact.currency_code.as_deref(),
                        selected_currency,
                    )
                    && (fact.concept.eq_ignore_ascii_case(&format!(
                        "{}:{}",
                        candidate.prefix, candidate.local_name
                    )) || fact.concept.eq_ignore_ascii_case(candidate.local_name))
            })
        })
        .map(|fact| fact.value)
}

fn classify_dividend_frequency(current_end: Option<&str>, previous_end: Option<&str>) -> String {
    let Some(current_end) = current_end else {
        return "unknown".to_string();
    };
    let Some(previous_end) = previous_end else {
        return "unknown".to_string();
    };
    let Ok(current) = NaiveDate::parse_from_str(current_end, "%Y-%m-%d") else {
        return "unknown".to_string();
    };
    let Ok(previous) = NaiveDate::parse_from_str(previous_end, "%Y-%m-%d") else {
        return "unknown".to_string();
    };
    let spacing = (current - previous).num_days().abs();
    if spacing <= 120 {
        "quarterly".to_string()
    } else if spacing <= 220 {
        "semiannual".to_string()
    } else if spacing <= 420 {
        "annual".to_string()
    } else {
        "special".to_string()
    }
}

fn period_identity_key(row: &PeriodRow, frequency: Frequency) -> String {
    match frequency {
        Frequency::Annual => format!(
            "{}:{}",
            row.period_start.clone().unwrap_or_default(),
            row.period_end
        ),
        Frequency::Quarterly => format!(
            "{}:{}:{}",
            row.period_start.clone().unwrap_or_default(),
            row.period_end,
            row.fiscal_period.clone().unwrap_or_default()
        ),
    }
}

fn period_label(fact: &NormalizedFact, frequency: Frequency) -> String {
    match frequency {
        Frequency::Annual => period_end_year(&fact.period_end)
            .or_else(|| fact.fiscal_year.clone())
            .unwrap_or_else(|| fact.period_end.clone()),
        Frequency::Quarterly => match (
            period_end_year(&fact.period_end),
            fact.fiscal_period.as_deref(),
        ) {
            (Some(year), Some(period)) if period != "FY" => format!("{year} {period}"),
            (_, Some(year)) => year.to_string(),
            _ => fact.period_end.clone(),
        },
    }
}

pub(crate) fn period_end_year(period_end: &str) -> Option<String> {
    period_end
        .split('-')
        .next()
        .filter(|year| year.len() == 4)
        .map(ToString::to_string)
}

pub(crate) fn fact_matches_period_row(fact: &NormalizedFact, row: &PeriodRow) -> bool {
    if fact.period_end != row.period_end {
        return false;
    }

    if let Some(row_start) = row.period_start.as_deref() {
        return fact.period_start.as_deref() == Some(row_start);
    }

    true
}

fn period_start_specificity(candidate: Option<&str>, row: Option<&str>) -> usize {
    usize::from(candidate == row)
}

fn optional_match_score(candidate: Option<&str>, row: Option<&str>) -> usize {
    usize::from(candidate == row)
}

#[cfg(test)]
mod tests {
    use super::{
        build_statement_rows, extract_companyfacts_currency_code, NormalizedFact, REVENUE_CONCEPTS,
    };
    use crate::terminal::sec_edgar::types::UnitFamily;
    use crate::terminal::Frequency;

    #[test]
    fn extracts_companyfacts_currency_codes_from_currency_units() {
        assert_eq!(
            extract_companyfacts_currency_code("USD"),
            Some("USD".to_string())
        );
        assert_eq!(
            extract_companyfacts_currency_code("EUR"),
            Some("EUR".to_string())
        );
    }

    #[test]
    fn extracts_companyfacts_currency_codes_from_per_share_units() {
        assert_eq!(
            extract_companyfacts_currency_code("USD/shares"),
            Some("USD".to_string())
        );
        assert_eq!(
            extract_companyfacts_currency_code("EUR/shares"),
            Some("EUR".to_string())
        );
    }

    #[test]
    fn build_statement_rows_dedupes_annual_periods_by_reporting_window() {
        let facts = vec![
            NormalizedFact {
                taxonomy: "us-gaap",
                concept: REVENUE_CONCEPTS[0].local_name.to_string(),
                unit_family: UnitFamily::Currency,
                currency_code: Some("USD".to_string()),
                value: 90.0,
                filed: "2025-04-08".to_string(),
                form: "20-F".to_string(),
                fiscal_year: Some("2024".to_string()),
                fiscal_period: Some("FY".to_string()),
                period_start: Some("2024-01-01".to_string()),
                period_end: "2024-12-31".to_string(),
                accession_number: Some("0001".to_string()),
            },
            NormalizedFact {
                taxonomy: "us-gaap",
                concept: REVENUE_CONCEPTS[0].local_name.to_string(),
                unit_family: UnitFamily::Currency,
                currency_code: Some("USD".to_string()),
                value: 95.0,
                filed: "2026-04-10".to_string(),
                form: "20-F".to_string(),
                fiscal_year: Some("2025".to_string()),
                fiscal_period: Some("FY".to_string()),
                period_start: Some("2024-01-01".to_string()),
                period_end: "2024-12-31".to_string(),
                accession_number: Some("0002".to_string()),
            },
            NormalizedFact {
                taxonomy: "us-gaap",
                concept: REVENUE_CONCEPTS[0].local_name.to_string(),
                unit_family: UnitFamily::Currency,
                currency_code: Some("USD".to_string()),
                value: 110.0,
                filed: "2026-04-10".to_string(),
                form: "20-F".to_string(),
                fiscal_year: Some("2025".to_string()),
                fiscal_period: Some("FY".to_string()),
                period_start: Some("2025-01-01".to_string()),
                period_end: "2025-12-31".to_string(),
                accession_number: Some("0002".to_string()),
            },
        ];

        let rows = build_statement_rows(&facts, Frequency::Annual, 4);

        assert_eq!(rows.len(), 2);
        assert_eq!(rows[0].label, "2025");
        assert_eq!(rows[0].period_end, "2025-12-31");
        assert_eq!(rows[1].label, "2024");
        assert_eq!(rows[1].period_end, "2024-12-31");
        assert_eq!(rows[1].accession_number.as_deref(), Some("0002"));
    }
}
