//! 通用键值设置 — app_settings 表(KV,不适合 impl_repo! 宏)+ 列白名单(防 SQL 注入)

use std::sync::Arc;

use rusqlite::{params, Connection, OptionalExtension};
use tokio::sync::Mutex;

use df_types::error::{Error, Result};

use crate::db::Database;

use super::{now_millis_str, storage_err};

// ============================================================
// 通用键值设置 — app_settings 表
// ============================================================

/// 通用应用设置 KV Repo(表 `app_settings`)
///
/// 前端 localStorage 迁移目标:存主题/语言/AI 偏好/连接配置等,`value` 为 JSON 字符串。
/// KV 无固定 schema 记录结构,故不走 `impl_repo!` 宏,手写更直白。
pub struct SettingsRepo {
    conn: Arc<Mutex<Connection>>,
}

impl SettingsRepo {
    pub fn new(db: &Database) -> Self {
        Self { conn: db.conn() }
    }

    /// 取单个 key 的值(JSON 字符串),不存在返回 None
    pub async fn get(&self, key: &str) -> Result<Option<String>> {
        let conn = self.conn.clone();
        let key = key.to_owned();
        tokio::task::spawn_blocking(move || {
            let guard = conn.blocking_lock();
            let v = guard
                .query_row(
                    "SELECT value FROM app_settings WHERE key = ?1",
                    params![key],
                    |row| row.get::<_, String>(0),
                )
                .optional()
                .map_err(storage_err)?;
            Ok(v)
        })
        .await
        .map_err(storage_err)?
    }

    /// 写 key/value(`INSERT OR REPLACE`),刷新 updated_at
    pub async fn set(&self, key: &str, value: &str) -> Result<bool> {
        let conn = self.conn.clone();
        let key = key.to_owned();
        let value = value.to_owned();
        let now = now_millis_str();
        tokio::task::spawn_blocking(move || {
            let guard = conn.blocking_lock();
            let affected = guard
                .execute(
                    "INSERT OR REPLACE INTO app_settings (key, value, updated_at) VALUES (?1, ?2, ?3)",
                    params![key, value, now],
                )
                .map_err(storage_err)?;
            Ok(affected > 0)
        })
        .await
        .map_err(storage_err)?
    }

    /// 取全部 key/value
    pub async fn get_all(&self) -> Result<Vec<(String, String)>> {
        let conn = self.conn.clone();
        tokio::task::spawn_blocking(move || {
            let guard = conn.blocking_lock();
            let mut stmt = guard
                .prepare("SELECT key, value FROM app_settings")
                .map_err(storage_err)?;
            let rows = stmt
                .query_map([], |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)))
                .map_err(storage_err)?;
            let mut results = Vec::new();
            for r in rows {
                results.push(r.map_err(storage_err)?);
            }
            Ok(results)
        })
        .await
        .map_err(storage_err)?
    }

    /// 删除 key,返回是否实际删除
    pub async fn delete(&self, key: &str) -> Result<bool> {
        let conn = self.conn.clone();
        let key = key.to_owned();
        tokio::task::spawn_blocking(move || {
            let guard = conn.blocking_lock();
            let affected = guard
                .execute("DELETE FROM app_settings WHERE key = ?1", params![key])
                .map_err(storage_err)?;
            Ok(affected > 0)
        })
        .await
        .map_err(storage_err)?
    }
}

// ============================================================
// 列名白名单 — 防止 SQL 注入
// ============================================================

