#![allow(dead_code)]

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

use crate::clock::Clock;
use crate::state::dashboard::{
    DashboardData, EntityCounts, LastSyncInfo, ProjectSyncInfo, RecentActivityItem,
};

/// Fetch all data for the dashboard screen.
///
/// Runs aggregation queries for entity counts, per-project sync freshness,
/// recent activity, and the last sync run summary.
pub fn fetch_dashboard(conn: &Connection, clock: &dyn Clock) -> Result<DashboardData> {
    let counts = fetch_entity_counts(conn)?;
    let projects = fetch_project_sync_info(conn, clock)?;
    let recent = fetch_recent_activity(conn, clock)?;
    let last_sync = fetch_last_sync(conn)?;

    Ok(DashboardData {
        counts,
        projects,
        recent,
        last_sync,
    })
}

/// Count all entities in the database.
fn fetch_entity_counts(conn: &Connection) -> Result<EntityCounts> {
    let issues_total: i64 = conn
        .query_row("SELECT COUNT(*) FROM issues", [], |r| r.get(0))
        .context("counting issues")?;

    let issues_open: i64 = conn
        .query_row(
            "SELECT COUNT(*) FROM issues WHERE state = 'opened'",
            [],
            |r| r.get(0),
        )
        .context("counting open issues")?;

    let mrs_total: i64 = conn
        .query_row("SELECT COUNT(*) FROM merge_requests", [], |r| r.get(0))
        .context("counting merge requests")?;

    let mrs_open: i64 = conn
        .query_row(
            "SELECT COUNT(*) FROM merge_requests WHERE state = 'opened'",
            [],
            |r| r.get(0),
        )
        .context("counting open merge requests")?;

    let discussions: i64 = conn
        .query_row("SELECT COUNT(*) FROM discussions", [], |r| r.get(0))
        .context("counting discussions")?;

    let notes_total: i64 = conn
        .query_row("SELECT COUNT(*) FROM notes", [], |r| r.get(0))
        .context("counting notes")?;

    let notes_system: i64 = conn
        .query_row("SELECT COUNT(*) FROM notes WHERE is_system = 1", [], |r| {
            r.get(0)
        })
        .context("counting system notes")?;

    let notes_system_pct = if notes_total > 0 {
        u8::try_from(notes_system * 100 / notes_total).unwrap_or(100)
    } else {
        0
    };

    let documents: i64 = conn
        .query_row("SELECT COUNT(*) FROM documents", [], |r| r.get(0))
        .context("counting documents")?;

    let embeddings: i64 = conn
        .query_row("SELECT COUNT(*) FROM embedding_metadata", [], |r| r.get(0))
        .context("counting embeddings")?;

    #[allow(clippy::cast_sign_loss)] // SQL COUNT(*) is always >= 0
    Ok(EntityCounts {
        issues_open: issues_open as u64,
        issues_total: issues_total as u64,
        mrs_open: mrs_open as u64,
        mrs_total: mrs_total as u64,
        discussions: discussions as u64,
        notes_total: notes_total as u64,
        notes_system_pct,
        documents: documents as u64,
        embeddings: embeddings as u64,
    })
}

/// Per-project sync freshness based on the most recent sync_runs entry.
fn fetch_project_sync_info(conn: &Connection, clock: &dyn Clock) -> Result<Vec<ProjectSyncInfo>> {
    let now_ms = clock.now_ms();

    let mut stmt = conn
        .prepare(
            "SELECT p.path_with_namespace,
                    MAX(sr.finished_at) as last_sync_ms
             FROM projects p
             LEFT JOIN sync_runs sr ON sr.status = 'succeeded'
                AND sr.finished_at IS NOT NULL
             GROUP BY p.id
             ORDER BY p.path_with_namespace",
        )
        .context("preparing project sync query")?;

    let rows = stmt
        .query_map([], |row| {
            let path: String = row.get(0)?;
            let last_sync_ms: Option<i64> = row.get(1)?;
            Ok((path, last_sync_ms))
        })
        .context("querying project sync info")?;

    let mut result = Vec::new();
    for row in rows {
        let (path, last_sync_ms) = row.context("reading project sync row")?;
        let minutes_since_sync = match last_sync_ms {
            Some(ms) => {
                let elapsed_ms = now_ms.saturating_sub(ms);
                u64::try_from(elapsed_ms / 60_000).unwrap_or(u64::MAX)
            }
            None => u64::MAX, // Never synced.
        };
        result.push(ProjectSyncInfo {
            path,
            minutes_since_sync,
        });
    }

    Ok(result)
}

