use rusqlite::{params, Connection, Result};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProcessedTicket {
    pub id: String,
    pub tracker_id: String,
    pub artifact_id: i32,
    pub artifact_title: String,
    pub artifact_data: String,
    pub status: String,
    pub analyst_report: Option<String>,
    pub developer_report: Option<String>,
    pub worktree_path: Option<String>,
    pub branch_name: Option<String>,
    pub detected_at: String,
    pub processed_at: Option<String>,
}

fn from_row(row: &rusqlite::Row) -> rusqlite::Result<ProcessedTicket> {
    Ok(ProcessedTicket {
        id: row.get(0)?,
        tracker_id: row.get(1)?,
        artifact_id: row.get(2)?,
        artifact_title: row.get(3)?,
        artifact_data: row.get(4)?,
        status: row.get(5)?,
        analyst_report: row.get(6)?,
        developer_report: row.get(7)?,
        worktree_path: row.get(8)?,
        branch_name: row.get(9)?,
        detected_at: row.get(10)?,
        processed_at: row.get(11)?,
    })
}

const SELECT_ALL_COLS: &str = "SELECT id, tracker_id, artifact_id, artifact_title, artifact_data, \
    status, analyst_report, developer_report, worktree_path, branch_name, \
    detected_at, processed_at FROM processed_tickets";

impl ProcessedTicket {
    /// Insert a new ticket if one with the same (tracker_id, artifact_id) doesn't exist.
    /// Returns Some(ticket) if inserted, None if it was a duplicate.
    pub fn insert_if_new(
        conn: &Connection,
        tracker_id: &str,
        artifact_id: i32,
        artifact_title: &str,
        artifact_data: &str,
    ) -> Result<Option<ProcessedTicket>> {
        if Self::exists(conn, tracker_id, artifact_id)? {
            return Ok(None);
        }

        let id = Uuid::new_v4().to_string();
        let now = chrono::Utc::now().to_rfc3339();

        conn.execute(
            "INSERT INTO processed_tickets \
             (id, tracker_id, artifact_id, artifact_title, artifact_data, status, detected_at) \
             VALUES (?1, ?2, ?3, ?4, ?5, 'Pending', ?6)",
            params![id, tracker_id, artifact_id, artifact_title, artifact_data, now],
        )?;

        let ticket = ProcessedTicket {
            id,
            tracker_id: tracker_id.to_string(),
            artifact_id,
            artifact_title: artifact_title.to_string(),
            artifact_data: artifact_data.to_string(),
            status: "Pending".to_string(),
            analyst_report: None,
            developer_report: None,
            worktree_path: None,
            branch_name: None,
            detected_at: now,
            processed_at: None,
        };

        Ok(Some(ticket))
    }

    /// Returns true if a ticket with (tracker_id, artifact_id) already exists.
    pub fn exists(conn: &Connection, tracker_id: &str, artifact_id: i32) -> Result<bool> {
        let count: i64 = conn.query_row(
            "SELECT COUNT(*) FROM processed_tickets WHERE tracker_id = ?1 AND artifact_id = ?2",
            params![tracker_id, artifact_id],
            |row| row.get(0),
        )?;
        Ok(count > 0)
    }

    #[allow(dead_code)]
    pub fn list_by_tracker(conn: &Connection, tracker_id: &str) -> Result<Vec<ProcessedTicket>> {
        let sql = format!(
            "{} WHERE tracker_id = ?1 ORDER BY detected_at DESC",
            SELECT_ALL_COLS
        );
        let mut stmt = conn.prepare(&sql)?;
        let rows = stmt.query_map(params![tracker_id], from_row)?;
        rows.collect()
    }

    pub fn list_by_project(conn: &Connection, project_id: &str) -> Result<Vec<ProcessedTicket>> {
        let mut stmt = conn.prepare(
            "SELECT pt.id, pt.tracker_id, pt.artifact_id, pt.artifact_title, pt.artifact_data, \
             pt.status, pt.analyst_report, pt.developer_report, pt.worktree_path, pt.branch_name, \
             pt.detected_at, pt.processed_at \
             FROM processed_tickets pt \
             JOIN watched_trackers wt ON pt.tracker_id = wt.id \
             WHERE wt.project_id = ?1 \
             ORDER BY pt.detected_at DESC",
        )?;
        let rows = stmt.query_map(params![project_id], from_row)?;
        rows.collect()
    }

    pub fn get_by_id(conn: &Connection, id: &str) -> Result<ProcessedTicket> {
        let sql = format!("{} WHERE id = ?1", SELECT_ALL_COLS);
        conn.query_row(&sql, params![id], from_row)
    }

