use tauri::State;

use crate::db::models::{
    AiConversation, AiMessage, Cluster, ImageAttachment, Issue, IssueDetail, IssueFilter,
    IssueSummary, IssueUpdate, LogFile, PortForward, ResolutionStep, TimelineEvent,
};
use crate::state::AppState;

#[tauri::command]
pub async fn create_issue(
    title: String,
    description: String,
    severity: String,
    category: String,
    state: State<'_, AppState>,
) -> Result<Issue, String> {
    let issue = Issue::new(title, description, severity, category);

    let db = state.db.lock().map_err(|e| e.to_string())?;
    db.execute(
        "INSERT INTO issues (id, title, description, severity, status, category, source, created_at, updated_at, resolved_at, assigned_to, tags) \
         VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)",
        rusqlite::params![
            issue.id,
            issue.title,
            issue.description,
            issue.severity,
            issue.status,
            issue.category,
            issue.source,
            issue.created_at,
            issue.updated_at,
            issue.resolved_at,
            issue.assigned_to,
            issue.tags,
        ],
    )
    .map_err(|e| e.to_string())?;

    Ok(issue)
}

#[tauri::command]
pub async fn get_issue(
    issue_id: String,
    state: State<'_, AppState>,
) -> Result<IssueDetail, String> {
    let db = state.db.lock().map_err(|e| e.to_string())?;

    // Load issue
    let mut stmt = db
        .prepare(
            "SELECT id, title, description, severity, status, category, source, \
             created_at, updated_at, resolved_at, assigned_to, tags \
             FROM issues WHERE id = ?1",
        )
        .map_err(|e| e.to_string())?;

    let issue = stmt
        .query_row([&issue_id], |row| {
            Ok(Issue {
                id: row.get(0)?,
                title: row.get(1)?,
                description: row.get(2)?,
                severity: row.get(3)?,
                status: row.get(4)?,
                category: row.get(5)?,
                source: row.get(6)?,
                created_at: row.get(7)?,
                updated_at: row.get(8)?,
                resolved_at: row.get(9)?,
                assigned_to: row.get(10)?,
                tags: row.get(11)?,
            })
        })
        .map_err(|e| e.to_string())?;

    // Load log files
    let mut lf_stmt = db
        .prepare(
            "SELECT id, issue_id, file_name, file_path, file_size, mime_type, content_hash, uploaded_at, redacted \
             FROM log_files WHERE issue_id = ?1 ORDER BY uploaded_at ASC",
        )
        .map_err(|e| e.to_string())?;
    let log_files: Vec<LogFile> = lf_stmt
        .query_map([&issue_id], |row| {
            Ok(LogFile {
                id: row.get(0)?,
                issue_id: row.get(1)?,
                file_name: row.get(2)?,
                file_path: row.get(3)?,
                file_size: row.get(4)?,
                mime_type: row.get(5)?,
                content_hash: row.get(6)?,
                uploaded_at: row.get(7)?,
                redacted: row.get::<_, i32>(8)? != 0,
            })
        })
        .map_err(|e| e.to_string())?
        .filter_map(|r| r.ok())
        .collect();

    // Load image attachments
    let mut img_stmt = db
        .prepare(
            "SELECT id, issue_id, file_name, file_path, file_size, mime_type, upload_hash, uploaded_at, pii_warning_acknowledged, is_paste \
             FROM image_attachments WHERE issue_id = ?1 ORDER BY uploaded_at ASC",
        )
        .map_err(|e| e.to_string())?;
    let image_attachments: Vec<ImageAttachment> = img_stmt
        .query_map([&issue_id], |row| {
            Ok(ImageAttachment {
                id: row.get(0)?,
                issue_id: row.get(1)?,
                file_name: row.get(2)?,
                file_path: row.get(3)?,
                file_size: row.get(4)?,
                mime_type: row.get(5)?,
                upload_hash: row.get(6)?,
                uploaded_at: row.get(7)?,
                pii_warning_acknowledged: row.get::<_, i32>(8)? != 0,
                is_paste: row.get::<_, i32>(9)? != 0,
            })
        })
        .map_err(|e| e.to_string())?
        .filter_map(|r| r.ok())
        .collect();

    // Load resolution steps (5-whys)
    let mut rs_stmt = db
        .prepare(
            "SELECT id, issue_id, step_order, why_question, answer, evidence, created_at \
             FROM resolution_steps WHERE issue_id = ?1 ORDER BY step_order ASC",
        )
        .map_err(|e| e.to_string())?;
    let resolution_steps: Vec<ResolutionStep> = rs_stmt
        .query_map([&issue_id], |row| {
            Ok(ResolutionStep {
                id: row.get(0)?,
                issue_id: row.get(1)?,
                step_order: row.get(2)?,
                why_question: row.get(3)?,
                answer: row.get(4)?,
                evidence: row.get(5)?,
                created_at: row.get(6)?,
            })
        })
        .map_err(|e| e.to_string())?
        .filter_map(|r| r.ok())
        .collect();

    // Load conversations
    let mut conv_stmt = db
        .prepare(
            "SELECT id, issue_id, provider, model, created_at, title \
             FROM ai_conversations WHERE issue_id = ?1 ORDER BY created_at ASC",
        )
        .map_err(|e| e.to_string())?;
    let conversations: Vec<AiConversation> = conv_stmt
        .query_map([&issue_id], |row| {
            Ok(AiConversation {
                id: row.get(0)?,
                issue_id: row.get(1)?,
                provider: row.get(2)?,
                model: row.get(3)?,
                created_at: row.get(4)?,
                title: row.get(5)?,
            })
        })
        .map_err(|e| e.to_string())?
        .filter_map(|r| r.ok())
        .collect();

    // Load timeline events
    let mut te_stmt = db
        .prepare(
            "SELECT id, issue_id, event_type, description, metadata, created_at \
             FROM timeline_events WHERE issue_id = ?1 ORDER BY created_at ASC",
        )
        .map_err(|e| e.to_string())?;
    let timeline_events: Vec<TimelineEvent> = te_stmt
        .query_map([&issue_id], |row| {
            Ok(TimelineEvent {
                id: row.get(0)?,
                issue_id: row.get(1)?,
                event_type: row.get(2)?,
                description: row.get(3)?,
                metadata: row.get(4)?,
                created_at: row.get(5)?,
            })
        })
        .map_err(|e| e.to_string())?
        .filter_map(|r| r.ok())
        .collect();

    Ok(IssueDetail {
        issue,
        log_files,
        image_attachments,
        resolution_steps,
        conversations,
        timeline_events,
    })
}

