#![allow(dead_code)]

use anyhow::{Context, Result};
use rusqlite::Connection;

use crate::state::mr_list::{MrCursor, MrFilter, MrListPage, MrListRow, MrSortField, MrSortOrder};

/// Page size for MR list queries.
const MR_PAGE_SIZE: usize = 50;

/// Fetch a page of merge requests matching the given filter and sort.
///
/// Uses keyset pagination and snapshot fence — same pattern as issues.
pub fn fetch_mr_list(
    conn: &Connection,
    filter: &MrFilter,
    sort_field: MrSortField,
    sort_order: MrSortOrder,
    cursor: Option<&MrCursor>,
    snapshot_fence: Option<i64>,
) -> Result<MrListPage> {
    // -- Build dynamic WHERE conditions and params --------------------------
    let mut conditions: Vec<String> = Vec::new();
    let mut params: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();

    if let Some(pid) = filter.project_id {
        conditions.push("m.project_id = ?".into());
        params.push(Box::new(pid));
    }

    if let Some(ref state) = filter.state {
        conditions.push("m.state = ?".into());
        params.push(Box::new(state.clone()));
    }

    if let Some(ref author) = filter.author {
        conditions.push("m.author_username = ?".into());
        params.push(Box::new(author.clone()));
    }

    if let Some(draft) = filter.draft {
        conditions.push("m.draft = ?".into());
        params.push(Box::new(i64::from(draft)));
    }

    if let Some(ref target) = filter.target_branch {
        conditions.push("m.target_branch = ?".into());
        params.push(Box::new(target.clone()));
    }

    if let Some(ref source) = filter.source_branch {
        conditions.push("m.source_branch = ?".into());
        params.push(Box::new(source.clone()));
    }

    // Filter: reviewer (via join on mr_reviewers)
    let reviewer_join = if let Some(ref reviewer) = filter.reviewer {
        conditions.push("rv.username = ?".into());
        params.push(Box::new(reviewer.clone()));
        "JOIN mr_reviewers rv ON rv.merge_request_id = m.id"
    } else {
        ""
    };

    // Filter: label (via join on mr_labels + labels)
    let label_join = if let Some(ref label) = filter.label {
        conditions.push("fl.name = ?".into());
        params.push(Box::new(label.clone()));
        "JOIN mr_labels fil ON fil.merge_request_id = m.id \
         JOIN labels fl ON fl.id = fil.label_id"
    } else {
        ""
    };

    // Filter: free_text (LIKE on title)
    if let Some(ref text) = filter.free_text {
        conditions.push("m.title LIKE ?".into());
        params.push(Box::new(format!("%{text}%")));
    }

    // Snapshot fence
    if let Some(fence) = snapshot_fence {
        conditions.push("m.updated_at <= ?".into());
        params.push(Box::new(fence));
    }

    // -- Count query (before cursor filter) ---------------------------------
    let where_clause = if conditions.is_empty() {
        String::new()
    } else {
        format!("WHERE {}", conditions.join(" AND "))
    };

    let count_sql = format!(
        "SELECT COUNT(DISTINCT m.id) FROM merge_requests m \
         JOIN projects p ON p.id = m.project_id \
         {reviewer_join} {label_join} {where_clause}"
    );
    let count_params: Vec<&dyn rusqlite::types::ToSql> =
        params.iter().map(|b| b.as_ref()).collect();

    let total_count: i64 = conn
        .query_row(&count_sql, count_params.as_slice(), |r| r.get(0))
        .context("counting MRs for list")?;

    // -- Keyset cursor condition -------------------------------------------
    let (sort_col, sort_dir) = mr_sort_column_and_dir(sort_field, sort_order);
    let cursor_op = if sort_dir == "DESC" { "<" } else { ">" };

    if let Some(c) = cursor {
        conditions.push(format!("({sort_col}, m.iid) {cursor_op} (?, ?)"));
        params.push(Box::new(c.updated_at));
        params.push(Box::new(c.iid));
    }

    // -- Data query ---------------------------------------------------------
    let where_clause_full = if conditions.is_empty() {
        String::new()
    } else {
        format!("WHERE {}", conditions.join(" AND "))
    };

    let data_sql = format!(
        "SELECT p.path_with_namespace, m.iid, m.title, m.state, \
                m.author_username, m.target_branch, m.updated_at, m.draft, \
                GROUP_CONCAT(DISTINCT l.name) AS label_names \
         FROM merge_requests m \
         JOIN projects p ON p.id = m.project_id \
         {reviewer_join} \
         {label_join} \
         LEFT JOIN mr_labels ml ON ml.merge_request_id = m.id \
         LEFT JOIN labels l ON l.id = ml.label_id \
         {where_clause_full} \
         GROUP BY m.id \
         ORDER BY {sort_col} {sort_dir}, m.iid {sort_dir} \
         LIMIT ?"
    );

    let fetch_limit = (MR_PAGE_SIZE + 1) as i64;
    params.push(Box::new(fetch_limit));

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

    let mut stmt = conn.prepare(&data_sql).context("preparing MR list query")?;

    let rows_result = stmt
        .query_map(all_params.as_slice(), |row| {
            let project_path: String = row.get(0)?;
            let iid: i64 = row.get(1)?;
            let title: String = row.get::<_, Option<String>>(2)?.unwrap_or_default();
            let state: String = row.get::<_, Option<String>>(3)?.unwrap_or_default();
            let author: String = row.get::<_, Option<String>>(4)?.unwrap_or_default();
            let target_branch: String = row.get::<_, Option<String>>(5)?.unwrap_or_default();
            let updated_at: i64 = row.get(6)?;
            let draft_int: i64 = row.get(7)?;
            let label_names: Option<String> = row.get(8)?;

            let labels = label_names
                .map(|s| s.split(',').map(String::from).collect())
                .unwrap_or_default();

            Ok(MrListRow {
                project_path,
                iid,
                title,
                state,
                author,
                target_branch,
                labels,
                updated_at,
                draft: draft_int != 0,
            })
        })
        .context("querying MR list")?;

    let mut rows: Vec<MrListRow> = Vec::new();
    for row in rows_result {
        rows.push(row.context("reading MR list row")?);
    }

    let has_next = rows.len() > MR_PAGE_SIZE;
    if has_next {
        rows.truncate(MR_PAGE_SIZE);
    }

    let next_cursor = if has_next {
        rows.last().map(|r| MrCursor {
            updated_at: r.updated_at,
            iid: r.iid,
        })
    } else {
        None
    };

    #[allow(clippy::cast_sign_loss)]
    Ok(MrListPage {
        rows,
        next_cursor,
        total_count: total_count as u64,
    })
}

