//! 审计记录写/查操作 + 审批历史 DTO + 审计面板查询 IPC。
//!
//! 第六批从 audit/mod.rs 抽离，行为零变更。包含：
//! - `audit_tool_call`：写一条工具执行审计记录（insert）
//! - `record_audit`：`audit_tool_call` 的别名，语义更清晰（"记一条审计"）
//! - `query_audit_history`：按条件查询审计历史记录
//! - `ToolExecutionDto`：传给前端的精简审计视图
//! - `list_tool_executions`：审批历史面板查询 IPC
//!
//! 依赖 audit/utils.rs 的 `truncate_chars` 做参数/结果截断，通过 `super::truncate_chars` 引用。

use serde::{Deserialize, Serialize};
use tauri::State;

use df_ai::ai_tools::RiskLevel;
use df_storage::crud::{AiToolExecutionRepo, AuditQuery};
use df_storage::models::AiToolExecutionRecord;
use df_types::types::new_id;

use crate::commands::err_str;
use crate::commands::now_millis;
use crate::state::AppState;

use super::risk_str;

/// 写一条工具执行审计记录（insert 失败不阻断主流程，故 `let _ =`）
///
/// `decided_by` 有值（auto/human）= 已决策执行 → 记 executed_at；
/// `None`（pending 待审批）→ executed_at 留空，待 audit_finalize 回填。
pub(crate) async fn audit_tool_call(
    repo: &AiToolExecutionRepo,
    conv_id: &str,
    tool_call_id: &str,
    tool_name: &str,
    arguments: &str,
    status: &str,
    risk_level: RiskLevel,
    result: Option<String>,
    decided_by: Option<&str>,
    message_id: Option<&str>,
) {
    let executed_at = if decided_by.is_some() { Some(now_millis()) } else { None };
    if let Err(e) = repo
        .insert(AiToolExecutionRecord {
            id: new_id(),
            conversation_id: Some(conv_id.to_string()),
            // P1 消息级溯源:message_id 由调用方(process_tool_calls)从
            // ContextManager 取当前 assistant 消息 id 传入(LLM 返回带 tool_calls 的
            // assistant 消息已 push 到 per_conv.messages,入口取末条 assistant id)。
            // None 表示无 assistant 消息(异常路径/老数据无 id),展示侧兼容。
            message_id: message_id.map(|s| s.to_string()),
            tool_call_id: tool_call_id.to_string(),
            tool_name: tool_name.to_string(),
            arguments: arguments.to_string(),
            result,
            status: status.to_string(),
            risk_level: risk_str(risk_level).to_string(),
            requested_at: now_millis(),
            executed_at,
            decided_by: decided_by.map(|s| s.to_string()),
        })
        .await
    {
        tracing::error!(
            "audit_tool_call: 写审计记录失败(conv={}, tool_call_id={}, tool={}): {}",
            conv_id,
            tool_call_id,
            tool_name,
            e
        );
    }
}

/// `audit_tool_call` 的语义别名，功能完全相同。
/// 命名更符合"记录一条审计"的调用意图，供新代码使用。
#[allow(dead_code)]
pub(crate) async fn record_audit(
    repo: &AiToolExecutionRepo,
    conv_id: &str,
    tool_call_id: &str,
    tool_name: &str,
    arguments: &str,
    status: &str,
    risk_level: RiskLevel,
    result: Option<String>,
    decided_by: Option<&str>,
    message_id: Option<&str>,
) {
    audit_tool_call(repo, conv_id, tool_call_id, tool_name, arguments, status, risk_level, result, decided_by, message_id).await;
}

/// 查询审计历史记录（分页，按 requested_at 倒序）。
///
/// 封装 `list_recent` 添加一层可读语义，方便未来扩展筛选条件。
#[allow(dead_code)]
pub(crate) async fn query_audit_history(
    repo: &AiToolExecutionRepo,
    limit: u32,
    offset: u32,
) -> Result<Vec<AiToolExecutionRecord>, String> {
    let limit = limit.min(200);
    repo.list_recent(limit, offset)
        .await
        .map_err(|e| format!("query_audit_history 查询失败: {}", e))
}