#[tauri::command]
pub async fn update_issue(
    issue_id: String,
    updates: IssueUpdate,
    state: State<'_, AppState>,
) -> Result<Issue, String> {
    let db = state.db.lock().map_err(|e| e.to_string())?;
    let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();

    if let Some(ref title) = updates.title {
        db.execute(
            "UPDATE issues SET title = ?1, updated_at = ?2 WHERE id = ?3",
            rusqlite::params![title, now, issue_id],
        )
        .map_err(|e| e.to_string())?;
    }
    if let Some(ref description) = updates.description {
        db.execute(
            "UPDATE issues SET description = ?1, updated_at = ?2 WHERE id = ?3",
            rusqlite::params![description, now, issue_id],
        )
        .map_err(|e| e.to_string())?;
    }
    if let Some(ref severity) = updates.severity {
        db.execute(
            "UPDATE issues SET severity = ?1, updated_at = ?2 WHERE id = ?3",
            rusqlite::params![severity, now, issue_id],
        )
        .map_err(|e| e.to_string())?;
    }
    if let Some(ref status) = updates.status {
        db.execute(
            "UPDATE issues SET status = ?1, updated_at = ?2 WHERE id = ?3",
            rusqlite::params![status, now, issue_id],
        )
        .map_err(|e| e.to_string())?;
        if status == "resolved" {
            db.execute(
                "UPDATE issues SET resolved_at = ?1 WHERE id = ?2",
                rusqlite::params![now, issue_id],
            )
            .map_err(|e| e.to_string())?;
        }
    }
    if let Some(ref category) = updates.category {
        db.execute(
            "UPDATE issues SET category = ?1, updated_at = ?2 WHERE id = ?3",
            rusqlite::params![category, now, issue_id],
        )
        .map_err(|e| e.to_string())?;
    }
    if let Some(ref assigned_to) = updates.assigned_to {
        db.execute(
            "UPDATE issues SET assigned_to = ?1, updated_at = ?2 WHERE id = ?3",
            rusqlite::params![assigned_to, now, issue_id],
        )
        .map_err(|e| e.to_string())?;
    }
    if let Some(ref tags) = updates.tags {
        db.execute(
            "UPDATE issues SET tags = ?1, updated_at = ?2 WHERE id = ?3",
            rusqlite::params![tags, now, issue_id],
        )
        .map_err(|e| e.to_string())?;
    }

    // Fetch and return updated issue
    let mut stmt = db
        .prepare(
            "SELECT id, title, description, severity, status, category, source, \
             created_at, updated_at, resolved_at, assigned_to, tags \
             FROM issues WHERE id = ?1",
        )
        .map_err(|e| e.to_string())?;
    stmt.query_row([&issue_id], |row| {
        Ok(Issue {
            id: row.get(0)?,
            title: row.get(1)?,
            description: row.get(2)?,
            severity: row.get(3)?,
            status: row.get(4)?,
            category: row.get(5)?,
            source: row.get(6)?,
            created_at: row.get(7)?,
            updated_at: row.get(8)?,
            resolved_at: row.get(9)?,
            assigned_to: row.get(10)?,
            tags: row.get(11)?,
        })
    })
    .map_err(|e| e.to_string())
}