/// Recent activity: the 20 most recently updated issues and MRs.
fn fetch_recent_activity(conn: &Connection, clock: &dyn Clock) -> Result<Vec<RecentActivityItem>> {
    let now_ms = clock.now_ms();

    let mut stmt = conn
        .prepare(
            "SELECT entity_type, iid, title, state, updated_at FROM (
                SELECT 'issue' AS entity_type, iid, title, state, updated_at
                FROM issues
                UNION ALL
                SELECT 'mr' AS entity_type, iid, title, state, updated_at
                FROM merge_requests
             )
             ORDER BY updated_at DESC
             LIMIT 20",
        )
        .context("preparing recent activity query")?;

    let rows = stmt
        .query_map([], |row| {
            let entity_type: 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 updated_at: i64 = row.get(4)?;
            Ok((entity_type, iid, title, state, updated_at))
        })
        .context("querying recent activity")?;

    let mut result = Vec::new();
    for row in rows {
        let (entity_type, iid, title, state, updated_at) =
            row.context("reading recent activity row")?;
        let elapsed_ms = now_ms.saturating_sub(updated_at);
        let minutes_ago = u64::try_from(elapsed_ms / 60_000).unwrap_or(u64::MAX);
        result.push(RecentActivityItem {
            entity_type,
            iid: iid as u64,
            title,
            state,
            minutes_ago,
        });
    }

    Ok(result)
}

