//! Custom XBRL concept mapping types.
//!
//! This module defines the data structures for custom XBRL concept mappings
//! that can fill gaps in the standardized data loader.

use serde::{Deserialize, Serialize};

use crate::mappings::formula::ast::Expr;
use crate::terminal::sec_edgar::UnitFamily;

/// Mapping type: concept-based or formula-based.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MappingKind {
    /// Maps to a custom XBRL concept
    Concept(CustomConcept),
    /// Calculates using a formula expression
    Formula(FormulaMapping),
}

/// A formula-based mapping that calculates values from other fields.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FormulaMapping {
    /// The formula expression string (e.g., "($GrossProfit / $TotalRevenues) * 100")
    pub expression: String,
    /// Cached parsed AST (not serialized)
    #[serde(skip)]
    pub cached_ast: Option<Expr>,
}

/// A custom XBRL concept mapping that overrides or extends standard concept mappings.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CustomConceptMapping {
    /// Unique identifier for this mapping
    pub id: String,
    /// The target field this mapping applies to
    #[serde(alias = "target_field")]
    pub target_field: TargetField,
    /// The mapping kind (concept or formula)
    #[serde(alias = "custom_concept")]
    pub mapping_kind: MappingKind,
    /// Scope of when this mapping applies (ticker, period)
    pub scope: MappingScope,
    /// Whether this mapping is currently enabled
    pub enabled: bool,
    /// Optional user notes about why this mapping exists
    pub notes: Option<String>,
}

impl<'de> Deserialize<'de> for CustomConceptMapping {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        use serde::de::{Error, MapAccess, Visitor};

        struct MappingVisitor;

        impl<'de> Visitor<'de> for MappingVisitor {
            type Value = CustomConceptMapping;

            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
                formatter.write_str("a custom concept mapping")
            }

            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
            where
                A: MapAccess<'de>,
            {
                let mut id = None;
                let mut target_field = None;
                let mut custom_concept = None;
                let mut mapping_kind = None;
                let mut scope = MappingScope::default();
                let mut enabled = true;
                let mut notes = None;

                while let Some(key) = map.next_key()? {
                    match key {
                        "id" => {
                            if id.is_some() {
                                return Err(Error::duplicate_field("id"));
                            }
                            id = Some(map.next_value()?);
                        }
                        "targetField" | "target_field" => {
                            if target_field.is_some() {
                                return Err(Error::duplicate_field("targetField"));
                            }
                            target_field = Some(map.next_value()?);
                        }
                        "customConcept" | "custom_concept" => {
                            if custom_concept.is_some() {
                                return Err(Error::duplicate_field("customConcept"));
                            }
                            custom_concept = Some(map.next_value()?);
                        }
                        "mappingKind" | "mapping_kind" => {
                            if mapping_kind.is_some() {
                                return Err(Error::duplicate_field("mappingKind"));
                            }
                            mapping_kind = Some(map.next_value()?);
                        }
                        "scope" => {
                            scope = map.next_value()?;
                        }
                        "enabled" => {
                            enabled = map.next_value()?;
                        }
                        "notes" => {
                            notes = map.next_value()?;
                        }
                        _ => {
                            map.next_value::<serde::de::IgnoredAny>()?;
                        }
                    }
                }

                let id = id.ok_or_else(|| Error::missing_field("id"))?;
                let target_field =
                    target_field.ok_or_else(|| Error::missing_field("targetField"))?;

                // Use mappingKind if provided, otherwise fall back to customConcept
                let mapping_kind = match mapping_kind {
                    Some(kind) => kind,
                    None => {
                        let custom_concept = custom_concept
                            .ok_or_else(|| Error::missing_field("customConcept or mappingKind"))?;
                        MappingKind::Concept(custom_concept)
                    }
                };

                Ok(CustomConceptMapping {
                    id,
                    target_field,
                    mapping_kind,
                    scope,
                    enabled,
                    notes,
                })
            }
        }

        deserializer.deserialize_any(MappingVisitor)
    }
}

