use chrono::Utc;
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, Set};
use uuid::Uuid;

use crate::dto::{DictionaryItemResp, DictionaryResp};
use crate::entity::{dictionary, dictionary_item};
use crate::error::{ConfigError, ConfigResult};
use erp_core::audit::AuditLog;
use erp_core::audit_service;
use erp_core::error::check_version;
use erp_core::events::EventBus;
use erp_core::types::Pagination;

/// Dictionary CRUD service — manage dictionaries and their items within a tenant.
///
/// Dictionaries provide enumerated value sets (e.g. status codes, categories)
/// that can be referenced throughout the system by their unique `code`.
pub struct DictionaryService;

impl DictionaryService {
    /// List dictionaries within a tenant with pagination.
    ///
    /// Each dictionary includes its associated items.
    /// Returns `(dictionaries, total_count)`.
    pub async fn list(
        tenant_id: Uuid,
        pagination: &Pagination,
        db: &sea_orm::DatabaseConnection,
    ) -> ConfigResult<(Vec<DictionaryResp>, u64)> {
        let paginator = dictionary::Entity::find()
            .filter(dictionary::Column::TenantId.eq(tenant_id))
            .filter(dictionary::Column::DeletedAt.is_null())
            .paginate(db, pagination.limit());

        let total = paginator
            .num_items()
            .await
            .map_err(|e| ConfigError::Validation(e.to_string()))?;

        let page_index = pagination.page.unwrap_or(1).saturating_sub(1);
        let models = paginator
            .fetch_page(page_index)
            .await
            .map_err(|e| ConfigError::Validation(e.to_string()))?;

        let mut resps = Vec::with_capacity(models.len());
        for m in &models {
            let items = Self::fetch_items(m.id, tenant_id, db).await?;
            resps.push(dict_model_to_resp(m, items));
        }

        Ok((resps, total))
    }

    /// Fetch a single dictionary by ID, scoped to the given tenant.
    ///
    /// Includes all associated items.
    pub async fn get_by_id(
        id: Uuid,
        tenant_id: Uuid,
        db: &sea_orm::DatabaseConnection,
    ) -> ConfigResult<DictionaryResp> {
        let model = dictionary::Entity::find_by_id(id)
            .one(db)
            .await
            .map_err(|e| ConfigError::Validation(e.to_string()))?
            .filter(|d| d.tenant_id == tenant_id && d.deleted_at.is_none())
            .ok_or_else(|| ConfigError::NotFound("字典不存在".to_string()))?;

        let items = Self::fetch_items(model.id, tenant_id, db).await?;

        Ok(dict_model_to_resp(&model, items))
    }

    /// Create a new dictionary within the current tenant.
    ///
    /// Validates code uniqueness, then inserts the record and publishes
    /// a `dictionary.created` domain event.
    pub async fn create(
        tenant_id: Uuid,
        operator_id: Uuid,
        name: &str,
        code: &str,
        description: &Option<String>,
        db: &sea_orm::DatabaseConnection,
        event_bus: &EventBus,
    ) -> ConfigResult<DictionaryResp> {
        // Check code uniqueness within tenant
        let existing = dictionary::Entity::find()
            .filter(dictionary::Column::TenantId.eq(tenant_id))
            .filter(dictionary::Column::Code.eq(code))
            .filter(dictionary::Column::DeletedAt.is_null())
            .one(db)
            .await
            .map_err(|e| ConfigError::Validation(e.to_string()))?;
        if existing.is_some() {
            return Err(ConfigError::Validation("字典编码已存在".to_string()));
        }

        let now = Utc::now();
        let id = Uuid::now_v7();
        let model = dictionary::ActiveModel {
            id: Set(id),
            tenant_id: Set(tenant_id),
            name: Set(name.to_string()),
            code: Set(code.to_string()),
            description: Set(description.clone()),
            created_at: Set(now),
            updated_at: Set(now),
            created_by: Set(operator_id),
            updated_by: Set(operator_id),
            deleted_at: Set(None),
            version: Set(1),
        };
        model
            .insert(db)
            .await
            .map_err(|e| ConfigError::Validation(e.to_string()))?;

        event_bus
            .publish(
                erp_core::events::DomainEvent::new(
                    "dictionary.created",
                    tenant_id,
                    serde_json::json!({ "dictionary_id": id, "code": code }),
                ),
                db,
            )
            .await;

        audit_service::record(
            AuditLog::new(
                tenant_id,
                Some(operator_id),
                "dictionary.create",
                "dictionary",
            )
            .with_resource_id(id),
            db,
        )
        .await;

        Ok(DictionaryResp {
            id,
            name: name.to_string(),
            code: code.to_string(),
            description: description.clone(),
            items: vec![],
            version: 1,
        })
    }