#[tauri::command]
pub async fn delete_issue(issue_id: String, state: State<'_, AppState>) -> Result<(), String> {
    let db = state.db.lock().map_err(|e| e.to_string())?;

    // Delete related records (CASCADE should handle this, but be explicit)
    db.execute("DELETE FROM ai_messages WHERE conversation_id IN (SELECT id FROM ai_conversations WHERE issue_id = ?1)", [&issue_id])
        .map_err(|e| e.to_string())?;
    db.execute(
        "DELETE FROM ai_conversations WHERE issue_id = ?1",
        [&issue_id],
    )
    .map_err(|e| e.to_string())?;
    db.execute(
        "DELETE FROM pii_spans WHERE log_file_id IN (SELECT id FROM log_files WHERE issue_id = ?1)",
        [&issue_id],
    )
    .map_err(|e| e.to_string())?;
    db.execute("DELETE FROM log_files WHERE issue_id = ?1", [&issue_id])
        .map_err(|e| e.to_string())?;
    db.execute(
        "DELETE FROM image_attachments WHERE issue_id = ?1",
        [&issue_id],
    )
    .map_err(|e| e.to_string())?;
    db.execute(
        "DELETE FROM resolution_steps WHERE issue_id = ?1",
        [&issue_id],
    )
    .map_err(|e| e.to_string())?;
    db.execute(
        "DELETE FROM timeline_events WHERE issue_id = ?1",
        [&issue_id],
    )
    .map_err(|e| e.to_string())?;
    db.execute("DELETE FROM issues WHERE id = ?1", [&issue_id])
        .map_err(|e| e.to_string())?;

    Ok(())
}