impl CustomConceptMapping {
    /// Get the custom concept if this is a concept mapping.
    pub fn custom_concept(&self) -> Option<&CustomConcept> {
        match &self.mapping_kind {
            MappingKind::Concept(concept) => Some(concept),
            MappingKind::Formula(_) => None,
        }
    }

    /// Get the formula mapping if this is a formula mapping.
    pub fn formula(&self) -> Option<&FormulaMapping> {
        match &self.mapping_kind {
            MappingKind::Concept(_) => None,
            MappingKind::Formula(formula) => Some(formula),
        }
    }

    /// Check if this is a formula mapping.
    #[allow(dead_code)]
    pub fn is_formula(&self) -> bool {
        matches!(self.mapping_kind, MappingKind::Formula(_))
    }
}

/// Request payload for creating a new custom mapping.
#[derive(Debug, Clone)]
pub struct CreateCustomConceptMappingRequest {
    /// The target field this mapping applies to
    pub target_field: TargetField,
    /// The mapping kind (concept or formula)
    pub mapping_kind: MappingKind,
    /// Scope of when this mapping applies (ticker, period)
    pub scope: MappingScope,
    /// Optional user notes about why this mapping exists
    pub notes: Option<String>,
}

impl<'de> Deserialize<'de> for CreateCustomConceptMappingRequest {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        use serde::de::{Error, MapAccess, Visitor};

        struct CreateMappingVisitor;

        impl<'de> Visitor<'de> for CreateMappingVisitor {
            type Value = CreateCustomConceptMappingRequest;

            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
                formatter.write_str("a mapping request")
            }

            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
            where
                A: MapAccess<'de>,
            {
                let mut target_field = None;
                let mut custom_concept = None;
                let mut mapping_kind = None;
                let mut scope = MappingScope::default();
                let mut notes = None;

                while let Some(key) = map.next_key()? {
                    match key {
                        "targetField" | "target_field" => {
                            if target_field.is_some() {
                                return Err(Error::duplicate_field("targetField"));
                            }
                            target_field = Some(map.next_value()?);
                        }
                        "customConcept" | "custom_concept" => {
                            if custom_concept.is_some() {
                                return Err(Error::duplicate_field("customConcept"));
                            }
                            custom_concept = Some(map.next_value()?);
                        }
                        "mappingKind" | "mapping_kind" => {
                            if mapping_kind.is_some() {
                                return Err(Error::duplicate_field("mappingKind"));
                            }
                            mapping_kind = Some(map.next_value()?);
                        }
                        "scope" => {
                            scope = map.next_value()?;
                        }
                        "notes" => {
                            notes = map.next_value()?;
                        }
                        _ => {
                            return Err(Error::unknown_field(
                                key,
                                &[
                                    "targetField",
                                    "target_field",
                                    "customConcept",
                                    "custom_concept",
                                    "mappingKind",
                                    "mapping_kind",
                                    "scope",
                                    "notes",
                                ],
                            ));
                        }
                    }
                }

                let target_field =
                    target_field.ok_or_else(|| Error::missing_field("targetField"))?;

                // Use mappingKind if provided, otherwise fall back to customConcept
                let mapping_kind = match mapping_kind {
                    Some(kind) => kind,
                    None => {
                        let custom_concept = custom_concept
                            .ok_or_else(|| Error::missing_field("customConcept or mappingKind"))?;
                        MappingKind::Concept(custom_concept)
                    }
                };

                Ok(CreateCustomConceptMappingRequest {
                    target_field,
                    mapping_kind,
                    scope,
                    notes,
                })
            }
        }

        deserializer.deserialize_any(CreateMappingVisitor)
    }
}

impl CustomConceptMapping {
    #[cfg(test)]
    pub(crate) fn test_mapping(
        target_field: TargetField,
        custom_concept: CustomConcept,
        scope: MappingScope,
        notes: Option<String>,
    ) -> Self {
        Self {
            id: uuid::Uuid::new_v4().to_string(),
            target_field,
            mapping_kind: MappingKind::Concept(custom_concept),
            scope,
            enabled: true,
            notes,
        }
    }
}

