//! Business logic for custom XBRL concept mappings.
//!
//! Provides CRUD operations and validation for mappings.

use std::sync::{Arc, RwLock};

use super::formula::{FormulaParser, FormulaValidationResult};
use super::storage::MappingsStorage;
use super::types::{
    CreateCustomConceptMappingRequest, CustomConceptMapping, FormulaMapping, MappingError,
    MappingKind, MappingScope, MappingsConfig,
};
use crate::error::AppError;

struct CustomConceptMappingDraft {
    target_field: super::types::TargetField,
    mapping_kind: MappingKind,
    scope: MappingScope,
    notes: Option<String>,
}

/// Service for managing custom XBRL concept mappings.
pub struct MappingsService {
    storage: MappingsStorage,
    /// In-memory cache of the mappings configuration
    config_cache: Arc<RwLock<MappingsConfig>>,
}

impl MappingsService {
    fn read_cache(&self) -> Result<std::sync::RwLockReadGuard<'_, MappingsConfig>, MappingError> {
        self.config_cache.read().map_err(|e| MappingError::Storage {
            reason: format!("config cache lock poisoned: {e}"),
        })
    }

    fn write_cache(&self) -> Result<std::sync::RwLockWriteGuard<'_, MappingsConfig>, MappingError> {
        self.config_cache
            .write()
            .map_err(|e| MappingError::Storage {
                reason: format!("config cache lock poisoned: {e}"),
            })
    }

    /// Create a new mappings service.
    pub fn new(storage: MappingsStorage) -> Result<Self, MappingError> {
        let service = Self {
            storage,
            config_cache: Arc::new(RwLock::new(MappingsConfig::default())),
        };

        let initial_config = service.storage.load()?;
        let mut config = initial_config;
        for mapping in &mut config.mappings {
            Self::ensure_formula_parsed(mapping)?;
        }
        *service.write_cache()? = config;

        Ok(service)
    }

    /// List all custom mappings.
    pub async fn list_mappings(&self) -> Result<Vec<CustomConceptMapping>, MappingError> {
        let config = self.read_cache()?;
        Ok(config.mappings.clone())
    }

    /// Get a single mapping by ID.
    pub fn get_mapping(&self, id: &str) -> Result<CustomConceptMapping, MappingError> {
        let config = self.read_cache()?;
        config
            .mappings
            .iter()
            .find(|m| m.id == id)
            .cloned()
            .ok_or_else(|| MappingError::NotFound { id: id.to_string() })
    }

    /// Add a new custom mapping.
    pub async fn add_mapping(
        &self,
        mut mapping: CustomConceptMapping,
    ) -> Result<String, MappingError> {
        Self::ensure_formula_parsed(&mut mapping)?;

        self.validate_mapping(&mapping)?;

        self.check_for_duplicates(&mapping)?;

        let id = mapping.id.clone();
        {
            let mut config = self.write_cache()?;
            config.mappings.push(mapping);
        }

        self.save_config()?;

        Ok(id)
    }

    /// Find an existing mapping that overlaps with the given target field and scope.
    pub fn find_duplicate_id(
        &self,
        target_field: &super::types::TargetField,
        scope: &MappingScope,
    ) -> Option<String> {
        let config = self.read_cache().ok()?;
        for existing in &config.mappings {
            if self.scopes_overlap(&existing.scope, scope) && &existing.target_field == target_field
            {
                return Some(existing.id.clone());
            }
        }
        None
    }

    /// Create or update a mapping (upsert). If a mapping with the same target field
    /// and scope already exists, it is updated in place.
    pub async fn create_or_update_mapping(
        &self,
        request: CreateCustomConceptMappingRequest,
    ) -> Result<(CustomConceptMapping, bool), MappingError> {
        let draft = Self::normalize_mapping_request(request)?;

        if let Some(existing_id) = self.find_duplicate_id(&draft.target_field, &draft.scope) {
            let existing = self.get_mapping(&existing_id)?;
            let updated = CustomConceptMapping {
                id: existing_id.clone(),
                target_field: draft.target_field,
                mapping_kind: draft.mapping_kind,
                scope: draft.scope,
                enabled: existing.enabled,
                notes: draft.notes,
            };
            self.update_mapping(&existing_id, updated.clone()).await?;
            let reloaded = self.get_mapping(&existing_id)?;
            Ok((reloaded, true))
        } else {
            let mapping = CustomConceptMapping {
                id: uuid::Uuid::new_v4().to_string(),
                target_field: draft.target_field,
                mapping_kind: draft.mapping_kind,
                scope: draft.scope,
                enabled: true,
                notes: draft.notes,
            };
            let id = mapping.id.clone();
            self.add_mapping(mapping).await?;
            let created = self.get_mapping(&id)?;
            Ok((created, false))
        }
    }

    /// Create a new custom mapping, generating server-owned fields and normalizing input.
    #[cfg_attr(not(test), allow(dead_code))]
    pub async fn create_mapping(
        &self,
        request: CreateCustomConceptMappingRequest,
    ) -> Result<CustomConceptMapping, MappingError> {
        let draft = Self::normalize_mapping_request(request)?;

        let mapping = CustomConceptMapping {
            id: uuid::Uuid::new_v4().to_string(),
            target_field: draft.target_field,
            mapping_kind: draft.mapping_kind,
            scope: draft.scope,
            enabled: true,
            notes: draft.notes,
        };

        let id = mapping.id.clone();
        self.add_mapping(mapping).await?;
        let created = self.get_mapping(&id)?;
        Ok(created)
    }

    /// Update an existing mapping from a create/update request payload.
    pub async fn update_mapping_from_request(
        &self,
        id: &str,
        request: CreateCustomConceptMappingRequest,
    ) -> Result<CustomConceptMapping, MappingError> {
        let existing = self.get_mapping(id)?;
        let draft = Self::normalize_mapping_request(request)?;
        let updated = CustomConceptMapping {
            id: id.to_string(),
            target_field: draft.target_field,
            mapping_kind: draft.mapping_kind,
            scope: draft.scope,
            enabled: existing.enabled,
            notes: draft.notes,
        };

        self.update_mapping(id, updated).await?;
        self.get_mapping(id)
    }

    /// Update an existing mapping.
    pub async fn update_mapping(
        &self,
        id: &str,
        mut mapping: CustomConceptMapping,
    ) -> Result<(), MappingError> {
        // Ensure the ID matches
        mapping.id = id.to_string();

        Self::ensure_formula_parsed(&mut mapping)?;

        // Validate the mapping
        self.validate_mapping(&mapping)?;

        // Check for duplicates (excluding this mapping)
        self.check_for_duplicates_excluding(&mapping, id)?;

        // Update in config
        {
            let mut config = self.write_cache()?;
            let index = config
                .mappings
                .iter()
                .position(|m| m.id == id)
                .ok_or_else(|| MappingError::NotFound { id: id.to_string() })?;

            config.mappings[index] = mapping;
        }

        // Persist to storage
        self.save_config()?;

        Ok(())
    }

    /// Delete a mapping by ID.
    pub async fn delete_mapping(&self, id: &str) -> Result<(), MappingError> {
        // Remove from config
        {
            let mut config = self.write_cache()?;
            let index = config
                .mappings
                .iter()
                .position(|m| m.id == id)
                .ok_or_else(|| MappingError::NotFound { id: id.to_string() })?;

            config.mappings.remove(index);
        }

        // Persist to storage
        self.save_config()?;

        Ok(())
    }

    /// Toggle the enabled state of a mapping.
    pub async fn toggle_mapping(&self, id: &str) -> Result<bool, MappingError> {
        let mut config = self.write_cache()?;
        let index = config
            .mappings
            .iter()
            .position(|m| m.id == id)
            .ok_or_else(|| MappingError::NotFound { id: id.to_string() })?;

        config.mappings[index].enabled = !config.mappings[index].enabled;
        let new_state = config.mappings[index].enabled;

        // Persist to storage
        drop(config);
        self.save_config()?;

        Ok(new_state)
    }

    /// Import mappings from JSON.
    pub async fn import_mappings(&self, json: &str) -> Result<MappingsConfig, MappingError> {
        let mut config = self.storage.import(json)?;

        // Validate all mappings in the imported config
        for mapping in &mut config.mappings {
            Self::ensure_formula_parsed(mapping)?;
            self.validate_mapping(mapping)?;
        }

        // Replace current config
        {
            let mut cache = self.write_cache()?;
            *cache = config.clone();
        }

        // Persist
        self.storage.save(&config)?;

        Ok(config)
    }

    /// Export mappings to JSON.
    pub async fn export_mappings(&self) -> Result<String, MappingError> {
        self.storage.export()
    }

    /// Parse formula and populate cached_ast if missing (e.g., after frontend deserialization).
    fn ensure_formula_parsed(mapping: &mut CustomConceptMapping) -> Result<(), MappingError> {
        if let MappingKind::Formula(ref mut formula) = mapping.mapping_kind {
            if formula.cached_ast.is_none() {
                let ast = FormulaParser::parse(&formula.expression).map_err(|e| {
                    MappingError::Invalid {
                        reason: format!("Invalid formula: {}", e),
                    }
                })?;
                formula.cached_ast = Some(ast);
            }
        }
        Ok(())
    }

    fn normalize_mapping_request(
        request: CreateCustomConceptMappingRequest,
    ) -> Result<CustomConceptMappingDraft, MappingError> {
        let mapping_kind = match request.mapping_kind {
            MappingKind::Concept(concept) => {
                if concept.taxonomy.trim().is_empty() {
                    return Err(MappingError::Invalid {
                        reason: "Custom concept taxonomy cannot be empty".to_string(),
                    });
                }

                if concept.concept.trim().is_empty() {
                    return Err(MappingError::Invalid {
                        reason: "Custom concept name cannot be empty".to_string(),
                    });
                }

                MappingKind::Concept(super::types::CustomConcept {
                    taxonomy: concept.taxonomy.trim().to_string(),
                    concept: concept.concept.trim().to_string(),
                    unit_family: concept.unit_family,
                })
            }
            MappingKind::Formula(formula) => {
                let expression = formula.expression.trim();
                if expression.is_empty() {
                    return Err(MappingError::Invalid {
                        reason: "Formula expression cannot be empty".to_string(),
                    });
                }

                let ast = FormulaParser::parse(expression).map_err(|e| MappingError::Invalid {
                    reason: format!("Invalid formula: {}", e),
                })?;

                MappingKind::Formula(FormulaMapping {
                    expression: expression.to_string(),
                    cached_ast: Some(ast),
                })
            }
        };

        Ok(CustomConceptMappingDraft {
            target_field: request.target_field,
            mapping_kind,
            scope: MappingScope {
                ticker: normalize_optional_string(request.scope.ticker)
                    .map(|ticker| ticker.to_ascii_uppercase()),
                period_start: normalize_optional_string(request.scope.period_start),
                period_end: normalize_optional_string(request.scope.period_end),
            },
            notes: normalize_optional_string(request.notes),
        })
    }

    /// Validate a custom mapping.
    fn validate_mapping(&self, mapping: &CustomConceptMapping) -> Result<(), MappingError> {
        // Validate ID format (UUID)
        if uuid::Uuid::parse_str(&mapping.id).is_err() {
            return Err(MappingError::Invalid {
                reason: "Invalid UUID format for mapping ID".to_string(),
            });
        }

        // Validate mapping kind
        match &mapping.mapping_kind {
            MappingKind::Concept(concept) => {
                if concept.taxonomy.trim().is_empty() {
                    return Err(MappingError::Invalid {
                        reason: "Custom concept taxonomy cannot be empty".to_string(),
                    });
                }

                if concept.concept.trim().is_empty() {
                    return Err(MappingError::Invalid {
                        reason: "Custom concept name cannot be empty".to_string(),
                    });
                }
            }
            MappingKind::Formula(formula) => {
                // Formula validation is done during creation/update
                // The cached_ast should always be present for valid formulas
                if formula.cached_ast.is_none() {
                    return Err(MappingError::Invalid {
                        reason: "Formula has not been parsed".to_string(),
                    });
                }
            }
        }

        // Validate scope
        if let Some(ref ticker) = mapping.scope.ticker {
            if ticker.trim().is_empty() {
                return Err(MappingError::Invalid {
                    reason: "Ticker cannot be empty string".to_string(),
                });
            }
        }

        // Validate period dates
        if let (Some(start), Some(end)) = (&mapping.scope.period_start, &mapping.scope.period_end) {
            if let (Ok(start_date), Ok(end_date)) = (
                chrono::NaiveDate::parse_from_str(start, "%Y-%m-%d"),
                chrono::NaiveDate::parse_from_str(end, "%Y-%m-%d"),
            ) {
                if start_date > end_date {
                    return Err(MappingError::Invalid {
                        reason: "Period start date must be before or equal to end date".to_string(),
                    });
                }
            } else {
                return Err(MappingError::Invalid {
                    reason: "Period dates must be in ISO format (YYYY-MM-DD)".to_string(),
                });
            }
        }

        Ok(())
    }

    /// Validate a formula expression without creating a mapping.
    pub fn validate_formula(
        &self,
        expression: &str,
    ) -> Result<FormulaValidationResult, MappingError> {
        let expression = expression.trim();

        if expression.is_empty() {
            return Ok(FormulaValidationResult {
                is_valid: false,
                error: Some("Formula expression cannot be empty".to_string()),
                field_refs: None,
                functions: None,
            });
        }

        // Parse the formula
        let ast = match FormulaParser::parse(expression) {
            Ok(ast) => ast,
            Err(e) => {
                return Ok(FormulaValidationResult {
                    is_valid: false,
                    error: Some(e.to_string()),
                    field_refs: None,
                    functions: None,
                });
            }
        };

        let field_refs = ast.collect_field_refs();
        let functions = ast.collect_functions();

        Ok(FormulaValidationResult {
            is_valid: true,
            error: None,
            field_refs: if field_refs.is_empty() {
                None
            } else {
                Some(field_refs)
            },
            functions: if functions.is_empty() {
                None
            } else {
                Some(functions)
            },
        })
    }

    /// Check if a mapping would conflict with existing mappings.
    fn check_for_duplicates(&self, mapping: &CustomConceptMapping) -> Result<(), MappingError> {
        let config = self.read_cache()?;

        for existing in &config.mappings {
            if self.scopes_overlap(&existing.scope, &mapping.scope)
                && existing.target_field == mapping.target_field
            {
                return Err(MappingError::Duplicate {
                    target_field: mapping.target_field,
                    scope: mapping.scope.clone(),
                });
            }
        }

        Ok(())
    }

    /// Check for duplicates excluding a specific mapping ID (for updates).
    fn check_for_duplicates_excluding(
        &self,
        mapping: &CustomConceptMapping,
        exclude_id: &str,
    ) -> Result<(), MappingError> {
        let config = self.read_cache()?;

        for existing in &config.mappings {
            if existing.id == exclude_id {
                continue;
            }

            if self.scopes_overlap(&existing.scope, &mapping.scope)
                && existing.target_field == mapping.target_field
            {
                return Err(MappingError::Duplicate {
                    target_field: mapping.target_field,
                    scope: mapping.scope.clone(),
                });
            }
        }

        Ok(())
    }

    /// Check if two scopes overlap.
    fn scopes_overlap(&self, scope1: &MappingScope, scope2: &MappingScope) -> bool {
        // Check ticker overlap
        let ticker_match = match (&scope1.ticker, &scope2.ticker) {
            (None, _) | (_, None) => true, // Global scope overlaps with everything
            (Some(t1), Some(t2)) => t1.eq_ignore_ascii_case(t2),
        };

        if !ticker_match {
            return false;
        }

        // Check period overlap
        self.periods_overlap(
            &scope1.period_start,
            &scope1.period_end,
            &scope2.period_start,
            &scope2.period_end,
        )
    }

    /// Check if two period ranges overlap.
    fn periods_overlap(
        &self,
        start1: &Option<String>,
        end1: &Option<String>,
        start2: &Option<String>,
        end2: &Option<String>,
    ) -> bool {
        // If either has no period constraints, they overlap
        if (start1.is_none() && end1.is_none()) || (start2.is_none() && end2.is_none()) {
            return true;
        }

        // Parse dates
        let parse_date = |s: &Option<String>| {
            s.as_ref()
                .and_then(|d| chrono::NaiveDate::parse_from_str(d, "%Y-%m-%d").ok())
        };

        let s1 = parse_date(start1);
        let e1 = parse_date(end1);
        let s2 = parse_date(start2);
        let e2 = parse_date(end2);

        match (s1, e1, s2, e2) {
            (Some(s1), Some(e1), Some(s2), Some(e2)) => {
                // Two defined ranges - check overlap
                !(e1 < s2 || e2 < s1)
            }
            (Some(_s1), Some(_e1), Some(s2), None) => s2 <= _e1, // s2 is within [s1, e1]
            (Some(_s1), Some(_e1), None, Some(e2)) => e2 >= _s1, // e2 is within [s1, e1]
            (Some(_s1), Some(_e1), None, None) => true,          // Range overlaps with unbounded
            (None, None, Some(_s2), Some(_e2)) => true,          // Unbounded overlaps with range
            (None, None, Some(_), None) => true,                 // Unbounded overlaps
            (Some(_), None, Some(_), None) => true,              // Both start-bounded only
            (Some(_), None, None, Some(_)) => true,              // Both end-bounded only
            (Some(_), None, None, None) => true, // Partially bounded overlaps unbounded
            (None, None, None, None) => true,    // Both unbounded
            _ => true,
        }
    }

    /// Save the current configuration to storage.
    fn save_config(&self) -> Result<(), MappingError> {
        let config = self.read_cache()?;
        self.storage.save(&config)
    }
}