#[tauri::command]
pub async fn list_issues(
    filter: IssueFilter,
    state: State<'_, AppState>,
) -> Result<Vec<IssueSummary>, String> {
    let db = state.db.lock().map_err(|e| e.to_string())?;

    let limit = filter.limit.unwrap_or(50);
    let offset = filter.offset.unwrap_or(0);

    let mut sql = String::from(
        "SELECT DISTINCT i.id, i.title, i.severity, i.status, i.category, i.created_at, i.updated_at, \
         (SELECT COUNT(*) FROM log_files lf WHERE lf.issue_id = i.id) as log_count, \
         (SELECT COUNT(*) FROM resolution_steps rs WHERE rs.issue_id = i.id) as step_count \
         FROM issues i WHERE 1=1",
    );
    let mut params: Vec<Box<dyn rusqlite::types::ToSql>> = vec![];

    if let Some(ref status) = filter.status {
        sql.push_str(&format!(
            " AND i.status = ?{index}",
            index = params.len() + 1
        ));
        params.push(Box::new(status.clone()));
    }
    if let Some(ref severity) = filter.severity {
        sql.push_str(&format!(
            " AND i.severity = ?{index}",
            index = params.len() + 1
        ));
        params.push(Box::new(severity.clone()));
    }
    if let Some(ref category) = filter.category {
        sql.push_str(&format!(
            " AND i.category = ?{index}",
            index = params.len() + 1
        ));
        params.push(Box::new(category.clone()));
    }
    if let Some(ref domain) = filter.domain {
        sql.push_str(&format!(
            " AND i.category = ?{index}",
            index = params.len() + 1
        ));
        params.push(Box::new(domain.clone()));
    }
    if let Some(ref search) = filter.search {
        let pattern = format!("%{search}%");
        let idx = params.len() + 1;
        sql.push_str(&format!(
            " AND (i.title LIKE ?{idx} OR i.description LIKE ?{idx} OR i.category LIKE ?{idx} \
             OR EXISTS (SELECT 1 FROM ai_messages am \
                        JOIN ai_conversations ac ON ac.id = am.conversation_id \
                        WHERE ac.issue_id = i.id AND am.content LIKE ?{idx}) \
             OR EXISTS (SELECT 1 FROM resolution_steps rs \
                        WHERE rs.issue_id = i.id \
                        AND (rs.why_question LIKE ?{idx} OR rs.answer LIKE ?{idx} OR rs.evidence LIKE ?{idx})) \
             OR EXISTS (SELECT 1 FROM log_files lf \
                        WHERE lf.issue_id = i.id AND lf.file_name LIKE ?{idx}) \
             OR EXISTS (SELECT 1 FROM timeline_events te \
                        WHERE te.issue_id = i.id AND te.description LIKE ?{idx}))",
        ));
        params.push(Box::new(pattern));
    }

    sql.push_str(" ORDER BY i.updated_at DESC");
    sql.push_str(&format!(
        " LIMIT ?{limit_index} OFFSET ?{offset_index}",
        limit_index = params.len() + 1,
        offset_index = params.len() + 2
    ));
    params.push(Box::new(limit));
    params.push(Box::new(offset));

    let param_refs: Vec<&dyn rusqlite::types::ToSql> = params.iter().map(|p| p.as_ref()).collect();

    let mut stmt = db.prepare(&sql).map_err(|e| e.to_string())?;
    let issues = stmt
        .query_map(param_refs.as_slice(), |row| {
            Ok(IssueSummary {
                id: row.get(0)?,
                title: row.get(1)?,
                severity: row.get(2)?,
                status: row.get(3)?,
                category: row.get(4)?,
                created_at: row.get(5)?,
                updated_at: row.get(6)?,
                log_count: row.get(7)?,
                step_count: row.get(8)?,
            })
        })
        .map_err(|e| e.to_string())?
        .filter_map(|r| r.ok())
        .collect();

    Ok(issues)
}

#[tauri::command]
pub async fn search_issues(
    query: String,
    state: State<'_, AppState>,
) -> Result<Vec<IssueSummary>, String> {
    let filter = IssueFilter {
        search: Some(query),
        limit: Some(50),
        ..Default::default()
    };
    list_issues(filter, state).await
}