/// A custom XBRL concept definition.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CustomConcept {
    /// The taxonomy namespace (e.g., "us-gaap", "ifrs-full", "xyz-custom")
    pub taxonomy: String,
    /// The concept name within the taxonomy
    pub concept: String,
    /// The unit family for this concept
    #[serde(alias = "unit_family")]
    pub unit_family: UnitFamily,
}

/// The target financial field for a custom mapping.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TargetField {
    #[serde(
        alias = "totalRevenues",
        alias = "TotalRevenues",
        alias = "revenue",
        alias = "Revenue"
    )]
    TotalRevenues,
    #[serde(alias = "costOfSales", alias = "CostOfSales")]
    CostOfSales,
    #[serde(alias = "grossProfit", alias = "GrossProfit")]
    GrossProfit,
    #[serde(alias = "cashAndEquivalents", alias = "CashAndEquivalents")]
    CashAndEquivalents,
    #[serde(alias = "shortTermInvestments", alias = "ShortTermInvestments")]
    ShortTermInvestments,
    #[serde(alias = "accountsReceivableNet", alias = "AccountsReceivableNet")]
    AccountsReceivableNet,
    #[serde(alias = "inventory", alias = "Inventory")]
    Inventory,
    #[serde(alias = "otherCurrentAssets", alias = "OtherCurrentAssets")]
    OtherCurrentAssets,
    #[serde(alias = "totalCurrentAssets", alias = "TotalCurrentAssets")]
    TotalCurrentAssets,
    #[serde(
        alias = "propertyPlantAndEquipmentNet",
        alias = "PropertyPlantAndEquipmentNet"
    )]
    PropertyPlantAndEquipmentNet,
    #[serde(alias = "goodwill", alias = "Goodwill")]
    Goodwill,
    #[serde(alias = "intangibleAssets", alias = "IntangibleAssets")]
    IntangibleAssets,
    #[serde(alias = "otherNonCurrentAssets", alias = "OtherNonCurrentAssets")]
    OtherNonCurrentAssets,
    #[serde(alias = "totalAssets", alias = "TotalAssets")]
    TotalAssets,
    #[serde(alias = "accountsPayable", alias = "AccountsPayable")]
    AccountsPayable,
    #[serde(
        alias = "accruedExpensesAndOtherCurrentLiabilities",
        alias = "AccruedExpensesAndOtherCurrentLiabilities"
    )]
    AccruedExpensesAndOtherCurrentLiabilities,
    #[serde(alias = "shortTermDebt", alias = "ShortTermDebt")]
    ShortTermDebt,
    #[serde(alias = "totalCurrentLiabilities", alias = "TotalCurrentLiabilities")]
    TotalCurrentLiabilities,
    #[serde(alias = "longTermDebt", alias = "LongTermDebt")]
    LongTermDebt,
    #[serde(
        alias = "otherNonCurrentLiabilities",
        alias = "OtherNonCurrentLiabilities"
    )]
    OtherNonCurrentLiabilities,
    #[serde(alias = "totalLiabilities", alias = "TotalLiabilities")]
    TotalLiabilities,
    #[serde(alias = "retainedEarnings", alias = "RetainedEarnings")]
    RetainedEarnings,
    #[serde(
        alias = "accumulatedOtherComprehensiveIncomeLoss",
        alias = "AccumulatedOtherComprehensiveIncomeLoss"
    )]
    AccumulatedOtherComprehensiveIncomeLoss,
    #[serde(alias = "totalEquity", alias = "TotalEquity")]
    TotalEquity,
    #[serde(
        alias = "sellingGeneralAndAdministrative",
        alias = "SellingGeneralAndAdministrative",
        alias = "sellingGeneralAndAdministrativeExpenses",
        alias = "SellingGeneralAndAdministrativeExpenses"
    )]
    SellingGeneralAndAdministrative,
    #[serde(
        alias = "researchAndDevelopmentExpenses",
        alias = "ResearchAndDevelopmentExpenses",
        alias = "researchAndDevelopment",
        alias = "ResearchAndDevelopment"
    )]
    ResearchAndDevelopment,
    #[serde(alias = "otherOperatingExpenses", alias = "OtherOperatingExpenses")]
    OtherOperatingExpenses,
    #[serde(alias = "operatingProfit", alias = "OperatingProfit")]
    OperatingProfit,
    #[serde(
        alias = "consolidated_net_income",
        alias = "consolidatedNetIncome",
        alias = "ConsolidatedNetIncome",
        alias = "netIncome",
        alias = "NetIncome"
    )]
    NetIncome,
    #[serde(
        alias = "netIncomeAttributableToCommonShareholders",
        alias = "NetIncomeAttributableToCommonShareholders"
    )]
    NetIncomeAttributableToCommonShareholders,
    #[serde(alias = "basicEps", alias = "BasicEps")]
    BasicEps,
    #[serde(alias = "dilutedEps", alias = "DilutedEps")]
    DilutedEps,
    #[serde(
        alias = "basicWeightedAverageSharesOutstanding",
        alias = "BasicWeightedAverageSharesOutstanding"
    )]
    BasicWeightedAverageSharesOutstanding,
    #[serde(alias = "totalSharesOutstanding", alias = "TotalSharesOutstanding")]
    TotalSharesOutstanding,
    #[serde(
        alias = "dilutedWeightedAverageSharesOutstanding",
        alias = "DilutedWeightedAverageSharesOutstanding"
    )]
    DilutedWeightedAverageSharesOutstanding,
    #[serde(alias = "nonOperatingIncome", alias = "NonOperatingIncome")]
    NonOperatingIncome,
    #[serde(alias = "totalNonOperatingIncome", alias = "TotalNonOperatingIncome")]
    TotalNonOperatingIncome,
    #[serde(
        alias = "incomeBeforeProvisionForIncomeTaxes",
        alias = "IncomeBeforeProvisionForIncomeTaxes"
    )]
    IncomeBeforeProvisionForIncomeTaxes,
    #[serde(alias = "provisionForIncomeTaxes", alias = "ProvisionForIncomeTaxes")]
    ProvisionForIncomeTaxes,
    #[serde(alias = "effectiveTaxRate", alias = "EffectiveTaxRate")]
    EffectiveTaxRate,
    #[serde(alias = "ebitda", alias = "EBITDA")]
    Ebitda,
    #[serde(alias = "freeCashFlow", alias = "FreeCashFlow")]
    FreeCashFlow,
    #[serde(alias = "capitalExpenditure", alias = "CapitalExpenditure")]
    CapitalExpenditure,
    #[serde(alias = "operatingCashFlow", alias = "OperatingCashFlow")]
    OperatingCashFlow,
    #[serde(alias = "investingCashFlow", alias = "InvestingCashFlow")]
    InvestingCashFlow,
    #[serde(alias = "financingCashFlow", alias = "FinancingCashFlow")]
    FinancingCashFlow,
}