/// 按表返回允许用于 query / update_field 的列名。
///
/// 白名单两重作用:① 防注入(列名参数化前校验);② 按表隔离(update_task 误传
/// projects 的 "name" 会在校验阶段拒绝,而非靠 SQLite "no such column" 兜底报错)。
/// 专用更新路径的列不列入:knowledges.embedding(set_embedding)、projects.deleted_at
/// (soft_delete/restore)、tasks.deleted_at(soft_delete/restore)。未登记的表返回 None
/// → 放行(仅靠参数化防注入,向后兼容)。
pub fn allowed_columns_for(table: &str) -> Option<&'static [&'static str]> {
    Some(match table {
        "ideas" => &[
            // id\created_at 不列入 — 主键与创建时间不可通过通用 update_field 改写
            // (对标 tasks 白名单同款防护,防篡改主键/伪造创建时间)
            "title", "description", "status", "priority", "score", "tags", "source",
            "promoted_to", "ai_analysis", "scores", "related_ids", "updated_at",
        ],
        "projects" => &[
            "id", "name", "description", "status", "idea_id", "path", "stack", "created_at",
            "updated_at",
        ],
        "tasks" => &[
            // id/created_at 不列入:主键与创建时间不可通过通用 update_field 改写
            // (防篡改主键/伪造创建时间/跨项目移动)。
            // status 不列入(D-260616-04 status 收口):所有 status 改动走
            // advance_status_atomic(CAS SQL `WHERE id AND status=expected`,唯一 status 写入路径),
            // 不经通用 update_field。防 update_task IPC / AI 工具旁路改 status 绕过状态机
            // (can_transition 校验)与 review_rounds 累加。后人勿把 status 补回白名单。
            // review_rounds 不列入:它仅 advance_status_atomic 退回转换
            // (in_review→in_progress / testing→in_review)时原子 +1(收口:仅该专用路径可改,
            // update_task/update_field 白名单均不含)。后人勿把 review_rounds 补进白名单,
            // 否则破坏「review_rounds 唯一写入路径」收口、引入旁路写导致计数错乱。
            "project_id", "title", "description", "priority", "branch_name",
            "assignee",
            // workflow_def_id / base_branch: 预留字段,Git/workflow_defs 集成前无写入路径。
            // 当前推进链用硬编码三模板(task_workflow_templates.rs,不建 workflow_defs 表,
            // tasks.workflow_def_id 留 None),无任何代码写这两列。白名单列入仅为预留 +
            // 允许手动/未来填充,勿判死代码删除。base_branch 同理(code kind 闸门接 git 前预留)。
            "workflow_def_id", "base_branch",
            // output_json:ai_execute 写产出 / ai_self_review 读产出自审 / human_review 展示对象
            // (决策 a:task 中心,产出跟 task 走)。非状态机收口字段,合法可写。
            "output_json",
            // idea_id(任务关联灵感,1对1 单向):任务可关联/解关联一条灵感,
            // 非状态机收口字段,合法可写(idea_id 存在性由外键约束 + 上层校验兜底)。
            "idea_id",
            // 知识图谱 Phase 1 V29 三列(对标设计 §2.1):非状态机收口字段,合法可写。
            // - queue:管理维度池,走 move_task_queue IPC(设计 §五),IPC 层校验白名单 + 一致性约束。
            // - parent_id:父子纵向关联,1 级嵌套铁律由 IPC 层(create_task)校验不进白名单。
            // - content_json:结构化需求规格,走未来 update_content IPC(设计 §五)。
            // 注:status 仍不列入(D-260616-04 收口不变),父聚合/move_task_queue 联动 status
            // 走专用方法 set_status_for_aggregation,不经通用 update_field。
            "queue", "parent_id", "content_json",
            "updated_at",
            // TODO: project_id 跨表存在性校验待 commands/task.rs 层补。
            //   通用 CRUD 层(db repo)只懂表/列语义,不持有跨表业务约束(查 projects 表存在性)。
            //   project_id 当前可在白名单内改写,合法目标存在性由上层命令层校验。
        ],
        "releases" => &[
            "id", "project_id", "version", "status", "task_ids", "changelog", "created_at",
            "released_at",
        ],
        "branches" => &[
            "id", "project_id", "task_id", "name", "base", "status", "created_at", "updated_at",
            "merged_at",
        ],
        "workflow_executions" => &[
            "id", "name", "dag_json", "status", "triggered_by", "project_id", "task_id",
            "created_at", "completed_at",
        ],
        "node_executions" => &[
            "id", "workflow_id", "node_id", "node_type", "status", "input_json", "output_json",
            "error_message", "started_at", "completed_at",
        ],
        // api_key 不列入白名单:密钥写入应只走 update_full/insert + keyring 配套,
        // 禁止 update_field 旁路绕过 keyring(防明文写回 DB)。
        "ai_providers" => &[
            "id", "name", "provider_type", "base_url", "default_model", "models",
            "is_default", "config", "created_at", "updated_at",
        ],
        "ai_conversations" => &[
            "id", "title", "messages", "provider_id", "model", "models", "archived",
            "prompt_tokens", "completion_tokens", "created_at", "updated_at",
        ],
        "ai_tool_executions" => &[
            "id", "conversation_id", "message_id", "tool_call_id", "tool_name", "arguments",
            "result", "status", "risk_level", "requested_at", "executed_at", "decided_by",
        ],
        "knowledges" => &[
            "id", "kind", "title", "content", "tags", "status", "confidence", "reuse_count",
            "verified", "source_project", "source_ref", "reasoning", "created_at", "updated_at",
        ],
        "knowledge_events" => &[
            "id", "knowledge_id", "event_type", "source_ref", "context_json", "timestamp",
        ],
        // 知识图谱 Phase 3 V31(对标设计 §2.3):project_services 表有 updated_at,走 impl_repo! 宏
        // 标准 CRUD(insert/get_by_id/list_all/query/update_field/update_full/delete),故登记白名单。
        // id/created_at 不列入:主键与创建时间不可经通用 update_field 改写(对标 tasks/projects 防护)。
        // service_type 不列入白名单(类比 status 收口思路):service_type 是类型枚举,变更应走整行
        // update_full(过 service_type 应用层校验 + 凭证审查),而非 update_field 旁路绕过校验。
        "project_services" => &[
            "project_id", "name", "endpoint", "config_json", "environment",
            "remark", "updated_at",
        ],
        _ => return None,
    })
}

pub(crate) fn validate_column_name(field: &str, table: &str) -> Result<()> {
    match allowed_columns_for(table) {
        Some(cols) if cols.contains(&field) => Ok(()),
        Some(_) => Err(Error::Storage(format!("表 {} 不允许的字段名: {}", table, field))),
        // 未登记表保守拒绝:原放行 Ok,若未来未登记表走通用查询路径,列名直进字符串拼接即 SQL 注入;与 is_allowed_column 的 None=>false 对齐
        None => Err(Error::Storage(format!("表 {} 未登记列白名单,拒绝防注入", table))),
    }
}

/// 是否允许更新某表的某列(按表隔离白名单)。未登记表返回 false(保守拒绝,防误传)。
///
/// 供 AI 工具层等外部调用方复用 CRUD 白名单,避免双份字段列表不同步。
pub fn is_allowed_column(table: &str, field: &str) -> bool {
    match allowed_columns_for(table) {
        Some(cols) => cols.contains(&field),
        None => false,
    }
}