/// The most recent sync run summary.
fn fetch_last_sync(conn: &Connection) -> Result<Option<LastSyncInfo>> {
    let result = conn.query_row(
        "SELECT status, finished_at, command, error
         FROM sync_runs
         ORDER BY id DESC
         LIMIT 1",
        [],
        |row| {
            Ok(LastSyncInfo {
                status: row.get(0)?,
                finished_at: row.get(1)?,
                command: row.get(2)?,
                error: row.get(3)?,
            })
        },
    );

    match result {
        Ok(info) => Ok(Some(info)),
        Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
        Err(e) => Err(e).context("querying last sync run"),
    }
}

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

    /// Create the minimal schema needed for dashboard queries.
    fn create_dashboard_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 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
            );
            CREATE TABLE discussions (
                id INTEGER PRIMARY KEY,
                gitlab_discussion_id TEXT NOT NULL,
                project_id INTEGER NOT NULL,
                noteable_type TEXT NOT NULL,
                last_seen_at INTEGER NOT NULL
            );
            CREATE TABLE notes (
                id INTEGER PRIMARY KEY,
                gitlab_id INTEGER UNIQUE NOT NULL,
                discussion_id INTEGER NOT NULL,
                project_id INTEGER NOT NULL,
                is_system INTEGER NOT NULL DEFAULT 0,
                author_username TEXT,
                body TEXT,
                created_at INTEGER NOT NULL,
                updated_at INTEGER NOT NULL,
                last_seen_at INTEGER NOT NULL
            );
            CREATE TABLE documents (
                id INTEGER PRIMARY KEY,
                source_type TEXT NOT NULL,
                source_id INTEGER NOT NULL,
                project_id INTEGER NOT NULL,
                content_text TEXT NOT NULL,
                content_hash TEXT NOT NULL
            );
            CREATE TABLE embedding_metadata (
                document_id INTEGER NOT NULL,
                chunk_index INTEGER NOT NULL DEFAULT 0,
                model TEXT NOT NULL,
                dims INTEGER NOT NULL,
                document_hash TEXT NOT NULL,
                chunk_hash TEXT NOT NULL,
                created_at INTEGER NOT NULL,
                PRIMARY KEY(document_id, chunk_index)
            );
            CREATE TABLE sync_runs (
                id INTEGER PRIMARY KEY,
                started_at INTEGER NOT NULL,
                heartbeat_at INTEGER NOT NULL,
                finished_at INTEGER,
                status TEXT NOT NULL,
                command TEXT NOT NULL,
                error TEXT
            );
        ",
        )
        .expect("create dashboard schema");
    }

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

    /// Insert a test MR.
    fn insert_mr(conn: &Connection, iid: i64, state: &str, updated_at: i64) {
        conn.execute(
            "INSERT INTO merge_requests (gitlab_id, project_id, iid, title, state, created_at, updated_at, last_seen_at)
             VALUES (?1, 1, ?2, ?3, ?4, ?5, ?5, ?5)",
            rusqlite::params![iid * 100 + 50, iid, format!("MR {iid}"), state, updated_at],
        )
        .expect("insert mr");
    }

    #[test]
    fn test_fetch_dashboard_counts() {
        let conn = Connection::open_in_memory().unwrap();
        create_dashboard_schema(&conn);

        // 5 issues: 3 open, 2 closed.
        let now_ms = 1_700_000_000_000_i64;
        insert_issue(&conn, 1, "opened", now_ms - 10_000);
        insert_issue(&conn, 2, "opened", now_ms - 20_000);
        insert_issue(&conn, 3, "opened", now_ms - 30_000);
        insert_issue(&conn, 4, "closed", now_ms - 40_000);
        insert_issue(&conn, 5, "closed", now_ms - 50_000);

        let clock = FakeClock::from_ms(now_ms);
        let data = fetch_dashboard(&conn, &clock).unwrap();

        assert_eq!(data.counts.issues_open, 3);
        assert_eq!(data.counts.issues_total, 5);
    }

    #[test]
    fn test_fetch_dashboard_mr_counts() {
        let conn = Connection::open_in_memory().unwrap();
        create_dashboard_schema(&conn);

        let now_ms = 1_700_000_000_000_i64;
        insert_mr(&conn, 1, "opened", now_ms);
        insert_mr(&conn, 2, "merged", now_ms);
        insert_mr(&conn, 3, "opened", now_ms);
        insert_mr(&conn, 4, "closed", now_ms);

        let clock = FakeClock::from_ms(now_ms);
        let data = fetch_dashboard(&conn, &clock).unwrap();

        assert_eq!(data.counts.mrs_open, 2);
        assert_eq!(data.counts.mrs_total, 4);
    }

    #[test]
    fn test_fetch_dashboard_empty_database() {
        let conn = Connection::open_in_memory().unwrap();
        create_dashboard_schema(&conn);

        let clock = FakeClock::from_ms(1_700_000_000_000);
        let data = fetch_dashboard(&conn, &clock).unwrap();

        assert_eq!(data.counts.issues_open, 0);
        assert_eq!(data.counts.issues_total, 0);
        assert_eq!(data.counts.mrs_open, 0);
        assert_eq!(data.counts.mrs_total, 0);
        assert_eq!(data.counts.notes_system_pct, 0);
        assert!(data.projects.is_empty());
        assert!(data.recent.is_empty());
        assert!(data.last_sync.is_none());
    }

    #[test]
    fn test_fetch_dashboard_notes_system_pct() {
        let conn = Connection::open_in_memory().unwrap();
        create_dashboard_schema(&conn);

        // 4 notes: 1 system, 3 user -> 25% system.
        for i in 0..4 {
            conn.execute(
                "INSERT INTO notes (gitlab_id, discussion_id, project_id, is_system, created_at, updated_at, last_seen_at)
                 VALUES (?1, 1, 1, ?2, 1000, 1000, 1000)",
                rusqlite::params![i, if i == 0 { 1 } else { 0 }],
            )
            .unwrap();
        }

        let clock = FakeClock::from_ms(1_700_000_000_000);
        let data = fetch_dashboard(&conn, &clock).unwrap();

        assert_eq!(data.counts.notes_total, 4);
        assert_eq!(data.counts.notes_system_pct, 25);
    }

    #[test]
    fn test_fetch_dashboard_project_sync_info() {
        let conn = Connection::open_in_memory().unwrap();
        create_dashboard_schema(&conn);

        conn.execute(
            "INSERT INTO projects (gitlab_project_id, path_with_namespace) VALUES (1, 'group/alpha')",
            [],
        )
        .unwrap();
        conn.execute(
            "INSERT INTO projects (gitlab_project_id, path_with_namespace) VALUES (2, 'group/beta')",
            [],
        )
        .unwrap();

        // Sync ran 30 minutes ago. sync_runs is global (no project_id),
        // so all projects see the same last-sync time.
        let now_ms = 1_700_000_000_000_i64;
        conn.execute(
            "INSERT INTO sync_runs (started_at, heartbeat_at, finished_at, status, command)
             VALUES (?1, ?1, ?2, 'succeeded', 'sync')",
            [now_ms - 30 * 60_000, now_ms - 30 * 60_000],
        )
        .unwrap();

        let clock = FakeClock::from_ms(now_ms);
        let data = fetch_dashboard(&conn, &clock).unwrap();

        assert_eq!(data.projects.len(), 2);
        assert_eq!(data.projects[0].path, "group/alpha");
        assert_eq!(data.projects[0].minutes_since_sync, 30);
        assert_eq!(data.projects[1].path, "group/beta");
        assert_eq!(data.projects[1].minutes_since_sync, 30); // Same: sync_runs is global.
    }

    #[test]
    fn test_fetch_dashboard_recent_activity_ordered() {
        let conn = Connection::open_in_memory().unwrap();
        create_dashboard_schema(&conn);

        let now_ms = 1_700_000_000_000_i64;
        insert_issue(&conn, 1, "opened", now_ms - 60_000); // 1 min ago
        insert_mr(&conn, 1, "merged", now_ms - 120_000); // 2 min ago
        insert_issue(&conn, 2, "closed", now_ms - 180_000); // 3 min ago

        let clock = FakeClock::from_ms(now_ms);
        let data = fetch_dashboard(&conn, &clock).unwrap();

        assert_eq!(data.recent.len(), 3);
        assert_eq!(data.recent[0].entity_type, "issue");
        assert_eq!(data.recent[0].iid, 1);
        assert_eq!(data.recent[0].minutes_ago, 1);
        assert_eq!(data.recent[1].entity_type, "mr");
        assert_eq!(data.recent[1].minutes_ago, 2);
        assert_eq!(data.recent[2].entity_type, "issue");
        assert_eq!(data.recent[2].minutes_ago, 3);
    }

    #[test]
    fn test_fetch_dashboard_last_sync() {
        let conn = Connection::open_in_memory().unwrap();
        create_dashboard_schema(&conn);

        let now_ms = 1_700_000_000_000_i64;
        conn.execute(
            "INSERT INTO sync_runs (started_at, heartbeat_at, finished_at, status, command, error)
             VALUES (?1, ?1, ?2, 'failed', 'sync', 'network timeout')",
            [now_ms - 60_000, now_ms - 50_000],
        )
        .unwrap();
        conn.execute(
            "INSERT INTO sync_runs (started_at, heartbeat_at, finished_at, status, command)
             VALUES (?1, ?1, ?2, 'succeeded', 'sync')",
            [now_ms - 30_000, now_ms - 20_000],
        )
        .unwrap();

        let clock = FakeClock::from_ms(now_ms);
        let data = fetch_dashboard(&conn, &clock).unwrap();

        let sync = data.last_sync.unwrap();
        assert_eq!(sync.status, "succeeded");
        assert_eq!(sync.command, "sync");
        assert!(sync.error.is_none());
    }
}