#[tauri::command]
pub async fn get_issue_messages(
    issue_id: String,
    state: State<'_, AppState>,
) -> Result<Vec<AiMessage>, String> {
    let db = state.db.lock().map_err(|e| e.to_string())?;
    let mut stmt = db
        .prepare(
            "SELECT am.id, am.conversation_id, am.role, am.content, am.token_count, am.created_at \
             FROM ai_messages am \
             JOIN ai_conversations ac ON ac.id = am.conversation_id \
             WHERE ac.issue_id = ?1 \
             ORDER BY am.created_at ASC",
        )
        .map_err(|e| e.to_string())?;
    let messages = stmt
        .query_map([&issue_id], |row| {
            Ok(AiMessage {
                id: row.get(0)?,
                conversation_id: row.get(1)?,
                role: row.get(2)?,
                content: row.get(3)?,
                token_count: row.get(4)?,
                created_at: row.get(5)?,
            })
        })
        .map_err(|e| e.to_string())?
        .filter_map(|r| r.ok())
        .collect();
    Ok(messages)
}

#[tauri::command]
pub async fn add_five_why(
    issue_id: String,
    step_order: i64,
    why_question: String,
    answer: String,
    evidence: String,
    state: State<'_, AppState>,
) -> Result<ResolutionStep, String> {
    let step = ResolutionStep::new(issue_id.clone(), step_order, why_question, answer, evidence);

    let db = state.db.lock().map_err(|e| e.to_string())?;
    db.execute(
        "INSERT INTO resolution_steps (id, issue_id, step_order, why_question, answer, evidence, created_at) \
         VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
        rusqlite::params![
            step.id,
            step.issue_id,
            step.step_order,
            step.why_question,
            step.answer,
            step.evidence,
            step.created_at,
        ],
    )
    .map_err(|e| e.to_string())?;

    // Update issue timestamp
    let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
    db.execute(
        "UPDATE issues SET updated_at = ?1 WHERE id = ?2",
        rusqlite::params![now, issue_id],
    )
    .map_err(|e| e.to_string())?;

    Ok(step)
}

#[tauri::command]
pub async fn update_five_why(
    step_id: String,
    answer: String,
    evidence: Option<String>,
    state: State<'_, AppState>,
) -> Result<(), String> {
    let db = state.db.lock().map_err(|e| e.to_string())?;

    if let Some(ref ev) = evidence {
        db.execute(
            "UPDATE resolution_steps SET answer = ?1, evidence = ?2 WHERE id = ?3",
            rusqlite::params![answer, ev, step_id],
        )
        .map_err(|e| e.to_string())?;
    } else {
        db.execute(
            "UPDATE resolution_steps SET answer = ?1 WHERE id = ?2",
            rusqlite::params![answer, step_id],
        )
        .map_err(|e| e.to_string())?;
    }

    Ok(())
}

const VALID_EVENT_TYPES: &[&str] = &[
    "triage_started",
    "log_uploaded",
    "why_level_advanced",
    "root_cause_identified",
    "rca_generated",
    "postmortem_generated",
    "document_exported",
];