/// Map MR sort field + order to SQL column name and direction keyword.
fn mr_sort_column_and_dir(field: MrSortField, order: MrSortOrder) -> (&'static str, &'static str) {
    let col = match field {
        MrSortField::UpdatedAt => "m.updated_at",
        MrSortField::Iid => "m.iid",
        MrSortField::Title => "m.title",
        MrSortField::State => "m.state",
        MrSortField::Author => "m.author_username",
        MrSortField::TargetBranch => "m.target_branch",
    };
    let dir = match order {
        MrSortOrder::Desc => "DESC",
        MrSortOrder::Asc => "ASC",
    };
    (col, dir)
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Create the schema needed for MR list tests.
    fn create_mr_list_schema(conn: &Connection) {
        conn.execute_batch(
            "
            CREATE TABLE projects (
                id INTEGER PRIMARY KEY,
                gitlab_project_id INTEGER UNIQUE NOT NULL,
                path_with_namespace TEXT NOT NULL
            );
            CREATE TABLE merge_requests (
                id INTEGER PRIMARY KEY,
                gitlab_id INTEGER UNIQUE NOT NULL,
                project_id INTEGER NOT NULL,
                iid INTEGER NOT NULL,
                title TEXT,
                state TEXT,
                author_username TEXT,
                created_at INTEGER,
                updated_at INTEGER,
                last_seen_at INTEGER NOT NULL,
                draft INTEGER NOT NULL DEFAULT 0,
                target_branch TEXT,
                source_branch TEXT
            );
            CREATE TABLE labels (
                id INTEGER PRIMARY KEY,
                gitlab_id INTEGER,
                project_id INTEGER NOT NULL,
                name TEXT NOT NULL,
                color TEXT,
                description TEXT
            );
            CREATE TABLE mr_labels (
                merge_request_id INTEGER NOT NULL,
                label_id INTEGER NOT NULL,
                PRIMARY KEY(merge_request_id, label_id)
            );
            CREATE TABLE mr_reviewers (
                merge_request_id INTEGER NOT NULL,
                username TEXT NOT NULL,
                PRIMARY KEY(merge_request_id, username)
            );
        ",
        )
        .expect("create MR list schema");
    }

    /// Insert a test MR with full fields.
    fn insert_mr_full(
        conn: &Connection,
        iid: i64,
        state: &str,
        author: &str,
        target_branch: &str,
        draft: bool,
        updated_at: i64,
    ) {
        conn.execute(
            "INSERT INTO merge_requests \
             (gitlab_id, project_id, iid, title, state, author_username, \
              target_branch, draft, created_at, updated_at, last_seen_at) \
             VALUES (?1, 1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?8, ?8)",
            rusqlite::params![
                iid * 100 + 50,
                iid,
                format!("MR {iid}"),
                state,
                author,
                target_branch,
                i64::from(draft),
                updated_at,
            ],
        )
        .expect("insert mr full");
    }

    /// Attach a label to an MR.
    fn attach_mr_label(conn: &Connection, mr_iid: i64, label_name: &str) {
        let mr_id: i64 = conn
            .query_row(
                "SELECT id FROM merge_requests WHERE iid = ?",
                [mr_iid],
                |r| r.get(0),
            )
            .expect("find mr");

        conn.execute(
            "INSERT OR IGNORE INTO labels (project_id, name) VALUES (1, ?)",
            [label_name],
        )
        .expect("insert label");
        let label_id: i64 = conn
            .query_row("SELECT id FROM labels WHERE name = ?", [label_name], |r| {
                r.get(0)
            })
            .expect("find label");

        conn.execute(
            "INSERT INTO mr_labels (merge_request_id, label_id) VALUES (?, ?)",
            [mr_id, label_id],
        )
        .expect("attach mr label");
    }

    /// Add a reviewer to an MR.
    fn add_mr_reviewer(conn: &Connection, mr_iid: i64, username: &str) {
        let mr_id: i64 = conn
            .query_row(
                "SELECT id FROM merge_requests WHERE iid = ?",
                [mr_iid],
                |r| r.get(0),
            )
            .expect("find mr");

        conn.execute(
            "INSERT INTO mr_reviewers (merge_request_id, username) VALUES (?, ?)",
            rusqlite::params![mr_id, username],
        )
        .expect("add mr reviewer");
    }

    fn setup_mr_list_data(conn: &Connection) {
        let base = 1_700_000_000_000_i64;
        conn.execute(
            "INSERT INTO projects (gitlab_project_id, path_with_namespace) VALUES (1, 'group/project')",
            [],
        )
        .unwrap();

        insert_mr_full(conn, 1, "opened", "alice", "main", false, base - 10_000);
        insert_mr_full(conn, 2, "opened", "bob", "main", true, base - 20_000);
        insert_mr_full(conn, 3, "merged", "alice", "develop", false, base - 30_000);
        insert_mr_full(conn, 4, "opened", "charlie", "main", true, base - 40_000);
        insert_mr_full(conn, 5, "closed", "bob", "release", false, base - 50_000);

        attach_mr_label(conn, 1, "backend");
        attach_mr_label(conn, 1, "urgent");
        attach_mr_label(conn, 2, "frontend");
        attach_mr_label(conn, 4, "backend");

        add_mr_reviewer(conn, 1, "diana");
        add_mr_reviewer(conn, 2, "diana");
        add_mr_reviewer(conn, 3, "edward");
    }

    #[test]
    fn test_fetch_mr_list_basic() {
        let conn = Connection::open_in_memory().unwrap();
        create_mr_list_schema(&conn);
        setup_mr_list_data(&conn);

        let filter = MrFilter::default();
        let page = fetch_mr_list(
            &conn,
            &filter,
            MrSortField::UpdatedAt,
            MrSortOrder::Desc,
            None,
            None,
        )
        .unwrap();

        assert_eq!(page.total_count, 5);
        assert_eq!(page.rows.len(), 5);
        assert_eq!(page.rows[0].iid, 1); // newest first
        assert_eq!(page.rows[4].iid, 5);
        assert!(page.next_cursor.is_none());
    }

    #[test]
    fn test_fetch_mr_list_filter_state() {
        let conn = Connection::open_in_memory().unwrap();
        create_mr_list_schema(&conn);
        setup_mr_list_data(&conn);

        let filter = MrFilter {
            state: Some("opened".into()),
            ..Default::default()
        };
        let page = fetch_mr_list(
            &conn,
            &filter,
            MrSortField::UpdatedAt,
            MrSortOrder::Desc,
            None,
            None,
        )
        .unwrap();

        assert_eq!(page.total_count, 3);
        assert!(page.rows.iter().all(|r| r.state == "opened"));
    }

    #[test]
    fn test_fetch_mr_list_filter_draft() {
        let conn = Connection::open_in_memory().unwrap();
        create_mr_list_schema(&conn);
        setup_mr_list_data(&conn);

        let filter = MrFilter {
            draft: Some(true),
            ..Default::default()
        };
        let page = fetch_mr_list(
            &conn,
            &filter,
            MrSortField::UpdatedAt,
            MrSortOrder::Desc,
            None,
            None,
        )
        .unwrap();

        assert_eq!(page.total_count, 2); // MRs 2 and 4
        assert!(page.rows.iter().all(|r| r.draft));
    }

    #[test]
    fn test_fetch_mr_list_filter_target_branch() {
        let conn = Connection::open_in_memory().unwrap();
        create_mr_list_schema(&conn);
        setup_mr_list_data(&conn);

        let filter = MrFilter {
            target_branch: Some("main".into()),
            ..Default::default()
        };
        let page = fetch_mr_list(
            &conn,
            &filter,
            MrSortField::UpdatedAt,
            MrSortOrder::Desc,
            None,
            None,
        )
        .unwrap();

        assert_eq!(page.total_count, 3); // MRs 1, 2, 4
        assert!(page.rows.iter().all(|r| r.target_branch == "main"));
    }

    #[test]
    fn test_fetch_mr_list_filter_reviewer() {
        let conn = Connection::open_in_memory().unwrap();
        create_mr_list_schema(&conn);
        setup_mr_list_data(&conn);

        let filter = MrFilter {
            reviewer: Some("diana".into()),
            ..Default::default()
        };
        let page = fetch_mr_list(
            &conn,
            &filter,
            MrSortField::UpdatedAt,
            MrSortOrder::Desc,
            None,
            None,
        )
        .unwrap();

        assert_eq!(page.total_count, 2); // MRs 1 and 2
    }

    #[test]
    fn test_fetch_mr_list_filter_label() {
        let conn = Connection::open_in_memory().unwrap();
        create_mr_list_schema(&conn);
        setup_mr_list_data(&conn);

        let filter = MrFilter {
            label: Some("backend".into()),
            ..Default::default()
        };
        let page = fetch_mr_list(
            &conn,
            &filter,
            MrSortField::UpdatedAt,
            MrSortOrder::Desc,
            None,
            None,
        )
        .unwrap();

        assert_eq!(page.total_count, 2); // MRs 1 and 4
    }

    #[test]
    fn test_fetch_mr_list_labels_aggregated() {
        let conn = Connection::open_in_memory().unwrap();
        create_mr_list_schema(&conn);
        setup_mr_list_data(&conn);

        let filter = MrFilter::default();
        let page = fetch_mr_list(
            &conn,
            &filter,
            MrSortField::UpdatedAt,
            MrSortOrder::Desc,
            None,
            None,
        )
        .unwrap();

        let mr1 = page.rows.iter().find(|r| r.iid == 1).unwrap();
        assert_eq!(mr1.labels.len(), 2);
        assert!(mr1.labels.contains(&"backend".to_string()));
        assert!(mr1.labels.contains(&"urgent".to_string()));

        let mr5 = page.rows.iter().find(|r| r.iid == 5).unwrap();
        assert!(mr5.labels.is_empty());
    }

    #[test]
    fn test_fetch_mr_list_sort_ascending() {
        let conn = Connection::open_in_memory().unwrap();
        create_mr_list_schema(&conn);
        setup_mr_list_data(&conn);

        let filter = MrFilter::default();
        let page = fetch_mr_list(
            &conn,
            &filter,
            MrSortField::UpdatedAt,
            MrSortOrder::Asc,
            None,
            None,
        )
        .unwrap();

        assert_eq!(page.rows[0].iid, 5); // oldest first
        assert_eq!(page.rows[4].iid, 1);
    }

    #[test]
    fn test_fetch_mr_list_snapshot_fence() {
        let conn = Connection::open_in_memory().unwrap();
        create_mr_list_schema(&conn);
        setup_mr_list_data(&conn);

        let base = 1_700_000_000_000_i64;
        let fence = base - 25_000;
        let filter = MrFilter::default();
        let page = fetch_mr_list(
            &conn,
            &filter,
            MrSortField::UpdatedAt,
            MrSortOrder::Desc,
            None,
            Some(fence),
        )
        .unwrap();

        assert_eq!(page.total_count, 3);
        assert!(page.rows.iter().all(|r| r.updated_at <= fence));
    }

    #[test]
    fn test_fetch_mr_list_empty() {
        let conn = Connection::open_in_memory().unwrap();
        create_mr_list_schema(&conn);
        conn.execute(
            "INSERT INTO projects (gitlab_project_id, path_with_namespace) VALUES (1, 'g/p')",
            [],
        )
        .unwrap();

        let page = fetch_mr_list(
            &conn,
            &MrFilter::default(),
            MrSortField::UpdatedAt,
            MrSortOrder::Desc,
            None,
            None,
        )
        .unwrap();

        assert_eq!(page.total_count, 0);
        assert!(page.rows.is_empty());
        assert!(page.next_cursor.is_none());
    }

    #[test]
    fn test_fetch_mr_list_free_text() {
        let conn = Connection::open_in_memory().unwrap();
        create_mr_list_schema(&conn);
        setup_mr_list_data(&conn);

        let filter = MrFilter {
            free_text: Some("MR 3".into()),
            ..Default::default()
        };
        let page = fetch_mr_list(
            &conn,
            &filter,
            MrSortField::UpdatedAt,
            MrSortOrder::Desc,
            None,
            None,
        )
        .unwrap();

        assert_eq!(page.total_count, 1);
        assert_eq!(page.rows[0].iid, 3);
    }
}
