//! 项目事件流 Repo:ProjectEventRepo(project_events 表,知识图谱 Phase 2 V30)
//!
//! 统一事件流——一张表承载跨实体(idea/task/workflow/knowledge/module/service/project)的
//! 全部事件,AI 精准检索的基础(回答"上周做了什么 / 这个任务为何 blocked / 决策何时做出")。
//! 对标设计 docs/02-架构设计/专项设计/项目知识图谱与任务队列系统-2026-06-26.md §2.4。
//!
//! 关键设计(对标 TaskLinkRepo 全专用路径):
//! - **追加型审计表**:只 INSERT,无 UPDATE/DELETE(历史不改可追溯,对标 knowledge_events V10 /
//!   idea_evaluations V22)。故不走 `impl_repo!` 宏(宏生成的 update/delete/update_field 对审计
//!   语义是错的),手写专用 insert + 三个查询方法。
//! - 不登记通用列白名单(`allowed_columns_for("project_events")` 返回 None):同 task_links,
//!   无通用 update_field 路径故无需白名单。`baseline_tests::all_known_tables_have_column_whitelist`
//!   的已知表数组不含本表(与 task_links 同款豁免)。
//! - `event_type` / `entity_type` 白名单**应用层校验**(类比 task_links.link_type),不进 DB 约束
//!   (保留扩展性,新增事件/实体类型无需迁移)。本 Repo insert **不做白名单校验**——埋点层
//!   (commands/IPC hook)是事件唯一生产者,字符串字面量自带约束;开放校验留给未来若开放外部写入。
//! - 埋点策略(设计 §2.4 hook/after)与 knowledge_events 双写均在调用方(commands 层)实现,
//!   非 Repo 职责。事件写入失败 best-effort 不阻断主操作(设计 §10.1)。

use std::sync::Arc;

use rusqlite::{params, Row};
use tokio::sync::Mutex;

use df_types::error::Result;

use crate::db::Database;
use crate::models::ProjectEventRecord;

use super::{now_millis_str, storage_err};

// ============================================================
// from_row 辅助函数
// ============================================================

fn project_event_from_row(row: &Row<'_>) -> std::result::Result<ProjectEventRecord, rusqlite::Error> {
    Ok(ProjectEventRecord {
        id: row.get("id")?,
        project_id: row.get("project_id")?,
        event_type: row.get("event_type")?,
        entity_type: row.get("entity_type")?,
        entity_id: row.get("entity_id")?,
        from_state: row.get("from_state")?,
        to_state: row.get("to_state")?,
        context_json: row.get("context_json")?,
        source: row.get("source")?,
        conversation_id: row.get("conversation_id")?,
        created_at: row.get("created_at")?,
    })
}

/// project_events SELECT 列清单(11 字段,防 COLS 漂移,对标 KNOWLEDGE_COLS)。
const PROJECT_EVENT_COLS: &str = "id,project_id,event_type,entity_type,entity_id,from_state,\
                                  to_state,context_json,source,conversation_id,created_at";

// ============================================================
// ProjectEventRepo
// ============================================================