impl std::str::FromStr for TargetField {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "total_revenues" | "revenue" => Ok(TargetField::TotalRevenues),
            "cost_of_sales" => Ok(TargetField::CostOfSales),
            "gross_profit" => Ok(TargetField::GrossProfit),
            "cash_and_equivalents" => Ok(TargetField::CashAndEquivalents),
            "short_term_investments" => Ok(TargetField::ShortTermInvestments),
            "accounts_receivable_net" => Ok(TargetField::AccountsReceivableNet),
            "inventory" => Ok(TargetField::Inventory),
            "other_current_assets" => Ok(TargetField::OtherCurrentAssets),
            "total_current_assets" => Ok(TargetField::TotalCurrentAssets),
            "property_plant_and_equipment_net" => Ok(TargetField::PropertyPlantAndEquipmentNet),
            "goodwill" => Ok(TargetField::Goodwill),
            "intangible_assets" => Ok(TargetField::IntangibleAssets),
            "other_non_current_assets" => Ok(TargetField::OtherNonCurrentAssets),
            "total_assets" => Ok(TargetField::TotalAssets),
            "accounts_payable" => Ok(TargetField::AccountsPayable),
            "accrued_expenses_and_other_current_liabilities" => {
                Ok(TargetField::AccruedExpensesAndOtherCurrentLiabilities)
            }
            "short_term_debt" => Ok(TargetField::ShortTermDebt),
            "total_current_liabilities" => Ok(TargetField::TotalCurrentLiabilities),
            "long_term_debt" => Ok(TargetField::LongTermDebt),
            "other_non_current_liabilities" => Ok(TargetField::OtherNonCurrentLiabilities),
            "total_liabilities" => Ok(TargetField::TotalLiabilities),
            "retained_earnings" => Ok(TargetField::RetainedEarnings),
            "accumulated_other_comprehensive_income_loss" => {
                Ok(TargetField::AccumulatedOtherComprehensiveIncomeLoss)
            }
            "total_equity" => Ok(TargetField::TotalEquity),
            "selling_general_and_administrative" => {
                Ok(TargetField::SellingGeneralAndAdministrative)
            }
            "research_and_development" => Ok(TargetField::ResearchAndDevelopment),
            "other_operating_expenses" => Ok(TargetField::OtherOperatingExpenses),
            "operating_profit" => Ok(TargetField::OperatingProfit),
            "net_income" => Ok(TargetField::NetIncome),
            "net_income_attributable_to_common_shareholders" => {
                Ok(TargetField::NetIncomeAttributableToCommonShareholders)
            }
            "basic_eps" => Ok(TargetField::BasicEps),
            "diluted_eps" => Ok(TargetField::DilutedEps),
            "basic_weighted_average_shares_outstanding" => {
                Ok(TargetField::BasicWeightedAverageSharesOutstanding)
            }
            "total_shares_outstanding" => Ok(TargetField::TotalSharesOutstanding),
            "diluted_weighted_average_shares_outstanding" => {
                Ok(TargetField::DilutedWeightedAverageSharesOutstanding)
            }
            "non_operating_income" => Ok(TargetField::NonOperatingIncome),
            "total_non_operating_income" => Ok(TargetField::TotalNonOperatingIncome),
            "income_before_provision_for_income_taxes" => {
                Ok(TargetField::IncomeBeforeProvisionForIncomeTaxes)
            }
            "provision_for_income_taxes" => Ok(TargetField::ProvisionForIncomeTaxes),
            "effective_tax_rate" => Ok(TargetField::EffectiveTaxRate),
            "ebitda" => Ok(TargetField::Ebitda),
            "free_cash_flow" => Ok(TargetField::FreeCashFlow),
            "capital_expenditure" => Ok(TargetField::CapitalExpenditure),
            "operating_cash_flow" => Ok(TargetField::OperatingCashFlow),
            "investing_cash_flow" => Ok(TargetField::InvestingCashFlow),
            "financing_cash_flow" => Ok(TargetField::FinancingCashFlow),
            _ => Err(format!("Unknown target field: {}", s)),
        }
    }
}

