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

use crate::dto::SettingResp;
use crate::entity::setting;
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;

/// Setting scope hierarchy constants.
const SCOPE_PLATFORM: &str = "platform";
const SCOPE_TENANT: &str = "tenant";
const SCOPE_ORG: &str = "org";
const SCOPE_USER: &str = "user";

/// Setting CRUD service — manage hierarchical configuration values.
///
/// Settings support a 4-level inheritance hierarchy:
/// `user -> org -> tenant -> platform`
///
/// When reading a setting, if the exact scope+scope_id match is not found,
/// the service walks up the hierarchy to find the nearest ancestor value.
pub struct SettingService;

impl SettingService {
    /// Get a setting value with hierarchical fallback.
    ///
    /// Resolution order:
    /// 1. Exact match at (scope, scope_id)
    /// 2. Walk up the hierarchy based on scope:
    ///    - `user` -> org -> tenant -> platform
    ///    - `org` -> tenant -> platform
    ///    - `tenant` -> platform
    ///    - `platform` -> NotFound
    pub async fn get(
        key: &str,
        scope: &str,
        scope_id: &Option<Uuid>,
        tenant_id: Uuid,
        db: &sea_orm::DatabaseConnection,
    ) -> ConfigResult<SettingResp> {
        // 1. Try exact match
        if let Some(resp) = Self::find_exact(key, scope, scope_id, tenant_id, db).await? {
            return Ok(resp);
        }

        // 2. Walk up the hierarchy based on scope
        let fallback_chain = Self::fallback_chain(scope, scope_id, tenant_id)?;

        for (fb_scope, fb_scope_id) in fallback_chain {
            if let Some(resp) =
                Self::find_exact(key, &fb_scope, &fb_scope_id, tenant_id, db).await?
            {
                return Ok(resp);
            }
        }

        Err(ConfigError::NotFound(format!(
            "设置 '{}' 在 '{}' 作用域下不存在",
            key, scope
        )))
    }

    /// Set a setting value. Creates or updates.
    ///
    /// If a record with the same (scope, scope_id, key) exists and is not
    /// soft-deleted, it will be updated. Otherwise a new record is inserted.
    pub async fn set(
        params: crate::dto::SetSettingParams,
        tenant_id: Uuid,
        operator_id: Uuid,
        db: &sea_orm::DatabaseConnection,
        event_bus: &EventBus,
    ) -> ConfigResult<SettingResp> {
        // Look for an existing non-deleted record
        let mut query = setting::Entity::find()
            .filter(setting::Column::TenantId.eq(tenant_id))
            .filter(setting::Column::Scope.eq(&params.scope))
            .filter(setting::Column::SettingKey.eq(&params.key))
            .filter(setting::Column::DeletedAt.is_null());

        query = match params.scope_id {
            Some(id) => query.filter(setting::Column::ScopeId.eq(id)),
            None => query.filter(setting::Column::ScopeId.is_null()),
        };

        let existing = query
            .one(db)
            .await
            .map_err(|e| ConfigError::Validation(e.to_string()))?;

        if let Some(model) = existing {
            // Update existing record — 乐观锁校验
            let next_version = match params.version {
                Some(v) => {
                    check_version(v, model.version).map_err(|_| ConfigError::VersionMismatch)?
                }
                None => model.version + 1,
            };

            let mut active: setting::ActiveModel = model.into();
            active.setting_value = Set(params.value.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()))?;

            event_bus
                .publish(
                    erp_core::events::DomainEvent::new(
                        "setting.updated",
                        tenant_id,
                        serde_json::json!({
                            "setting_id": updated.id,
                            "key": params.key,
                            "scope": params.scope,
                        }),
                    ),
                    db,
                )
                .await;

            audit_service::record(
                AuditLog::new(tenant_id, Some(operator_id), "setting.upsert", "setting")
                    .with_resource_id(updated.id),
                db,
            )
            .await;

            Ok(Self::model_to_resp(&updated))
        } else {
            // Insert new record
            let now = Utc::now();
            let id = Uuid::now_v7();
            let model = setting::ActiveModel {
                id: Set(id),
                tenant_id: Set(tenant_id),
                scope: Set(params.scope.clone()),
                scope_id: Set(params.scope_id),
                setting_key: Set(params.key.clone()),
                setting_value: Set(params.value),
                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),
            };
            let inserted = model
                .insert(db)
                .await
                .map_err(|e| ConfigError::Validation(e.to_string()))?;