#[cfg_attr(not(test), allow(dead_code))]
fn normalize_optional_string(value: Option<String>) -> Option<String> {
    value.and_then(|value| {
        let trimmed = value.trim();
        (!trimmed.is_empty()).then(|| trimmed.to_string())
    })
}

impl From<MappingsService> for AppError {
    fn from(_error: MappingsService) -> Self {
        AppError::Mappings(MappingError::Storage {
            reason: "Mappings service initialization failed".to_string(),
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::mappings::{CreateCustomConceptMappingRequest, TargetField};
    use crate::terminal::sec_edgar::UnitFamily;
    use tempfile::TempDir;

    fn create_test_service() -> (MappingsService, TempDir) {
        let temp_dir = TempDir::new().unwrap();
        let storage = MappingsStorage::new(temp_dir.path().to_path_buf()).unwrap();
        let service = MappingsService::new(storage).unwrap();
        (service, temp_dir)
    }

    fn create_test_mapping(ticker: Option<&str>) -> CustomConceptMapping {
        CustomConceptMapping::test_mapping(
            TargetField::TotalRevenues,
            crate::mappings::types::CustomConcept {
                taxonomy: "custom".to_string(),
                concept: "AlternativeRevenue".to_string(),
                unit_family: UnitFamily::Currency,
            },
            MappingScope {
                ticker: ticker.map(|t| t.to_string()),
                period_start: None,
                period_end: None,
            },
            None,
        )
    }

    #[test]
    fn test_list_mappings_returns_empty_initially() {
        let (service, _temp) = create_test_service();
        futures::executor::block_on(async {
            let mappings = service.list_mappings().await.unwrap();
            assert!(mappings.is_empty());
        });
    }

    #[test]
    fn test_add_mapping() {
        let (service, _temp) = create_test_service();
        futures::executor::block_on(async {
            let mapping = create_test_mapping(Some("AAPL"));
            let id = service.add_mapping(mapping).await.unwrap();

            let mappings = service.list_mappings().await.unwrap();
            assert_eq!(mappings.len(), 1);
            assert_eq!(mappings[0].id, id);
        });
    }

    #[test]
    fn test_delete_mapping() {
        let (service, _temp) = create_test_service();
        futures::executor::block_on(async {
            let mapping = create_test_mapping(Some("AAPL"));
            let id = service.add_mapping(mapping).await.unwrap();

            service.delete_mapping(&id).await.unwrap();

            let mappings = service.list_mappings().await.unwrap();
            assert!(mappings.is_empty());
        });
    }

    #[test]
    fn test_toggle_mapping() {
        let (service, _temp) = create_test_service();
        futures::executor::block_on(async {
            let mapping = create_test_mapping(Some("AAPL"));
            let id = service.add_mapping(mapping).await.unwrap();

            let enabled = service.toggle_mapping(&id).await.unwrap();
            assert!(!enabled);

            let enabled = service.toggle_mapping(&id).await.unwrap();
            assert!(enabled);
        });
    }

    #[test]
    fn test_duplicate_detection() {
        let (service, _temp) = create_test_service();
        futures::executor::block_on(async {
            let mapping = create_test_mapping(Some("AAPL"));
            service.add_mapping(mapping.clone()).await.unwrap();

            // Try to add duplicate
            let result = service.add_mapping(mapping).await;
            assert!(result.is_err());
        });
    }

    #[test]
    fn test_create_mapping_normalizes_server_owned_fields() {
        let (service, _temp) = create_test_service();
        futures::executor::block_on(async {
            let created = service
                .create_mapping(CreateCustomConceptMappingRequest {
                    target_field: TargetField::TotalRevenues,
                    mapping_kind: MappingKind::Concept(crate::mappings::types::CustomConcept {
                        taxonomy: " us-gaap ".to_string(),
                        concept: " RevenueFromContractWithCustomerExcludingAssessedTax "
                            .to_string(),
                        unit_family: UnitFamily::Currency,
                    }),
                    scope: MappingScope {
                        ticker: Some(" aapl ".to_string()),
                        period_start: Some(" 2024-01-01 ".to_string()),
                        period_end: Some("".to_string()),
                    },
                    notes: Some("  Use company-specific revenue concept  ".to_string()),
                })
                .await
                .unwrap();

            assert_eq!(created.scope.ticker.as_deref(), Some("AAPL"));
            assert_eq!(created.scope.period_start.as_deref(), Some("2024-01-01"));
            assert!(created.scope.period_end.is_none());
            assert_eq!(
                created.notes.as_deref(),
                Some("Use company-specific revenue concept")
            );
            assert!(created.enabled);
            assert!(!created.id.is_empty());
        });
    }

    #[test]
    fn test_period_scope_overlap_allows_distinct_non_overlapping_ranges() {
        let (service, _temp) = create_test_service();
        futures::executor::block_on(async {
            let mut first = create_test_mapping(Some("AAPL"));
            first.scope.period_start = Some("2023-01-01".to_string());
            first.scope.period_end = Some("2023-12-31".to_string());

            service.add_mapping(first).await.unwrap();

            let mut second = create_test_mapping(Some("AAPL"));
            second.scope.period_start = Some("2024-01-01".to_string());
            second.scope.period_end = Some("2024-12-31".to_string());

            service.add_mapping(second).await.unwrap();
            assert_eq!(service.list_mappings().await.unwrap().len(), 2);
        });
    }

    #[test]
    fn test_import_and_export() {
        let (service, _temp) = create_test_service();
        futures::executor::block_on(async {
            let mapping = create_test_mapping(Some("AAPL"));
            service.add_mapping(mapping.clone()).await.unwrap();

            let exported = service.export_mappings().await.unwrap();

            // Clear and import
            let (service2, _temp2) = create_test_service();
            let imported = service2.import_mappings(&exported).await.unwrap();
            assert_eq!(imported.mappings.len(), 1);
            assert_eq!(imported.mappings[0].scope.ticker, Some("AAPL".to_string()));
        });
    }
}