    /// Update editable dictionary fields (name and description).
    ///
    /// Code cannot be changed after creation.
    /// Performs optimistic locking via version check.
    pub async fn update(
        id: Uuid,
        tenant_id: Uuid,
        operator_id: Uuid,
        req: &crate::dto::UpdateDictionaryReq,
        db: &sea_orm::DatabaseConnection,
    ) -> ConfigResult<DictionaryResp> {
        let model = dictionary::Entity::find_by_id(id)
            .one(db)
            .await
            .map_err(|e| ConfigError::Validation(e.to_string()))?
            .filter(|d| d.tenant_id == tenant_id && d.deleted_at.is_none())
            .ok_or_else(|| ConfigError::NotFound("字典不存在".to_string()))?;

        let next_version =
            check_version(req.version, model.version).map_err(|_| ConfigError::VersionMismatch)?;

        let mut active: dictionary::ActiveModel = model.into();

        if let Some(n) = &req.name {
            active.name = Set(n.clone());
        }
        if let Some(d) = &req.description {
            active.description = Set(Some(d.clone()));
        }

        active.updated_at = Set(Utc::now());
        active.updated_by = Set(operator_id);
        active.version = Set(next_version);

        let updated = active
            .update(db)
            .await
            .map_err(|e| ConfigError::Validation(e.to_string()))?;

        let items = Self::fetch_items(updated.id, tenant_id, db).await?;

        audit_service::record(
            AuditLog::new(
                tenant_id,
                Some(operator_id),
                "dictionary.update",
                "dictionary",
            )
            .with_resource_id(id),
            db,
        )
        .await;

        Ok(dict_model_to_resp(&updated, items))
    }

    /// Soft-delete a dictionary by setting the `deleted_at` timestamp.
    /// Performs optimistic locking via version check.
    pub async fn delete(
        id: Uuid,
        tenant_id: Uuid,
        operator_id: Uuid,
        version: i32,
        db: &sea_orm::DatabaseConnection,
        event_bus: &EventBus,
    ) -> ConfigResult<()> {
        let model = dictionary::Entity::find_by_id(id)
            .one(db)
            .await
            .map_err(|e| ConfigError::Validation(e.to_string()))?
            .filter(|d| d.tenant_id == tenant_id && d.deleted_at.is_none())
            .ok_or_else(|| ConfigError::NotFound("字典不存在".to_string()))?;

        let next_version =
            check_version(version, model.version).map_err(|_| ConfigError::VersionMismatch)?;

        let mut active: dictionary::ActiveModel = model.into();
        active.deleted_at = Set(Some(Utc::now()));
        active.updated_at = Set(Utc::now());
        active.updated_by = Set(operator_id);
        active.version = Set(next_version);
        active
            .update(db)
            .await
            .map_err(|e| ConfigError::Validation(e.to_string()))?;

        event_bus
            .publish(
                erp_core::events::DomainEvent::new(
                    "dictionary.deleted",
                    tenant_id,
                    serde_json::json!({ "dictionary_id": id }),
                ),
                db,
            )
            .await;

        audit_service::record(
            AuditLog::new(
                tenant_id,
                Some(operator_id),
                "dictionary.delete",
                "dictionary",
            )
            .with_resource_id(id),
            db,
        )
        .await;

        Ok(())
    }