#[tauri::command]
pub async fn add_timeline_event(
    issue_id: String,
    event_type: String,
    description: String,
    metadata: Option<String>,
    state: State<'_, AppState>,
) -> Result<TimelineEvent, String> {
    if !VALID_EVENT_TYPES.contains(&event_type.as_str()) {
        return Err(format!("Invalid event_type: {event_type}"));
    }

    let meta = metadata.unwrap_or_else(|| "{}".to_string());
    if meta.len() > 10240 {
        return Err("metadata exceeds maximum size of 10KB".to_string());
    }
    serde_json::from_str::<serde_json::Value>(&meta)
        .map_err(|_| "metadata must be valid JSON".to_string())?;

    let event = TimelineEvent::new(
        issue_id.clone(),
        event_type.clone(),
        description.clone(),
        meta,
    );

    let mut db = state.db.lock().map_err(|e| e.to_string())?;
    let tx = db.transaction().map_err(|e| e.to_string())?;

    tx.execute(
        "INSERT INTO timeline_events (id, issue_id, event_type, description, metadata, created_at) \
         VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
        rusqlite::params![
            event.id,
            event.issue_id,
            event.event_type,
            event.description,
            event.metadata,
            event.created_at,
        ],
    )
    .map_err(|e| e.to_string())?;

    crate::audit::log::write_audit_event(
        &tx,
        &event_type,
        "issue",
        &issue_id,
        &serde_json::json!({ "description": description, "metadata": event.metadata }).to_string(),
    )
    .map_err(|_| "Failed to write security audit entry".to_string())?;

    let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
    tx.execute(
        "UPDATE issues SET updated_at = ?1 WHERE id = ?2",
        rusqlite::params![now, issue_id],
    )
    .map_err(|e| e.to_string())?;

    tx.commit().map_err(|e| e.to_string())?;

    Ok(event)
}

#[tauri::command]
pub async fn get_timeline_events(
    issue_id: String,
    state: State<'_, AppState>,
) -> Result<Vec<TimelineEvent>, String> {
    let db = state.db.lock().map_err(|e| e.to_string())?;
    let mut stmt = db
        .prepare(
            "SELECT id, issue_id, event_type, description, metadata, created_at \
             FROM timeline_events WHERE issue_id = ?1 ORDER BY created_at ASC",
        )
        .map_err(|e| e.to_string())?;
    let events = stmt
        .query_map([&issue_id], |row| {
            Ok(TimelineEvent {
                id: row.get(0)?,
                issue_id: row.get(1)?,
                event_type: row.get(2)?,
                description: row.get(3)?,
                metadata: row.get(4)?,
                created_at: row.get(5)?,
            })
        })
        .map_err(|e| e.to_string())?
        .filter_map(|r| r.ok())
        .collect();
    Ok(events)
}

#[cfg(test)]
mod tests {
    use rusqlite::Connection;

    fn setup_test_db() -> Connection {
        let conn = Connection::open_in_memory().unwrap();
        crate::db::migrations::run_migrations(&conn).unwrap();
        conn
    }

    fn insert_issue(conn: &Connection, id: &str, title: &str, description: &str) {
        let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
        conn.execute(
            "INSERT INTO issues (id, title, description, severity, status, category, source, created_at, updated_at, assigned_to, tags) \
             VALUES (?1, ?2, ?3, 'medium', 'open', 'general', 'manual', ?4, ?5, '', '[]')",
            rusqlite::params![id, title, description, &now, &now],
        )
        .unwrap();
    }

    fn run_search_query(conn: &Connection, search: &str) -> Vec<String> {
        let pattern = format!("%{search}%");
        let idx = 1;
        let sql = format!(
            "SELECT DISTINCT i.id, i.title, i.severity, i.status, i.category, i.created_at, i.updated_at, \
             (SELECT COUNT(*) FROM log_files lf2 WHERE lf2.issue_id = i.id) as log_count, \
             (SELECT COUNT(*) FROM resolution_steps rs2 WHERE rs2.issue_id = i.id) as step_count \
             FROM issues i WHERE 1=1 \
             AND (i.title LIKE ?{idx} OR i.description LIKE ?{idx} OR i.category LIKE ?{idx} \
              OR EXISTS (SELECT 1 FROM ai_messages am \
                         JOIN ai_conversations ac ON ac.id = am.conversation_id \
                         WHERE ac.issue_id = i.id AND am.content LIKE ?{idx}) \
              OR EXISTS (SELECT 1 FROM resolution_steps rs \
                         WHERE rs.issue_id = i.id \
                         AND (rs.why_question LIKE ?{idx} OR rs.answer LIKE ?{idx} OR rs.evidence LIKE ?{idx})) \
              OR EXISTS (SELECT 1 FROM log_files lf \
                         WHERE lf.issue_id = i.id AND lf.file_name LIKE ?{idx}) \
              OR EXISTS (SELECT 1 FROM timeline_events te \
                         WHERE te.issue_id = i.id AND te.description LIKE ?{idx})) \
             ORDER BY i.updated_at DESC"
        );
        let mut stmt = conn.prepare(&sql).unwrap();
        stmt.query_map([&pattern], |row| row.get::<_, String>(0))
            .unwrap()
            .filter_map(|r| r.ok())
            .collect()
    }