impl TargetField {
    pub fn as_str(&self) -> &'static str {
        match self {
            TargetField::TotalRevenues => "total_revenues",
            TargetField::CostOfSales => "cost_of_sales",
            TargetField::GrossProfit => "gross_profit",
            TargetField::CashAndEquivalents => "cash_and_equivalents",
            TargetField::ShortTermInvestments => "short_term_investments",
            TargetField::AccountsReceivableNet => "accounts_receivable_net",
            TargetField::Inventory => "inventory",
            TargetField::OtherCurrentAssets => "other_current_assets",
            TargetField::TotalCurrentAssets => "total_current_assets",
            TargetField::PropertyPlantAndEquipmentNet => "property_plant_and_equipment_net",
            TargetField::Goodwill => "goodwill",
            TargetField::IntangibleAssets => "intangible_assets",
            TargetField::OtherNonCurrentAssets => "other_non_current_assets",
            TargetField::TotalAssets => "total_assets",
            TargetField::AccountsPayable => "accounts_payable",
            TargetField::AccruedExpensesAndOtherCurrentLiabilities => {
                "accrued_expenses_and_other_current_liabilities"
            }
            TargetField::ShortTermDebt => "short_term_debt",
            TargetField::TotalCurrentLiabilities => "total_current_liabilities",
            TargetField::LongTermDebt => "long_term_debt",
            TargetField::OtherNonCurrentLiabilities => "other_non_current_liabilities",
            TargetField::TotalLiabilities => "total_liabilities",
            TargetField::RetainedEarnings => "retained_earnings",
            TargetField::AccumulatedOtherComprehensiveIncomeLoss => {
                "accumulated_other_comprehensive_income_loss"
            }
            TargetField::TotalEquity => "total_equity",
            TargetField::SellingGeneralAndAdministrative => "selling_general_and_administrative",
            TargetField::ResearchAndDevelopment => "research_and_development",
            TargetField::OtherOperatingExpenses => "other_operating_expenses",
            TargetField::OperatingProfit => "operating_profit",
            TargetField::NetIncome => "net_income",
            TargetField::NetIncomeAttributableToCommonShareholders => {
                "net_income_attributable_to_common_shareholders"
            }
            TargetField::BasicEps => "basic_eps",
            TargetField::DilutedEps => "diluted_eps",
            TargetField::BasicWeightedAverageSharesOutstanding => {
                "basic_weighted_average_shares_outstanding"
            }
            TargetField::TotalSharesOutstanding => "total_shares_outstanding",
            TargetField::DilutedWeightedAverageSharesOutstanding => {
                "diluted_weighted_average_shares_outstanding"
            }
            TargetField::NonOperatingIncome => "non_operating_income",
            TargetField::TotalNonOperatingIncome => "total_non_operating_income",
            TargetField::IncomeBeforeProvisionForIncomeTaxes => {
                "income_before_provision_for_income_taxes"
            }
            TargetField::ProvisionForIncomeTaxes => "provision_for_income_taxes",
            TargetField::EffectiveTaxRate => "effective_tax_rate",
            TargetField::Ebitda => "ebitda",
            TargetField::FreeCashFlow => "free_cash_flow",
            TargetField::CapitalExpenditure => "capital_expenditure",
            TargetField::OperatingCashFlow => "operating_cash_flow",
            TargetField::InvestingCashFlow => "investing_cash_flow",
            TargetField::FinancingCashFlow => "financing_cash_flow",
        }
    }
}