    /// Add a new item to a dictionary.
    ///
    /// Validates that the item `value` is unique within the dictionary.
    pub async fn add_item(
        dictionary_id: Uuid,
        tenant_id: Uuid,
        operator_id: Uuid,
        req: &crate::dto::CreateDictionaryItemReq,
        db: &sea_orm::DatabaseConnection,
    ) -> ConfigResult<DictionaryItemResp> {
        // Verify the dictionary exists and belongs to this tenant
        let _dict = dictionary::Entity::find_by_id(dictionary_id)
            .one(db)
            .await
            .map_err(|e| ConfigError::Validation(e.to_string()))?
            .filter(|d| d.tenant_id == tenant_id && d.deleted_at.is_none())
            .ok_or_else(|| ConfigError::NotFound("字典不存在".to_string()))?;

        // Check value uniqueness within dictionary
        let existing = dictionary_item::Entity::find()
            .filter(dictionary_item::Column::DictionaryId.eq(dictionary_id))
            .filter(dictionary_item::Column::TenantId.eq(tenant_id))
            .filter(dictionary_item::Column::Value.eq(&req.value))
            .filter(dictionary_item::Column::DeletedAt.is_null())
            .one(db)
            .await
            .map_err(|e| ConfigError::Validation(e.to_string()))?;
        if existing.is_some() {
            return Err(ConfigError::Validation("字典项值已存在".to_string()));
        }

        let now = Utc::now();
        let id = Uuid::now_v7();
        let sort_order = req.sort_order.unwrap_or(0);
        let model = dictionary_item::ActiveModel {
            id: Set(id),
            tenant_id: Set(tenant_id),
            dictionary_id: Set(dictionary_id),
            label: Set(req.label.clone()),
            value: Set(req.value.clone()),
            sort_order: Set(sort_order),
            color: Set(req.color.clone()),
            created_at: Set(now),
            updated_at: Set(now),
            created_by: Set(operator_id),
            updated_by: Set(operator_id),
            deleted_at: Set(None),
            version: Set(1),
        };
        model
            .insert(db)
            .await
            .map_err(|e| ConfigError::Validation(e.to_string()))?;

        audit_service::record(
            AuditLog::new(
                tenant_id,
                Some(operator_id),
                "dictionary_item.create",
                "dictionary_item",
            )
            .with_resource_id(id),
            db,
        )
        .await;

        Ok(DictionaryItemResp {
            id,
            dictionary_id,
            label: req.label.clone(),
            value: req.value.clone(),
            sort_order,
            color: req.color.clone(),
            version: 1,
        })
    }

    /// Update editable dictionary item fields (label, value, sort_order, color).
    /// Performs optimistic locking via version check.
    pub async fn update_item(
        item_id: Uuid,
        tenant_id: Uuid,
        operator_id: Uuid,
        req: &crate::dto::UpdateDictionaryItemReq,
        db: &sea_orm::DatabaseConnection,
    ) -> ConfigResult<DictionaryItemResp> {
        let model = dictionary_item::Entity::find_by_id(item_id)
            .one(db)
            .await
            .map_err(|e| ConfigError::Validation(e.to_string()))?
            .filter(|i| i.tenant_id == tenant_id && i.deleted_at.is_none())
            .ok_or_else(|| ConfigError::NotFound("字典项不存在".to_string()))?;

        let next_version =
            check_version(req.version, model.version).map_err(|_| ConfigError::VersionMismatch)?;

        let mut active: dictionary_item::ActiveModel = model.into();

        if let Some(l) = &req.label {
            active.label = Set(l.clone());
        }
        if let Some(v) = &req.value {
            active.value = Set(v.clone());
        }
        if let Some(s) = req.sort_order {
            active.sort_order = Set(s);
        }
        if let Some(c) = &req.color {
            active.color = Set(Some(c.clone()));
        }

        active.updated_at = Set(Utc::now());
        active.updated_by = Set(operator_id);
        active.version = Set(next_version);

        let updated = active
            .update(db)
            .await
            .map_err(|e| ConfigError::Validation(e.to_string()))?;

        audit_service::record(
            AuditLog::new(
                tenant_id,
                Some(operator_id),
                "dictionary_item.update",
                "dictionary_item",
            )
            .with_resource_id(item_id),
            db,
        )
        .await;

        Ok(item_model_to_resp(&updated))
    }

