#![allow(dead_code)]

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

use crate::state::issue_list::{
    IssueCursor, IssueFilter, IssueListPage, IssueListRow, SortField, SortOrder,
};

/// Page size for issue list queries.
const ISSUE_PAGE_SIZE: usize = 50;

/// Fetch a page of issues matching the given filter and sort.
///
/// Uses keyset pagination: when `cursor` is `Some`, returns rows after
/// (less-than for DESC, greater-than for ASC) the cursor boundary.
/// When `snapshot_fence` is `Some`, limits results to rows updated_at <= fence
/// to prevent newly synced items from shifting the page window.
pub fn fetch_issue_list(
    conn: &Connection,
    filter: &IssueFilter,
    sort_field: SortField,
    sort_order: SortOrder,
    cursor: Option<&IssueCursor>,
    snapshot_fence: Option<i64>,
) -> Result<IssueListPage> {
    // -- Build dynamic WHERE conditions and params --------------------------
    let mut conditions: Vec<String> = Vec::new();
    let mut params: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();

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

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

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

    // Filter: label (via join)
    let label_join = if let Some(ref label) = filter.label {
        conditions.push("fl.name = ?".into());
        params.push(Box::new(label.clone()));
        "JOIN issue_labels fil ON fil.issue_id = i.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("i.title LIKE ?".into());
        params.push(Box::new(format!("%{text}%")));
    }

    // Snapshot fence
    if let Some(fence) = snapshot_fence {
        conditions.push("i.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 i.id) FROM issues i \
         JOIN projects p ON p.id = i.project_id \
         {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 issues for list")?;

    // -- Keyset cursor condition -------------------------------------------
    let (sort_col, sort_dir) = 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}, i.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, i.iid, i.title, i.state, \
                i.author_username, i.updated_at, \
                GROUP_CONCAT(DISTINCT l.name) AS label_names \
         FROM issues i \
         JOIN projects p ON p.id = i.project_id \
         {label_join} \
         LEFT JOIN issue_labels il ON il.issue_id = i.id \
         LEFT JOIN labels l ON l.id = il.label_id \
         {where_clause_full} \
         GROUP BY i.id \
         ORDER BY {sort_col} {sort_dir}, i.iid {sort_dir} \
         LIMIT ?"
    );

    // +1 to detect if there's a next page
    let fetch_limit = (ISSUE_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 issue 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 updated_at: i64 = row.get(5)?;
            let label_names: Option<String> = row.get(6)?;

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

            Ok(IssueListRow {
                project_path,
                iid,
                title,
                state,
                author,
                labels,
                updated_at,
            })
        })
        .context("querying issue list")?;

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

    // Determine next cursor from the last row (if we got more than page size)
    let has_next = rows.len() > ISSUE_PAGE_SIZE;
    if has_next {
        rows.truncate(ISSUE_PAGE_SIZE);
    }

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

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

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

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

    /// Create the minimal schema needed for issue list queries.
    fn create_issue_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 issues (
                id INTEGER PRIMARY KEY,
                gitlab_id INTEGER UNIQUE NOT NULL,
                project_id INTEGER NOT NULL,
                iid INTEGER NOT NULL,
                title TEXT,
                state TEXT NOT NULL,
                author_username TEXT,
                created_at INTEGER NOT NULL,
                updated_at INTEGER NOT NULL,
                last_seen_at INTEGER NOT NULL
            );
            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 issue_labels (
                issue_id INTEGER NOT NULL,
                label_id INTEGER NOT NULL,
                PRIMARY KEY(issue_id, label_id)
            );
        ",
        )
        .expect("create issue list schema");
    }

    /// Insert a test issue with an author.
    fn insert_issue_full(conn: &Connection, iid: i64, state: &str, author: &str, updated_at: i64) {
        conn.execute(
            "INSERT INTO issues (gitlab_id, project_id, iid, title, state, author_username, created_at, updated_at, last_seen_at)
             VALUES (?1, 1, ?2, ?3, ?4, ?5, ?6, ?6, ?6)",
            rusqlite::params![
                iid * 100,
                iid,
                format!("Issue {iid}"),
                state,
                author,
                updated_at
            ],
        )
        .expect("insert issue full");
    }

    /// Attach a label to an issue.
    fn attach_label(conn: &Connection, issue_iid: i64, label_name: &str) {
        // Find issue id.
        let issue_id: i64 = conn
            .query_row("SELECT id FROM issues WHERE iid = ?", [issue_iid], |r| {
                r.get(0)
            })
            .expect("find issue");

        // Ensure label exists.
        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 issue_labels (issue_id, label_id) VALUES (?, ?)",
            [issue_id, label_id],
        )
        .expect("attach label");
    }

    fn setup_issue_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_issue_full(conn, 1, "opened", "alice", base - 10_000);
        insert_issue_full(conn, 2, "opened", "bob", base - 20_000);
        insert_issue_full(conn, 3, "closed", "alice", base - 30_000);
        insert_issue_full(conn, 4, "opened", "charlie", base - 40_000);
        insert_issue_full(conn, 5, "closed", "bob", base - 50_000);

        attach_label(conn, 1, "bug");
        attach_label(conn, 1, "critical");
        attach_label(conn, 2, "feature");
        attach_label(conn, 4, "bug");
    }

    #[test]
    fn test_fetch_issue_list_basic() {
        let conn = Connection::open_in_memory().unwrap();
        create_issue_list_schema(&conn);
        setup_issue_list_data(&conn);

        let filter = IssueFilter::default();
        let page = fetch_issue_list(
            &conn,
            &filter,
            SortField::UpdatedAt,
            SortOrder::Desc,
            None,
            None,
        )
        .unwrap();

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

    #[test]
    fn test_fetch_issue_list_filter_state() {
        let conn = Connection::open_in_memory().unwrap();
        create_issue_list_schema(&conn);
        setup_issue_list_data(&conn);

        let filter = IssueFilter {
            state: Some("opened".into()),
            ..Default::default()
        };
        let page = fetch_issue_list(
            &conn,
            &filter,
            SortField::UpdatedAt,
            SortOrder::Desc,
            None,
            None,
        )
        .unwrap();

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

    #[test]
    fn test_fetch_issue_list_filter_author() {
        let conn = Connection::open_in_memory().unwrap();
        create_issue_list_schema(&conn);
        setup_issue_list_data(&conn);

        let filter = IssueFilter {
            author: Some("alice".into()),
            ..Default::default()
        };
        let page = fetch_issue_list(
            &conn,
            &filter,
            SortField::UpdatedAt,
            SortOrder::Desc,
            None,
            None,
        )
        .unwrap();

        assert_eq!(page.total_count, 2);
        assert_eq!(page.rows.len(), 2);
        assert!(page.rows.iter().all(|r| r.author == "alice"));
    }

    #[test]
    fn test_fetch_issue_list_filter_label() {
        let conn = Connection::open_in_memory().unwrap();
        create_issue_list_schema(&conn);
        setup_issue_list_data(&conn);

        let filter = IssueFilter {
            label: Some("bug".into()),
            ..Default::default()
        };
        let page = fetch_issue_list(
            &conn,
            &filter,
            SortField::UpdatedAt,
            SortOrder::Desc,
            None,
            None,
        )
        .unwrap();

        assert_eq!(page.total_count, 2); // issues 1 and 4
        assert_eq!(page.rows.len(), 2);
    }

    #[test]
    fn test_fetch_issue_list_labels_aggregated() {
        let conn = Connection::open_in_memory().unwrap();
        create_issue_list_schema(&conn);
        setup_issue_list_data(&conn);

        let filter = IssueFilter::default();
        let page = fetch_issue_list(
            &conn,
            &filter,
            SortField::UpdatedAt,
            SortOrder::Desc,
            None,
            None,
        )
        .unwrap();

        // Issue 1 has labels "bug" and "critical".
        let issue1 = page.rows.iter().find(|r| r.iid == 1).unwrap();
        assert_eq!(issue1.labels.len(), 2);
        assert!(issue1.labels.contains(&"bug".to_string()));
        assert!(issue1.labels.contains(&"critical".to_string()));

        // Issue 5 has no labels.
        let issue5 = page.rows.iter().find(|r| r.iid == 5).unwrap();
        assert!(issue5.labels.is_empty());
    }

    #[test]
    fn test_fetch_issue_list_sort_ascending() {
        let conn = Connection::open_in_memory().unwrap();
        create_issue_list_schema(&conn);
        setup_issue_list_data(&conn);

        let filter = IssueFilter::default();
        let page = fetch_issue_list(
            &conn,
            &filter,
            SortField::UpdatedAt,
            SortOrder::Asc,
            None,
            None,
        )
        .unwrap();

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

    #[test]
    fn test_fetch_issue_list_snapshot_fence() {
        let conn = Connection::open_in_memory().unwrap();
        create_issue_list_schema(&conn);
        setup_issue_list_data(&conn);

        let base = 1_700_000_000_000_i64;
        // Fence at base-25000: should exclude issues 1 (at base-10000) and 2 (at base-20000).
        let fence = base - 25_000;
        let filter = IssueFilter::default();
        let page = fetch_issue_list(
            &conn,
            &filter,
            SortField::UpdatedAt,
            SortOrder::Desc,
            None,
            Some(fence),
        )
        .unwrap();

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

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

        let page = fetch_issue_list(
            &conn,
            &IssueFilter::default(),
            SortField::UpdatedAt,
            SortOrder::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_issue_list_free_text() {
        let conn = Connection::open_in_memory().unwrap();
        create_issue_list_schema(&conn);
        setup_issue_list_data(&conn);

        let filter = IssueFilter {
            free_text: Some("Issue 3".into()),
            ..Default::default()
        };
        let page = fetch_issue_list(
            &conn,
            &filter,
            SortField::UpdatedAt,
            SortOrder::Desc,
            None,
            None,
        )
        .unwrap();

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