/// The scope of when a custom mapping applies.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MappingScope {
    /// Optional ticker symbol (e.g., "AAPL"). If None, applies globally.
    pub ticker: Option<String>,
    /// Optional period start date (ISO format, e.g., "2023-01-01")
    #[serde(alias = "period_start")]
    pub period_start: Option<String>,
    /// Optional period end date (ISO format, e.g., "2023-12-31")
    #[serde(alias = "period_end")]
    pub period_end: Option<String>,
}

/// The configuration container for all custom mappings.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MappingsConfig {
    /// Configuration version for migrations
    pub version: u64,
    /// ISO timestamp of last update
    #[serde(alias = "last_updated")]
    pub last_updated: String,
    /// All custom mappings
    pub mappings: Vec<CustomConceptMapping>,
}

impl Default for MappingsConfig {
    fn default() -> Self {
        Self {
            version: 1,
            last_updated: chrono::Utc::now().to_rfc3339(),
            mappings: Vec::new(),
        }
    }
}

/// Error type for mapping operations.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum MappingError {
    #[error("Mapping not found: {id}")]
    NotFound { id: String },

    #[error(
        "Duplicate mapping: a mapping for {target_field:?} with scope {scope:?} already exists"
    )]
    Duplicate {
        target_field: TargetField,
        scope: MappingScope,
    },

    #[error("Invalid mapping: {reason}")]
    Invalid { reason: String },

    #[error("Storage error: {reason}")]
    Storage { reason: String },

    #[error("Serialization error: {reason}")]
    Serialization { reason: String },
}

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

    #[test]
    fn mapping_scope_defaults_to_global() {
        let scope = MappingScope::default();
        assert!(scope.ticker.is_none());
        assert!(scope.period_start.is_none());
        assert!(scope.period_end.is_none());
    }

    #[test]
    fn mapping_scope_supports_ticker_filter() {
        let scope = MappingScope {
            ticker: Some("AAPL".to_string()),
            period_start: None,
            period_end: None,
        };
        assert_eq!(scope.ticker, Some("AAPL".to_string()));
        assert!(scope.period_start.is_none());
        assert!(scope.period_end.is_none());
    }

    #[test]
    fn test_mapping_helper_creates_enabled_mapping() {
        let mapping = CustomConceptMapping::test_mapping(
            TargetField::TotalRevenues,
            CustomConcept {
                taxonomy: "custom".to_string(),
                concept: "AlternativeRevenue".to_string(),
                unit_family: UnitFamily::Currency,
            },
            MappingScope {
                ticker: Some("TEST".to_string()),
                period_start: None,
                period_end: None,
            },
            None,
        );

        assert!(!mapping.id.is_empty());
        assert!(mapping.enabled);
        assert!(mapping.notes.is_none());
        assert_eq!(mapping.target_field, TargetField::TotalRevenues);
    }

    #[test]
    fn test_mapping_helper_supports_notes() {
        let mapping = CustomConceptMapping::test_mapping(
            TargetField::Ebitda,
            CustomConcept {
                taxonomy: "custom".to_string(),
                concept: "CustomEBITDA".to_string(),
                unit_family: UnitFamily::Currency,
            },
            MappingScope {
                ticker: Some("TEST".to_string()),
                period_start: None,
                period_end: None,
            },
            Some("TEST company changed reporting in 2023".to_string()),
        );

        assert_eq!(
            mapping.notes,
            Some("TEST company changed reporting in 2023".to_string())
        );
    }

    #[test]
    fn serde_round_trip_uses_camel_case_keys() {
        let config = MappingsConfig {
            version: 1,
            last_updated: "2026-04-13T10:30:00Z".to_string(),
            mappings: vec![CustomConceptMapping::test_mapping(
                TargetField::TotalRevenues,
                CustomConcept {
                    taxonomy: "custom".to_string(),
                    concept: "AltRevenue".to_string(),
                    unit_family: UnitFamily::Currency,
                },
                MappingScope {
                    ticker: Some("AAPL".to_string()),
                    period_start: Some("2024-01-01".to_string()),
                    period_end: Some("2024-12-31".to_string()),
                },
                None,
            )],
        };

        let json = serde_json::to_string(&config).unwrap();
        assert!(json.contains("\"lastUpdated\""));
        assert!(json.contains("\"targetField\""));
        // Should serialize as mappingKind, with customConcept as the value
        assert!(json.contains("\"mappingKind\""));
        assert!(json.contains("\"concept\""));
        assert!(json.contains("\"unitFamily\":\"currency\""));
        assert!(json.contains("\"periodStart\""));
        assert!(json.contains("\"periodEnd\""));
    }

    #[test]
    fn serde_accepts_legacy_snake_case_keys() {
        let json = r#"{
            "version": 1,
            "last_updated": "2026-04-13T10:30:00Z",
            "mappings": [{
                "id": "3b4693ff-71bc-4484-a955-42a75c5dfca7",
                "target_field": "total_revenues",
                "custom_concept": {
                    "taxonomy": "custom",
                    "concept": "AltRevenue",
                    "unit_family": "currency"
                },
                "scope": {
                    "ticker": "AAPL",
                    "period_start": "2024-01-01",
                    "period_end": "2024-12-31"
                },
                "enabled": true
            }]
        }"#;

        let config: MappingsConfig = serde_json::from_str(json).unwrap();
        assert_eq!(config.last_updated, "2026-04-13T10:30:00Z");
        assert_eq!(config.mappings[0].target_field, TargetField::TotalRevenues);
        assert_eq!(
            config.mappings[0].custom_concept().unwrap().unit_family,
            UnitFamily::Currency
        );
        assert_eq!(
            config.mappings[0].scope.period_start.as_deref(),
            Some("2024-01-01")
        );
    }

    #[test]
    fn create_request_accepts_cost_of_sales_target_field() {
        let json = r#"{
            "targetField": "cost_of_sales",
            "customConcept": {
                "taxonomy": "us-gaap",
                "concept": "CostOfGoodsSold",
                "unitFamily": "currency"
            },
            "scope": {
                "ticker": "AAPL"
            }
        }"#;

        let request = serde_json::from_str::<CreateCustomConceptMappingRequest>(json).unwrap();
        assert_eq!(request.target_field, TargetField::CostOfSales);
        assert_eq!(request.scope.ticker.as_deref(), Some("AAPL"));
    }

    #[test]
    fn create_request_accepts_pascal_case_cost_of_sales_target_field() {
        let json = r#"{
            "targetField": "CostOfSales",
            "customConcept": {
                "taxonomy": "us-gaap",
                "concept": "CostOfGoodsSold",
                "unitFamily": "currency"
            },
            "scope": {
                "ticker": "AAPL"
            }
        }"#;

        let request = serde_json::from_str::<CreateCustomConceptMappingRequest>(json).unwrap();
        assert_eq!(request.target_field, TargetField::CostOfSales);
    }

    #[test]
    fn create_request_accepts_consolidated_net_income_alias() {
        let json = r#"{
            "targetField": "consolidatedNetIncome",
            "customConcept": {
                "taxonomy": "us-gaap",
                "concept": "NetIncomeLoss",
                "unitFamily": "currency"
            },
            "scope": {
                "ticker": "AAPL"
            }
        }"#;

        let request = serde_json::from_str::<CreateCustomConceptMappingRequest>(json).unwrap();
        assert_eq!(request.target_field, TargetField::NetIncome);
    }

    #[test]
    fn balance_sheet_target_fields_round_trip() {
        let cases = [
            ("cash_and_equivalents", TargetField::CashAndEquivalents),
            ("short_term_investments", TargetField::ShortTermInvestments),
            (
                "accounts_receivable_net",
                TargetField::AccountsReceivableNet,
            ),
            ("inventory", TargetField::Inventory),
            ("other_current_assets", TargetField::OtherCurrentAssets),
            ("total_current_assets", TargetField::TotalCurrentAssets),
            (
                "property_plant_and_equipment_net",
                TargetField::PropertyPlantAndEquipmentNet,
            ),
            ("goodwill", TargetField::Goodwill),
            ("intangible_assets", TargetField::IntangibleAssets),
            (
                "other_non_current_assets",
                TargetField::OtherNonCurrentAssets,
            ),
            ("total_assets", TargetField::TotalAssets),
            ("accounts_payable", TargetField::AccountsPayable),
            (
                "accrued_expenses_and_other_current_liabilities",
                TargetField::AccruedExpensesAndOtherCurrentLiabilities,
            ),
            ("short_term_debt", TargetField::ShortTermDebt),
            (
                "total_current_liabilities",
                TargetField::TotalCurrentLiabilities,
            ),
            ("long_term_debt", TargetField::LongTermDebt),
            (
                "other_non_current_liabilities",
                TargetField::OtherNonCurrentLiabilities,
            ),
            ("total_liabilities", TargetField::TotalLiabilities),
            ("retained_earnings", TargetField::RetainedEarnings),
            (
                "accumulated_other_comprehensive_income_loss",
                TargetField::AccumulatedOtherComprehensiveIncomeLoss,
            ),
            ("total_equity", TargetField::TotalEquity),
        ];

        for (raw, expected) in cases {
            let parsed = raw.parse::<TargetField>().unwrap();
            assert_eq!(parsed, expected);
            assert_eq!(parsed.as_str(), raw);
        }
    }

    #[test]
    fn test_mappings_config_default() {
        let config = MappingsConfig::default();
        assert_eq!(config.version, 1);
        assert!(config.mappings.is_empty());
        assert!(!config.last_updated.is_empty());
    }

    #[test]
    fn create_request_rejects_unknown_top_level_fields() {
        let json = r#"{
            "id": "custom-id",
            "targetField": "total_revenues",
            "customConcept": {
                "taxonomy": "us-gaap",
                "concept": "RevenueFromContractWithCustomerExcludingAssessedTax",
                "unitFamily": "currency"
            },
            "scope": {}
        }"#;

        let result = serde_json::from_str::<CreateCustomConceptMappingRequest>(json);
        assert!(result.is_err());
    }
}