    /// Soft-delete a dictionary item by setting the `deleted_at` timestamp.
    /// Performs optimistic locking via version check.
    pub async fn delete_item(
        item_id: Uuid,
        tenant_id: Uuid,
        operator_id: Uuid,
        version: i32,
        db: &sea_orm::DatabaseConnection,
    ) -> ConfigResult<()> {
        let model = dictionary_item::Entity::find_by_id(item_id)
            .one(db)
            .await
            .map_err(|e| ConfigError::Validation(e.to_string()))?
            .filter(|i| i.tenant_id == tenant_id && i.deleted_at.is_none())
            .ok_or_else(|| ConfigError::NotFound("字典项不存在".to_string()))?;

        let next_version =
            check_version(version, model.version).map_err(|_| ConfigError::VersionMismatch)?;

        let mut active: dictionary_item::ActiveModel = model.into();
        active.deleted_at = Set(Some(Utc::now()));
        active.updated_at = Set(Utc::now());
        active.updated_by = Set(operator_id);
        active.version = Set(next_version);
        active
            .update(db)
            .await
            .map_err(|e| ConfigError::Validation(e.to_string()))?;

        audit_service::record(
            AuditLog::new(
                tenant_id,
                Some(operator_id),
                "dictionary_item.delete",
                "dictionary_item",
            )
            .with_resource_id(item_id),
            db,
        )
        .await;

        Ok(())
    }

    /// Look up a dictionary by its `code` and return all items.
    ///
    /// Useful for frontend dropdowns and enum-like lookups.
    pub async fn list_items_by_code(
        code: &str,
        tenant_id: Uuid,
        db: &sea_orm::DatabaseConnection,
    ) -> ConfigResult<Vec<DictionaryItemResp>> {
        let dict = dictionary::Entity::find()
            .filter(dictionary::Column::TenantId.eq(tenant_id))
            .filter(dictionary::Column::Code.eq(code))
            .filter(dictionary::Column::DeletedAt.is_null())
            .one(db)
            .await
            .map_err(|e| ConfigError::Validation(e.to_string()))?
            .ok_or_else(|| ConfigError::NotFound(format!("字典编码 '{}' 不存在", code)))?;

        Self::fetch_items(dict.id, tenant_id, db).await
    }

    // ---- 内部辅助方法 ----

    /// Fetch all non-deleted items for a given dictionary.
    async fn fetch_items(
        dictionary_id: Uuid,
        tenant_id: Uuid,
        db: &sea_orm::DatabaseConnection,
    ) -> ConfigResult<Vec<DictionaryItemResp>> {
        let items = dictionary_item::Entity::find()
            .filter(dictionary_item::Column::DictionaryId.eq(dictionary_id))
            .filter(dictionary_item::Column::TenantId.eq(tenant_id))
            .filter(dictionary_item::Column::DeletedAt.is_null())
            .all(db)
            .await
            .map_err(|e| ConfigError::Validation(e.to_string()))?;

        Ok(items.iter().map(item_model_to_resp).collect())
    }
}

/// Free function wrapping the private helper so the mapping logic is reusable
/// in both async methods and synchronous unit tests without a database.
fn item_model_to_resp(m: &dictionary_item::Model) -> DictionaryItemResp {
    DictionaryItemResp {
        id: m.id,
        dictionary_id: m.dictionary_id,
        label: m.label.clone(),
        value: m.value.clone(),
        sort_order: m.sort_order,
        color: m.color.clone(),
        version: m.version,
    }
}