/// 审批历史 DTO（传给前端的精简视图，敏感字段截断防泄露）
///
/// arguments/result 在落库时是完整 JSON（可能含项目名/路径/长结果），审计面板只展示摘要，
/// 故截断到固定长度（参数 120 / 结果 160），既保留可读性又不泄露全量数据到前端 DOM。
#[derive(Debug, Clone, Serialize)]
pub struct ToolExecutionDto {
    pub id: String,
    pub conversation_id: Option<String>,
    pub tool_call_id: String,
    pub tool_name: String,
    /// 参数摘要（截断 120 字符，完整原值仍留库）
    pub arguments_brief: String,
    /// 结果摘要（截断 160 字符，None → 空串便于前端展示）
    pub result_brief: Option<String>,
    /// pending/approved/rejected/executing/completed/failed
    pub status: String,
    /// low/medium/high
    pub risk_level: String,
    pub requested_at: String,
    pub executed_at: Option<String>,
    /// human/auto，None 表示尚未决策
    pub decided_by: Option<String>,
}

/// 审批历史查询入参(前端透传,空值=不过滤)。
///
/// 复用 [`AuditQuery`](`df_storage::crud::AuditQuery`) 的字段语义:status/risk_level 精确匹配,
/// tool_keyword 走 tool_name LIKE。limit/offset 默认 50/0,storage 层钳制 limit ≤ 200。
///
/// `Deserialize`:Tauri IPC 从前端 JSON 反序列化。
#[derive(Debug, Clone, Default, Deserialize)]
pub struct ToolExecQuery {
    /// 状态精确匹配(pending/approved/rejected/executing/completed/failed/interrupted)
    pub status: Option<String>,
    /// 风险等级精确匹配(low/medium/high)
    pub risk_level: Option<String>,
    /// 工具名关键词(tool_name LIKE %kw%)
    pub tool_keyword: Option<String>,
    pub limit: Option<u32>,
    pub offset: Option<u32>,
}

impl From<ToolExecQuery> for AuditQuery {
    fn from(q: ToolExecQuery) -> Self {
        AuditQuery {
            status: q.status,
            risk_level: q.risk_level,
            tool_keyword: q.tool_keyword,
            limit: q.limit,
            offset: q.offset,
        }
    }
}

/// 审批历史分页结果(对标项目通用 `{items,total,has_more}` 结构)。
///
/// - `items`:当前页审计 DTO 列表
/// - `total`:满足筛选条件的总行数(忽略分页裁剪,前端用于"第 N 页 / 共 M 条"展示)
/// - `has_more`:基于 `loaded < total` 推断,而非"本页是否满 limit"启发式
#[derive(Debug, Clone, Serialize)]
pub struct ToolExecutionPage {
    pub items: Vec<ToolExecutionDto>,
    pub total: i64,
    pub has_more: bool,
}

/// 审批历史面板查询：按 requested_at 倒序（最新在前）分页返回工具调用审计记录。
///
/// 支持 status / risk_level / 工具名关键词筛选(WHERE 在后端收口,非前端 filter 当前页)。
/// 默认 limit=50 / offset=0（第一页）。limit 在 storage 层钳制 ≤200 防滥用。
/// 敏感字段（arguments/result）截断成摘要返回，完整原值仍留库。
///
/// 返回 `{items,total,has_more}`:total 为满足筛选条件的真实总数(独立 COUNT 查询),
/// has_more 基于 `offset + items.len() < total` 推断。
#[tauri::command]
pub async fn list_tool_executions(
    state: State<'_, AppState>,
    query: Option<ToolExecQuery>,
) -> Result<ToolExecutionPage, String> {
    let q = query.unwrap_or_default();
    let limit = q.limit.unwrap_or(50);
    let offset = q.offset.unwrap_or(0);
    let audit_q = AuditQuery::from(q);

    let records = state
        .ai_tool_executions
        .list_by_query(&audit_q)
        .await
        .map_err(err_str)?;
    let total = state
        .ai_tool_executions
        .count_by_query(&audit_q)
        .await
        .map_err(err_str)?;

    let items: Vec<ToolExecutionDto> = records
        .into_iter()
        .map(|r| ToolExecutionDto {
            id: r.id,
            conversation_id: r.conversation_id,
            tool_call_id: r.tool_call_id,
            tool_name: r.tool_name,
            arguments_brief: super::truncate_chars(&r.arguments, 120),
            result_brief: r.result.map(|s| super::truncate_chars(&s, 160)),
            status: r.status,
            risk_level: r.risk_level,
            requested_at: r.requested_at,
            executed_at: r.executed_at,
            decided_by: r.decided_by,
        })
        .collect();
    let has_more = (offset as i64 + items.len() as i64) < total;

    Ok(ToolExecutionPage {
        items,
        total,
        has_more,
    })
}