    pub fn update_status(conn: &Connection, id: &str, status: &str) -> Result<()> {
        conn.execute(
            "UPDATE processed_tickets SET status = ?1 WHERE id = ?2",
            params![status, id],
        )?;
        Ok(())
    }

    pub fn set_analyst_report(conn: &Connection, id: &str, report: &str) -> Result<()> {
        conn.execute(
            "UPDATE processed_tickets SET analyst_report = ?1 WHERE id = ?2",
            params![report, id],
        )?;
        Ok(())
    }

    pub fn set_developer_report(conn: &Connection, id: &str, report: &str) -> Result<()> {
        conn.execute(
            "UPDATE processed_tickets SET developer_report = ?1, processed_at = datetime('now') WHERE id = ?2",
            params![report, id],
        )?;
        Ok(())
    }

    pub fn set_worktree_info(
        conn: &Connection,
        id: &str,
        worktree_path: &str,
        branch_name: &str,
    ) -> Result<()> {
        conn.execute(
            "UPDATE processed_tickets SET worktree_path = ?1, branch_name = ?2 WHERE id = ?3",
            params![worktree_path, branch_name, id],
        )?;
        Ok(())
    }

    pub fn list_pending(conn: &Connection) -> Result<Vec<ProcessedTicket>> {
        let sql = format!(
            "{} WHERE status = 'Pending' ORDER BY detected_at ASC",
            SELECT_ALL_COLS
        );
        let mut stmt = conn.prepare(&sql)?;
        let rows = stmt.query_map([], from_row)?;
        rows.collect()
    }