    #[test]
    fn test_search_finds_by_title() {
        let conn = setup_test_db();
        insert_issue(&conn, "issue-1", "Kubernetes OOM crash", "No details");
        insert_issue(&conn, "issue-2", "Network timeout", "No details");

        let results = run_search_query(&conn, "Kubernetes");
        assert_eq!(results, vec!["issue-1"]);
    }

    #[test]
    fn test_search_finds_by_description() {
        let conn = setup_test_db();
        insert_issue(
            &conn,
            "issue-1",
            "Generic title",
            "The pod was killed due to memory pressure",
        );
        insert_issue(&conn, "issue-2", "Other", "All fine");

        let results = run_search_query(&conn, "memory pressure");
        assert_eq!(results, vec!["issue-1"]);
    }

    #[test]
    fn test_search_finds_by_ai_message_content() {
        let conn = setup_test_db();
        insert_issue(&conn, "issue-1", "Unrelated title", "Unrelated desc");
        let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
        conn.execute(
            "INSERT INTO ai_conversations (id, issue_id, provider, model, created_at, title) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
            rusqlite::params!["conv-1", "issue-1", "openai", "gpt-4o", &now, "Conv"],
        ).unwrap();
        conn.execute(
            "INSERT INTO ai_messages (id, conversation_id, role, content, token_count, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
            rusqlite::params!["msg-1", "conv-1", "assistant", "The root cause is a deadlock in PostgreSQL", 10, &now],
        ).unwrap();

        let results = run_search_query(&conn, "deadlock in PostgreSQL");
        assert_eq!(results, vec!["issue-1"]);
    }

    #[test]
    fn test_search_finds_by_resolution_step_answer() {
        let conn = setup_test_db();
        insert_issue(&conn, "issue-1", "Unrelated title", "Unrelated desc");
        let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
        conn.execute(
            "INSERT INTO resolution_steps (id, issue_id, step_order, why_question, answer, evidence, created_at) \
             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
            rusqlite::params!["step-1", "issue-1", 1, "Why did it fail?", "Connection pool exhausted", "metrics dashboard", &now],
        ).unwrap();

        let results = run_search_query(&conn, "pool exhausted");
        assert_eq!(results, vec!["issue-1"]);
    }

    #[test]
    fn test_search_finds_by_log_file_name() {
        let conn = setup_test_db();
        insert_issue(&conn, "issue-1", "Unrelated title", "Unrelated desc");
        let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
        conn.execute(
            "INSERT INTO log_files (id, issue_id, file_name, file_path, file_size, mime_type, content_hash, uploaded_at, redacted) \
             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
            rusqlite::params!["lf-1", "issue-1", "nginx-error-2025.log", "/tmp/nginx-error-2025.log", 1024, "text/plain", "abc", &now, 0],
        ).unwrap();

        let results = run_search_query(&conn, "nginx-error");
        assert_eq!(results, vec!["issue-1"]);
    }

    #[test]
    fn test_search_finds_by_timeline_description() {
        let conn = setup_test_db();
        insert_issue(&conn, "issue-1", "Unrelated title", "Unrelated desc");
        conn.execute(
            "INSERT INTO timeline_events (id, issue_id, event_type, description, metadata, created_at) \
             VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
            rusqlite::params!["te-1", "issue-1", "triage_started", "Engineer started investigating DNS resolution failure", "{}", "2025-01-15 10:00:00"],
        ).unwrap();

        let results = run_search_query(&conn, "DNS resolution");
        assert_eq!(results, vec!["issue-1"]);
    }