/// Free function for dictionary model -> response DTO mapping.
fn dict_model_to_resp(m: &dictionary::Model, items: Vec<DictionaryItemResp>) -> DictionaryResp {
    DictionaryResp {
        id: m.id,
        name: m.name.clone(),
        code: m.code.clone(),
        description: m.description.clone(),
        items,
        version: m.version,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use chrono::Utc;
    use uuid::Uuid;

    fn sample_dict_model() -> dictionary::Model {
        dictionary::Model {
            id: Uuid::now_v7(),
            tenant_id: Uuid::now_v7(),
            name: "测试字典".to_string(),
            code: "test_dict".to_string(),
            description: Some("描述".to_string()),
            created_at: Utc::now(),
            updated_at: Utc::now(),
            created_by: Uuid::now_v7(),
            updated_by: Uuid::now_v7(),
            deleted_at: None,
            version: 1,
        }
    }

    fn sample_item_model() -> dictionary_item::Model {
        dictionary_item::Model {
            id: Uuid::now_v7(),
            tenant_id: Uuid::now_v7(),
            dictionary_id: Uuid::now_v7(),
            label: "选项A".to_string(),
            value: "option_a".to_string(),
            sort_order: 1,
            color: Some("#FF0000".to_string()),
            created_at: Utc::now(),
            updated_at: Utc::now(),
            created_by: Uuid::now_v7(),
            updated_by: Uuid::now_v7(),
            deleted_at: None,
            version: 1,
        }
    }

    // ---- dict_model_to_resp ----

    #[test]
    fn dict_model_to_resp_with_items() {
        let m = sample_dict_model();
        let item = item_model_to_resp(&sample_item_model());
        let resp = dict_model_to_resp(&m, vec![item]);

        assert_eq!(resp.id, m.id);
        assert_eq!(resp.name, "测试字典");
        assert_eq!(resp.code, "test_dict");
        assert_eq!(resp.description, Some("描述".to_string()));
        assert_eq!(resp.version, 1);
        assert_eq!(resp.items.len(), 1);
        assert_eq!(resp.items[0].label, "选项A");
    }

    #[test]
    fn dict_model_to_resp_without_description() {
        let mut m = sample_dict_model();
        m.description = None;
        let resp = dict_model_to_resp(&m, vec![]);

        assert_eq!(resp.description, None);
        assert!(resp.items.is_empty());
    }

    #[test]
    fn dict_model_to_resp_preserves_version() {
        let mut m = sample_dict_model();
        m.version = 42;
        let resp = dict_model_to_resp(&m, vec![]);

        assert_eq!(resp.version, 42);
    }

    // ---- item_model_to_resp ----

    #[test]
    fn item_model_to_resp_all_fields() {
        let m = sample_item_model();
        let resp = item_model_to_resp(&m);

        assert_eq!(resp.id, m.id);
        assert_eq!(resp.dictionary_id, m.dictionary_id);
        assert_eq!(resp.label, "选项A");
        assert_eq!(resp.value, "option_a");
        assert_eq!(resp.sort_order, 1);
        assert_eq!(resp.color, Some("#FF0000".to_string()));
        assert_eq!(resp.version, 1);
    }

    #[test]
    fn item_model_to_resp_without_color() {
        let mut m = sample_item_model();
        m.color = None;
        let resp = item_model_to_resp(&m);

        assert_eq!(resp.color, None);
    }

    #[test]
    fn item_model_to_resp_default_sort_order() {
        let mut m = sample_item_model();
        m.sort_order = 0;
        let resp = item_model_to_resp(&m);

        assert_eq!(resp.sort_order, 0);
    }

    #[test]
    fn item_model_to_resp_preserves_version() {
        let mut m = sample_item_model();
        m.version = 7;
        let resp = item_model_to_resp(&m);

        assert_eq!(resp.version, 7);
    }
}