            event_bus
                .publish(
                    erp_core::events::DomainEvent::new(
                        "setting.created",
                        tenant_id,
                        serde_json::json!({
                            "setting_id": id,
                            "key": params.key,
                            "scope": params.scope,
                        }),
                    ),
                    db,
                )
                .await;

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

            Ok(Self::model_to_resp(&inserted))
        }
    }

    /// List all settings for a specific scope and scope_id, with pagination.
    pub async fn list_by_scope(
        scope: &str,
        scope_id: &Option<Uuid>,
        tenant_id: Uuid,
        pagination: &Pagination,
        db: &sea_orm::DatabaseConnection,
    ) -> ConfigResult<(Vec<SettingResp>, u64)> {
        let mut query = setting::Entity::find()
            .filter(setting::Column::TenantId.eq(tenant_id))
            .filter(setting::Column::Scope.eq(scope))
            .filter(setting::Column::DeletedAt.is_null());

        query = match scope_id {
            Some(id) => query.filter(setting::Column::ScopeId.eq(*id)),
            None => query.filter(setting::Column::ScopeId.is_null()),
        };

        let paginator = query.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 resps: Vec<SettingResp> = models.iter().map(Self::model_to_resp).collect();

        Ok((resps, total))
    }

    /// Soft-delete a setting by setting the `deleted_at` timestamp.
    /// Performs optimistic locking via version check.
    pub async fn delete(
        key: &str,
        scope: &str,
        scope_id: &Option<Uuid>,
        tenant_id: Uuid,
        operator_id: Uuid,
        version: i32,
        db: &sea_orm::DatabaseConnection,
    ) -> ConfigResult<()> {
        let mut query = setting::Entity::find()
            .filter(setting::Column::TenantId.eq(tenant_id))
            .filter(setting::Column::Scope.eq(scope))
            .filter(setting::Column::SettingKey.eq(key))
            .filter(setting::Column::DeletedAt.is_null());

        query = match scope_id {
            Some(id) => query.filter(setting::Column::ScopeId.eq(*id)),
            None => query.filter(setting::Column::ScopeId.is_null()),
        };

        let model = query
            .one(db)
            .await
            .map_err(|e| ConfigError::Validation(e.to_string()))?
            .ok_or_else(|| {
                ConfigError::NotFound(format!("设置 '{}' 在 '{}' 作用域下不存在", key, scope))
            })?;

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

        let setting_id = model.id;
        let mut active: setting::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), "setting.delete", "setting")
                .with_resource_id(setting_id),
            db,
        )
        .await;

        Ok(())
    }

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

    /// Find an exact setting match by key, scope, and scope_id.
    async fn find_exact(
        key: &str,
        scope: &str,
        scope_id: &Option<Uuid>,
        tenant_id: Uuid,
        db: &sea_orm::DatabaseConnection,
    ) -> ConfigResult<Option<SettingResp>> {
        let mut query = setting::Entity::find()
            .filter(setting::Column::TenantId.eq(tenant_id))
            .filter(setting::Column::Scope.eq(scope))
            .filter(setting::Column::SettingKey.eq(key))
            .filter(setting::Column::DeletedAt.is_null());

        // SQL 中 `= NULL` 永远返回 false，必须用 IS NULL 匹配 NULL 值
        query = match scope_id {
            Some(id) => query.filter(setting::Column::ScopeId.eq(*id)),
            None => query.filter(setting::Column::ScopeId.is_null()),
        };

        let model = query
            .one(db)
            .await
            .map_err(|e| ConfigError::Validation(e.to_string()))?;

        Ok(model.as_ref().map(Self::model_to_resp))
    }

    /// Build the fallback chain for hierarchical lookup.
    ///
    /// Returns a list of (scope, scope_id) tuples to try in order.
    pub(crate) fn fallback_chain(
        scope: &str,
        _scope_id: &Option<Uuid>,
        tenant_id: Uuid,
    ) -> ConfigResult<Vec<(String, Option<Uuid>)>> {
        match scope {
            SCOPE_USER => {
                // user -> org -> tenant -> platform
                // Note: We cannot resolve the actual org_id from user scope here
                // without a dependency on auth module. The caller should handle
                // org-level resolution externally if needed. We skip org fallback
                // and go directly to tenant.
                Ok(vec![
                    (SCOPE_TENANT.to_string(), Some(tenant_id)),
                    (SCOPE_PLATFORM.to_string(), None),
                ])
            }
            SCOPE_ORG => Ok(vec![
                (SCOPE_TENANT.to_string(), Some(tenant_id)),
                (SCOPE_PLATFORM.to_string(), None),
            ]),
            SCOPE_TENANT => Ok(vec![(SCOPE_PLATFORM.to_string(), None)]),
            SCOPE_PLATFORM => Ok(vec![]),
            _ => Err(ConfigError::Validation(format!(
                "不支持的作用域类型: '{}'",
                scope
            ))),
        }
    }

    /// Convert a SeaORM model to a response DTO.
    pub(crate) fn model_to_resp(model: &setting::Model) -> SettingResp {
        SettingResp {
            id: model.id,
            scope: model.scope.clone(),
            scope_id: model.scope_id,
            setting_key: model.setting_key.clone(),
            setting_value: model.setting_value.clone(),
            version: model.version,
        }
    }
}

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

    fn tid() -> Uuid {
        Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap()
    }

    // ---- fallback_chain ----

    #[test]
    fn fallback_user_scope_returns_tenant_then_platform() {
        let chain = SettingService::fallback_chain("user", &None, tid()).unwrap();
        assert_eq!(chain.len(), 2);
        assert_eq!(chain[0], ("tenant".to_string(), Some(tid())));
        assert_eq!(chain[1], ("platform".to_string(), None));
    }

    #[test]
    fn fallback_org_scope_returns_tenant_then_platform() {
        let chain = SettingService::fallback_chain("org", &None, tid()).unwrap();
        assert_eq!(chain.len(), 2);
        assert_eq!(chain[0], ("tenant".to_string(), Some(tid())));
        assert_eq!(chain[1], ("platform".to_string(), None));
    }

    #[test]
    fn fallback_tenant_scope_returns_platform() {
        let chain = SettingService::fallback_chain("tenant", &None, tid()).unwrap();
        assert_eq!(chain.len(), 1);
        assert_eq!(chain[0], ("platform".to_string(), None));
    }

    #[test]
    fn fallback_platform_scope_returns_empty() {
        let chain = SettingService::fallback_chain("platform", &None, tid()).unwrap();
        assert!(chain.is_empty());
    }

    #[test]
    fn fallback_invalid_scope_returns_error() {
        let result = SettingService::fallback_chain("invalid", &None, tid());
        assert!(result.is_err());
        match result.unwrap_err() {
            ConfigError::Validation(msg) => assert!(msg.contains("不支持的作用域")),
            other => panic!("期望 Validation，得到 {:?}", other),
        }
    }

    // ---- model_to_resp ----

    #[test]
    fn model_to_resp_maps_all_fields() {
        let m = setting::Model {
            id: Uuid::parse_str("00000000-0000-0000-0000-000000000010").unwrap(),
            tenant_id: tid(),
            scope: "tenant".to_string(),
            scope_id: Some(tid()),
            setting_key: "theme.primary_color".to_string(),
            setting_value: serde_json::json!("#1890ff"),
            created_at: chrono::Utc::now(),
            updated_at: chrono::Utc::now(),
            created_by: tid(),
            updated_by: tid(),
            deleted_at: None,
            version: 3,
        };
        let resp = SettingService::model_to_resp(&m);
        assert_eq!(resp.scope, "tenant");
        assert_eq!(resp.setting_key, "theme.primary_color");
        assert_eq!(resp.setting_value, serde_json::json!("#1890ff"));
        assert_eq!(resp.version, 3);
    }

    #[test]
    fn model_to_resp_null_scope_id() {
        let m = setting::Model {
            id: Uuid::parse_str("00000000-0000-0000-0000-000000000010").unwrap(),
            tenant_id: tid(),
            scope: "platform".to_string(),
            scope_id: None,
            setting_key: "language.default".to_string(),
            setting_value: serde_json::json!("zh-CN"),
            created_at: chrono::Utc::now(),
            updated_at: chrono::Utc::now(),
            created_by: tid(),
            updated_by: tid(),
            deleted_at: None,
            version: 1,
        };
        let resp = SettingService::model_to_resp(&m);
        assert_eq!(resp.scope_id, None);
    }
}