    #[test]
    fn test_search_no_duplicates_for_multiple_matches() {
        let conn = setup_test_db();
        insert_issue(
            &conn,
            "issue-1",
            "Kubernetes crash",
            "Kubernetes pod killed",
        );
        let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
        conn.execute(
            "INSERT INTO ai_conversations (id, issue_id, provider, model, created_at, title) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
            rusqlite::params!["conv-1", "issue-1", "openai", "gpt-4o", &now, "Conv"],
        ).unwrap();
        conn.execute(
            "INSERT INTO ai_messages (id, conversation_id, role, content, token_count, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
            rusqlite::params!["msg-1", "conv-1", "assistant", "Kubernetes OOM detected", 5, &now],
        ).unwrap();

        let results = run_search_query(&conn, "Kubernetes");
        assert_eq!(results.len(), 1);
        assert_eq!(results[0], "issue-1");
    }
}

// ─── Kubernetes Cluster CRUD ────────────────────────────────────────────────

use rusqlite::ffi;

#[tauri::command]
pub async fn load_clusters(state: State<'_, AppState>) -> Result<Vec<Cluster>, String> {
    let db = state.db.lock().map_err(|e| e.to_string())?;

    let mut stmt = db
        .prepare(
            "SELECT id, name, context, server_url, kubeconfig_content, created_at, updated_at \
             FROM clusters ORDER BY name ASC",
        )
        .map_err(|e| e.to_string())?;

    let clusters: Vec<Cluster> = stmt
        .query_map([], |row| {
            Ok(Cluster {
                id: row.get(0)?,
                name: row.get(1)?,
                context: row.get(2)?,
                server_url: row.get(3)?,
                kubeconfig_content: row.get(4)?,
                created_at: row.get(5)?,
                updated_at: row.get(6)?,
            })
        })
        .map_err(|e| e.to_string())?
        .filter_map(|r| r.ok())
        .collect();

    Ok(clusters)
}

// ─── Port Forward CRUD ──────────────────────────────────────────────────────

#[tauri::command]
pub async fn load_port_forwards(state: State<'_, AppState>) -> Result<Vec<PortForward>, String> {
    let db = state.db.lock().map_err(|e| e.to_string())?;

    let mut stmt = db
        .prepare(
            "SELECT id, cluster_id, namespace, pod, container, ports, local_ports, status, error_message, created_at, updated_at \
             FROM port_forwards ORDER BY created_at ASC",
        )
        .map_err(|e| e.to_string())?;

    let port_forwards: Vec<PortForward> = stmt
        .query_map([], |row| {
            let ports_str: String = row.get(5)?;
            let local_ports_str: String = row.get(6)?;
            let ports: Vec<u16> = match serde_json::from_str(&ports_str) {
                Ok(v) => v,
                Err(e) => {
                    return Err(rusqlite::Error::SqliteFailure(
                        ffi::Error::new(ffi::SQLITE_ERROR),
                        Some(format!("Failed to parse ports: {e}")),
                    ))
                }
            };
            let local_ports: Vec<u16> = match serde_json::from_str(&local_ports_str) {
                Ok(v) => v,
                Err(e) => {
                    return Err(rusqlite::Error::SqliteFailure(
                        ffi::Error::new(ffi::SQLITE_ERROR),
                        Some(format!("Failed to parse local_ports: {e}")),
                    ))
                }
            };
            Ok(PortForward {
                id: row.get(0)?,
                cluster_id: row.get(1)?,
                namespace: row.get(2)?,
                pod: row.get(3)?,
                container: row.get(4)?,
                ports,
                local_ports,
                status: row.get(7)?,
                error_message: row.get(8)?,
                created_at: row.get(9)?,
                updated_at: row.get(10)?,
            })
        })
        .map_err(|e| e.to_string())?
        .filter_map(|r| r.ok())
        .collect();

    Ok(port_forwards)
}