    pub fn set_error(conn: &Connection, id: &str, error_message: &str) -> Result<()> {
        conn.execute(
            "UPDATE processed_tickets SET status = 'Error', analyst_report = COALESCE(analyst_report, '') || ?1, processed_at = datetime('now') WHERE id = ?2",
            params![error_message, id],
        )?;
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::db;
    use crate::models::agent::{Agent, AgentRole, AgentTool};
    use crate::models::project::Project;
    use crate::models::tracker::WatchedTracker;

    fn setup() -> (Connection, String) {
        let conn = db::init_in_memory().expect("db init should succeed");
        let project = Project::insert(&conn, "Test", "/path", None, "main").unwrap();
        let analyst = Agent::insert(&conn, "A", AgentRole::Analyst, AgentTool::Codex, "").unwrap();
        let developer =
            Agent::insert(&conn, "D", AgentRole::Developer, AgentTool::ClaudeCode, "").unwrap();
        let tracker = WatchedTracker::insert(
            &conn,
            &project.id,
            456,
            "Bugs",
            10,
            &analyst.id,
            &developer.id,
            vec![],
        )
        .unwrap();
        (conn, tracker.id)
    }

    #[test]
    fn test_insert_if_new_creates_ticket() {
        let (conn, tracker_id) = setup();

        let result = ProcessedTicket::insert_if_new(
            &conn,
            &tracker_id,
            101,
            "Fix login bug",
            "{\"id\": 101}",
        )
        .expect("insert_if_new should succeed");

        assert!(result.is_some());
        let ticket = result.unwrap();
        assert_eq!(ticket.status, "Pending");
        assert_eq!(ticket.tracker_id, tracker_id);
        assert_eq!(ticket.artifact_id, 101);
        assert_eq!(ticket.artifact_title, "Fix login bug");
        assert!(ticket.analyst_report.is_none());
        assert!(ticket.processed_at.is_none());
    }

    #[test]
    fn test_insert_if_new_returns_none_for_duplicate() {
        let (conn, tracker_id) = setup();

        let first = ProcessedTicket::insert_if_new(
            &conn,
            &tracker_id,
            202,
            "Crash on startup",
            "{\"id\": 202}",
        )
        .expect("first insert should succeed");
        assert!(first.is_some());

        let second = ProcessedTicket::insert_if_new(
            &conn,
            &tracker_id,
            202,
            "Crash on startup",
            "{\"id\": 202}",
        )
        .expect("second insert_if_new should succeed");
        assert!(second.is_none());
    }

    #[test]
    fn test_exists() {
        let (conn, tracker_id) = setup();

        let before = ProcessedTicket::exists(&conn, &tracker_id, 303)
            .expect("exists check should succeed");
        assert!(!before);

        ProcessedTicket::insert_if_new(&conn, &tracker_id, 303, "Some ticket", "{}")
            .expect("insert should succeed");

        let after = ProcessedTicket::exists(&conn, &tracker_id, 303)
            .expect("exists check after insert should succeed");
        assert!(after);
    }

    #[test]
    fn test_list_by_tracker() {
        let (conn, tracker_id) = setup();

        ProcessedTicket::insert_if_new(&conn, &tracker_id, 1, "Ticket One", "{}").unwrap();
        ProcessedTicket::insert_if_new(&conn, &tracker_id, 2, "Ticket Two", "{}").unwrap();

        let tickets =
            ProcessedTicket::list_by_tracker(&conn, &tracker_id).expect("list should succeed");
        assert_eq!(tickets.len(), 2);
    }

    #[test]
    fn test_get_by_id() {
        let (conn, tracker_id) = setup();

        let inserted =
            ProcessedTicket::insert_if_new(&conn, &tracker_id, 404, "Not Found Bug", "{\"id\": 404}")
                .expect("insert should succeed")
                .expect("should be Some");

        let found =
            ProcessedTicket::get_by_id(&conn, &inserted.id).expect("get_by_id should succeed");

        assert_eq!(found.id, inserted.id);
        assert_eq!(found.artifact_id, 404);
        assert_eq!(found.artifact_title, "Not Found Bug");
        assert_eq!(found.status, "Pending");
    }

    #[test]
    fn test_update_status() {
        let (conn, tracker_id) = setup();
        let ticket = ProcessedTicket::insert_if_new(&conn, &tracker_id, 1, "T1", "{}")
            .unwrap()
            .unwrap();

        ProcessedTicket::update_status(&conn, &ticket.id, "Analyzing").unwrap();
        let updated = ProcessedTicket::get_by_id(&conn, &ticket.id).unwrap();
        assert_eq!(updated.status, "Analyzing");
    }

    #[test]
    fn test_set_analyst_report() {
        let (conn, tracker_id) = setup();
        let ticket = ProcessedTicket::insert_if_new(&conn, &tracker_id, 1, "T1", "{}")
            .unwrap()
            .unwrap();

        ProcessedTicket::set_analyst_report(&conn, &ticket.id, "## Report\nAll good.").unwrap();
        let updated = ProcessedTicket::get_by_id(&conn, &ticket.id).unwrap();
        assert_eq!(updated.analyst_report.unwrap(), "## Report\nAll good.");
    }

    #[test]
    fn test_set_developer_report() {
        let (conn, tracker_id) = setup();
        let ticket = ProcessedTicket::insert_if_new(&conn, &tracker_id, 1, "T1", "{}")
            .unwrap()
            .unwrap();

        ProcessedTicket::set_developer_report(&conn, &ticket.id, "Fixed in main.rs").unwrap();
        let updated = ProcessedTicket::get_by_id(&conn, &ticket.id).unwrap();
        assert_eq!(updated.developer_report.unwrap(), "Fixed in main.rs");
        assert!(updated.processed_at.is_some());
    }

    #[test]
    fn test_set_worktree_info() {
        let (conn, tracker_id) = setup();
        let ticket = ProcessedTicket::insert_if_new(&conn, &tracker_id, 1, "T1", "{}")
            .unwrap()
            .unwrap();

        ProcessedTicket::set_worktree_info(&conn, &ticket.id, "/tmp/wt", "orchai/1").unwrap();
        let updated = ProcessedTicket::get_by_id(&conn, &ticket.id).unwrap();
        assert_eq!(updated.worktree_path.unwrap(), "/tmp/wt");
        assert_eq!(updated.branch_name.unwrap(), "orchai/1");
    }

    #[test]
    fn test_list_pending() {
        let (conn, tracker_id) = setup();
        ProcessedTicket::insert_if_new(&conn, &tracker_id, 1, "T1", "{}").unwrap();
        ProcessedTicket::insert_if_new(&conn, &tracker_id, 2, "T2", "{}").unwrap();

        let pending = ProcessedTicket::list_pending(&conn).unwrap();
        assert_eq!(pending.len(), 2);
        assert_eq!(pending[0].artifact_id, 1);
        assert_eq!(pending[1].artifact_id, 2);

        ProcessedTicket::update_status(&conn, &pending[0].id, "Analyzing").unwrap();
        let pending2 = ProcessedTicket::list_pending(&conn).unwrap();
        assert_eq!(pending2.len(), 1);
        assert_eq!(pending2[0].artifact_id, 2);
    }

    #[test]
    fn test_set_error() {
        let (conn, tracker_id) = setup();
        let ticket = ProcessedTicket::insert_if_new(&conn, &tracker_id, 1, "T1", "{}")
            .unwrap()
            .unwrap();

        ProcessedTicket::set_error(&conn, &ticket.id, "CLI timeout after 600s").unwrap();
        let updated = ProcessedTicket::get_by_id(&conn, &ticket.id).unwrap();
        assert_eq!(updated.status, "Error");
        assert_eq!(updated.analyst_report.unwrap(), "CLI timeout after 600s");
    }
}