/// 项目统一事件流表 Repo(project_events,V30,追加型审计表)。
///
/// 不走 `impl_repo!` 宏:① 追加型审计表无 UPDATE/DELETE(宏生成的 update/update_field/delete
/// 对审计语义是错的);② 表无 `updated_at`(宏的 update_field 硬编码 `SET ... updated_at = ?`
/// 会触发 "no such column: updated_at")。故手写专用方法,对标 TaskLinkRepo 全专用路径。
pub struct ProjectEventRepo {
    conn: Arc<Mutex<rusqlite::Connection>>,
}

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

    /// 追加一条事件(只 INSERT,无 UPDATE,审计语义不可篡改)。
    ///
    /// 返回插入的事件 id。`created_at` 由本方法内部取当前毫秒时间戳填充(调用方无需传),
    /// 与 IdeaRepo::insert(now_millis_str)同款收口——事件生产时机即落库时机,时间由存储层定。
    ///
    /// 不做 event_type / entity_type 白名单校验(埋点层是唯一生产者,字符串字面量自带约束)。
    pub async fn insert(&self, record: ProjectEventRecord) -> Result<String> {
        let conn = self.conn.clone();
        // created_at 由存储层覆盖(调用方传入的不可信),保证「事件生产时机即落库时机」。
        let now = now_millis_str();
        tokio::task::spawn_blocking(move || {
            let guard = conn.blocking_lock();
            guard
                .execute(
                    "INSERT INTO project_events \
                     (id, project_id, event_type, entity_type, entity_id, from_state, to_state, \
                      context_json, source, conversation_id, created_at) \
                     VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)",
                    params![
                        record.id,
                        record.project_id,
                        record.event_type,
                        record.entity_type,
                        record.entity_id,
                        record.from_state,
                        record.to_state,
                        record.context_json,
                        record.source,
                        record.conversation_id,
                        now,
                    ],
                )
                .map_err(storage_err)?;
            Ok(record.id)
        })
        .await
        .map_err(storage_err)?
    }

    /// 按项目查询事件流(time DESC,id DESC 兜底同毫秒并列,limit 钳制上限 200,
    /// 命中 idx_project_events_project)。
    ///
    /// AI / Dashboard 项目时间线视图用:取某项目最近 N 条事件(设计 §2.4 / §10.3.4)。
    /// 同毫秒插入的事件(高频埋点常见)以 id DESC 兜底——生产 id 是 ULID(字典序单调递增),
    /// 字典序 DESC 等价插入逆序,保证「最后发生的事件在最前」语义稳定(纯 created_at DESC
    /// 同毫秒并列时顺序不确定,审计表必须可复现)。
    pub async fn get_by_project(
        &self,
        project_id: &str,
        limit: u32,
    ) -> Result<Vec<ProjectEventRecord>> {
        let conn = self.conn.clone();
        let project_id = project_id.to_owned();
        // 钳制 limit 防滥用(最大 200,对标 KnowledgeEventsRepo::list_recent)
        let safe_limit = limit.min(200) as i64;
        tokio::task::spawn_blocking(move || {
            let guard = conn.blocking_lock();
            let sql = format!(
                "SELECT {PROJECT_EVENT_COLS} FROM project_events \
                 WHERE project_id = ?1 ORDER BY created_at DESC, id DESC LIMIT ?2"
            );
            let mut stmt = guard.prepare(&sql).map_err(storage_err)?;
            let rows = stmt
                .query_map(params![project_id, safe_limit], project_event_from_row)
                .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)?
    }

    /// 按实体反查事件流(命中 idx_project_events_entity)。
    ///
    /// AI「这个任务/灵感发生过什么」反查:取某实体的全部事件(时间倒序)。跨项目——
    /// 同一实体(如被移动过项目的任务)的全部历史事件都返回。同毫秒并列以 id DESC 兜底
    /// (语义稳定,见 get_by_project 说明)。
    pub async fn get_by_entity(
        &self,
        entity_type: &str,
        entity_id: &str,
    ) -> Result<Vec<ProjectEventRecord>> {
        let conn = self.conn.clone();
        let entity_type = entity_type.to_owned();
        let entity_id = entity_id.to_owned();
        tokio::task::spawn_blocking(move || {
            let guard = conn.blocking_lock();
            let sql = format!(
                "SELECT {PROJECT_EVENT_COLS} FROM project_events \
                 WHERE entity_type = ?1 AND entity_id = ?2 ORDER BY created_at DESC, id DESC"
            );
            let mut stmt = guard.prepare(&sql).map_err(storage_err)?;
            let rows = stmt
                .query_map(params![entity_type, entity_id], project_event_from_row)
                .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)?
    }

    /// 全表取每个项目的最新事件时间(`SELECT project_id, MAX(created_at) GROUP BY project_id`)。
    ///
    /// 供「项目列表最近活跃排序」使用:不改 projects 表 schema,通过 project_events 统一事件流
    /// 推导各项目真实活跃时间(任务/灵感/状态推进等业务事件),而非 projects.updated_at(后者随
    /// 元信息修改如改 description 也会刷新,不反映真实业务活跃)。
    ///
    /// 返回 `HashMap<project_id, latest_created_at>`。无事件的项目不在 map 中(调用方用
    /// COALESCE 回退 projects.updated_at)。命中 idx_project_events_project 的 project_id 维度,
    /// 单用户桌面应用事件量小无压力。
    ///
    /// 注:`MAX(created_at)` 在 SQLite 中对 TEXT(毫秒时间戳字符串)做字典序比较等价数值序
    /// (定长毫秒字符串),语义正确。
    pub async fn latest_activity_per_project(&self) -> Result<std::collections::HashMap<String, String>> {
        let conn = self.conn.clone();
        tokio::task::spawn_blocking(move || {
            let guard = conn.blocking_lock();
            let mut stmt = guard
                .prepare(
                    "SELECT project_id, MAX(created_at) AS latest \
                     FROM project_events GROUP BY project_id",
                )
                .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 map = std::collections::HashMap::new();
            for r in rows {
                let (pid, latest) = r.map_err(storage_err)?;
                map.insert(pid, latest);
            }
            Ok(map)
        })
        .await
        .map_err(storage_err)?
    }

    /// 跨项目列最近 N 条事件(全表 created_at DESC,id DESC 兜底,top-N,命中
    /// idx_project_events_project 的 created_at 维度)。
    ///
    /// 对标 KnowledgeEventsRepo::list_recent:供「全局事件流 / 跨项目时间线」视图使用
    /// (设计 §10.3.4 模式 B 事件流)。limit 上限钳制 200,防恶意/失误传超大值。
    /// 同毫秒并列以 id DESC 兜底(语义稳定,见 get_by_project 说明)。
    pub async fn list_recent(&self, limit: u32) -> Result<Vec<ProjectEventRecord>> {
        let conn = self.conn.clone();
        let safe_limit = limit.min(200) as i64;
        tokio::task::spawn_blocking(move || {
            let guard = conn.blocking_lock();
            let sql = format!(
                "SELECT {PROJECT_EVENT_COLS} FROM project_events \
                 ORDER BY created_at DESC, id DESC LIMIT ?1"
            );
            let mut stmt = guard.prepare(&sql).map_err(storage_err)?;
            let rows = stmt
                .query_map(params![safe_limit], project_event_from_row)
                .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)?
    }
}

// ============================================================
// 单元测试 — ProjectEventRepo insert + get_by_project + get_by_entity(内存 DB,
// 对标 task_link_repo 测试:先建占位 project 满足 FK,再插事件)
// ============================================================

#[cfg(test)]
mod tests {
    use super::*;
    use crate::crud::ProjectRepo;
    use crate::models::ProjectRecord;
    use df_types::types::ProjectStatus;

    /// 构造内存 DB + 占位 project(project_events.project_id FK 要求 projects 存在)。
    async fn setup() -> (crate::db::Database, ProjectEventRepo) {
        let db = crate::db::Database::open_in_memory()
            .await
            .expect("open_in_memory");
        let repo = ProjectEventRepo::new(&db);
        let project_repo = ProjectRepo::new(&db);
        project_repo
            .insert(ProjectRecord {
                id: "proj-1".to_string(),
                name: "proj-1".to_string(),
                description: String::new(),
                status: ProjectStatus::Planning,
                idea_id: None,
                path: None,
                stack: None,
                created_at: "1700000000000".to_string(),
                updated_at: "1700000000000".to_string(),
            })
            .await
            .unwrap();
        (db, repo)
    }

    /// 构造一条 ProjectEventRecord fixture(可指定 created_at 便于排序断言)。
    fn erec(id: &str, project_id: &str, created_at: &str) -> ProjectEventRecord {
        ProjectEventRecord {
            id: id.to_string(),
            project_id: project_id.to_string(),
            event_type: "task_advanced".to_string(),
            entity_type: Some("task".to_string()),
            entity_id: Some("task-A".to_string()),
            from_state: Some("todo".to_string()),
            to_state: Some("in_progress".to_string()),
            context_json: None,
            source: Some("ai".to_string()),
            conversation_id: Some("conv-1".to_string()),
            created_at: created_at.to_string(),
        }
    }

    #[tokio::test]
    async fn insert_and_read_back_full_fields() {
        let (_db, repo) = setup().await;
        let id = repo.insert(erec("e1", "proj-1", "1700000000001")).await.unwrap();
        assert_eq!(id, "e1");

        let got = repo
            .get_by_project("proj-1", 10)
            .await
            .unwrap()
            .into_iter()
            .next()
            .expect("应有 1 条事件");
        // 全字段回读校验
        assert_eq!(got.id, "e1");
        assert_eq!(got.project_id, "proj-1");
        assert_eq!(got.event_type, "task_advanced");
        assert_eq!(got.entity_type.as_deref(), Some("task"));
        assert_eq!(got.entity_id.as_deref(), Some("task-A"));
        assert_eq!(got.from_state.as_deref(), Some("todo"));
        assert_eq!(got.to_state.as_deref(), Some("in_progress"));
        assert!(got.context_json.is_none());
        assert_eq!(got.source.as_deref(), Some("ai"));
        assert_eq!(got.conversation_id.as_deref(), Some("conv-1"));
        // created_at 由存储层覆盖为当前毫秒(非 fixture 传入的旧值)
        assert_ne!(got.created_at, "1700000000001", "created_at 应由存储层覆盖");
        assert!(!got.created_at.is_empty());
    }

    #[tokio::test]
    async fn get_by_project_orders_desc_and_filters() {
        let (_db, repo) = setup().await;
        // 三条事件不同时间(create_at 由存储层覆盖,但插入顺序决定落库 created_at 单调递增,
        // 故 DESC 应按插入逆序返回——最后插的最前)
        repo.insert(erec("e1", "proj-1", "1")).await.unwrap();
        repo.insert(erec("e2", "proj-1", "1")).await.unwrap();
        repo.insert(erec("e3", "proj-1", "1")).await.unwrap();

        let got = repo.get_by_project("proj-1", 10).await.unwrap();
        assert_eq!(got.len(), 3, "应返回全部 3 条事件");
        // DESC:最后插入的 e3 在前
        assert_eq!(got[0].id, "e3");
        assert_eq!(got[1].id, "e2");
        assert_eq!(got[2].id, "e1");

        // 过滤:不存在的项目返回空
        let empty = repo.get_by_project("no-such-proj", 10).await.unwrap();
        assert!(empty.is_empty());
    }

    #[tokio::test]
    async fn get_by_project_limit_clamped() {
        let (_db, repo) = setup().await;
        for i in 0..5 {
            repo.insert(erec(&format!("e{i}"), "proj-1", "1"))
                .await
                .unwrap();
        }
        // limit=2 只返回 2 条(最新两条)
        let got = repo.get_by_project("proj-1", 2).await.unwrap();
        assert_eq!(got.len(), 2);
        // limit=99999 钳制为 200,但只有 5 条,返回 5 条
        let got_all = repo.get_by_project("proj-1", 99999).await.unwrap();
        assert_eq!(got_all.len(), 5, "钳制后仍返回全部存在的事件");
    }

    #[tokio::test]
    async fn get_by_entity_cross_project_and_filters() {
        let (_db, repo) = setup().await;
        // 同一实体 task-A 在 proj-1 下产生 2 条事件
        repo.insert(erec("e1", "proj-1", "1")).await.unwrap();
        repo.insert(erec("e2", "proj-1", "1")).await.unwrap();
        // 另一实体 task-B 1 条(改 entity_id)
        let mut other = erec("e3", "proj-1", "1");
        other.entity_id = Some("task-B".to_string());
        repo.insert(other).await.unwrap();

        // task-A 反查:返回 2 条(DESC)
        let got_a = repo.get_by_entity("task", "task-A").await.unwrap();
        assert_eq!(got_a.len(), 2);
        assert_eq!(got_a[0].id, "e2");
        assert_eq!(got_a[1].id, "e1");

        // task-B 反查:返回 1 条
        let got_b = repo.get_by_entity("task", "task-B").await.unwrap();
        assert_eq!(got_b.len(), 1);
        assert_eq!(got_b[0].id, "e3");

        // 不存在的实体返回空
        let empty = repo.get_by_entity("task", "no-such").await.unwrap();
        assert!(empty.is_empty());
    }

    #[tokio::test]
    async fn list_recent_cross_project() {
        let (_db, repo) = setup().await;
        for i in 0..3 {
            repo.insert(erec(&format!("e{i}"), "proj-1", "1"))
                .await
                .unwrap();
        }
        // list_recent 跨项目取最近 N 条(此处仅 proj-1)
        let got = repo.list_recent(10).await.unwrap();
        assert_eq!(got.len(), 3);
        // DESC:最后插入的最前
        assert_eq!(got[0].id, "e2");
    }

    /// latest_activity_per_project:每项目取 MAX(created_at),无事件项目不在 map 中。
    #[tokio::test]
    async fn latest_activity_per_project_groups() {
        let (db, repo) = setup().await;
        // 补一个 proj-2 占位 project(FK 要求)
        let project_repo = ProjectRepo::new(&db);
        project_repo
            .insert(ProjectRecord {
                id: "proj-2".to_string(),
                name: "proj-2".to_string(),
                description: String::new(),
                status: ProjectStatus::Planning,
                idea_id: None,
                path: None,
                stack: None,
                created_at: "1700000000000".to_string(),
                updated_at: "1700000000000".to_string(),
            })
            .await
            .unwrap();

        // proj-1: 插 2 条(created_at 由存储层覆盖,第 2 条更晚)
        repo.insert(erec("e1", "proj-1", "1")).await.unwrap();
        repo.insert(erec("e2", "proj-1", "1")).await.unwrap();
        // proj-2: 插 1 条
        repo.insert(erec("e3", "proj-2", "1")).await.unwrap();

        let map = repo.latest_activity_per_project().await.unwrap();
        // 两项目都在 map 中
        assert_eq!(map.len(), 2);
        // proj-1 的 latest = 最后插入的 e2 的 created_at(存储层覆盖的当前毫秒)
        let proj1_latest = map.get("proj-1").expect("proj-1 应在 map 中");
        let proj2_latest = map.get("proj-2").expect("proj-2 应在 map 中");
        // proj-1 最后插入(e2)晚于 proj-2(e3)的插入顺序:e3 在 e2 之前?
        // 插入顺序:e1(proj-1), e2(proj-1), e3(proj-2) → e3 的 created_at 最大。
        // 故 proj-2 的 latest 应 ≥ proj-1 的 latest。
        assert!(
            proj2_latest >= proj1_latest,
            "proj-2(e3 最后插)created_at 应 ≥ proj-1: proj2={} proj1={}",
            proj2_latest,
            proj1_latest
        );

        // 无事件项目不在 map(清空重建场景)
        let empty_map_empty = repo.latest_activity_per_project().await.unwrap();
        assert!(!empty_map_empty.contains_key("no-such"));
    }

    /// entity_type/entity_id 为 None(纯决策日志无明确实体)也能正常插入与查询。
    #[tokio::test]
    async fn insert_with_null_entity_fields() {
        let (_db, repo) = setup().await;
        let mut rec = erec("e1", "proj-1", "1");
        rec.event_type = "decision_made".to_string();
        rec.entity_type = None;
        rec.entity_id = None;
        rec.from_state = None;
        rec.to_state = None;
        repo.insert(rec).await.unwrap();

        let got = repo.get_by_project("proj-1", 10).await.unwrap();
        assert_eq!(got.len(), 1);
        assert!(got[0].entity_type.is_none());
        assert!(got[0].entity_id.is_none());
        assert!(got[0].from_state.is_none());
        assert!(got[0].to_state.is_none());
    }
}
