pub mod queries;
pub mod render_human;
pub mod render_robot;
pub mod types;

use std::collections::HashSet;

use rusqlite::Connection;

use crate::Config;
use crate::cli::MeArgs;
use crate::core::cursor;
use crate::core::db::create_connection;
use crate::core::error::{LoreError, Result};
use crate::core::paths::get_db_path;
use crate::core::project::resolve_project;
use crate::core::time::parse_since;

use self::queries::{
    query_activity, query_authored_mrs, query_mentioned_in, query_open_issues, query_reviewing_mrs,
    query_since_last_check,
};
use self::types::{AttentionState, MeDashboard, MeSummary, SinceLastCheck};

/// Default activity lookback: 1 day in milliseconds.
const DEFAULT_ACTIVITY_SINCE_DAYS: i64 = 1;
const MS_PER_DAY: i64 = 24 * 60 * 60 * 1000;
/// Recency window for closed/merged items in the "Mentioned In" section: 7 days.
const RECENCY_WINDOW_MS: i64 = 7 * MS_PER_DAY;
/// Only show mentions from notes created within this window (30 days).
const MENTION_WINDOW_MS: i64 = 30 * MS_PER_DAY;

/// Resolve the effective username from CLI flag or config.
///
/// Precedence: `--user` flag > `config.gitlab.username` > error (AC-1.2).
pub fn resolve_username<'a>(args: &'a MeArgs, config: &'a Config) -> Result<&'a str> {
    if let Some(ref user) = args.user {
        return Ok(user.as_str());
    }
    if let Some(ref username) = config.gitlab.username {
        return Ok(username.as_str());
    }
    Err(LoreError::ConfigInvalid {
        details: "No GitLab username configured. Set gitlab.username in config.json or pass --user <username>.".to_string(),
    })
}

/// Resolve the project scope for the dashboard.
///
/// Returns a list of project IDs to filter by. An empty vec means "all projects".
///
/// Precedence (AC-8):
/// - `--project` and `--all` both set → error (AC-8.4, clap also enforces this)
/// - `--all` → empty vec (all projects)
/// - `--project` → resolve to single project ID via fuzzy match
/// - config.default_project → resolve that
/// - no default → empty vec (all projects)
pub fn resolve_project_scope(
    conn: &Connection,
    args: &MeArgs,
    config: &Config,
) -> Result<Vec<i64>> {
    if args.all {
        return Ok(Vec::new());
    }
    if let Some(ref project) = args.project {
        let id = resolve_project(conn, project)?;
        return Ok(vec![id]);
    }
    if let Some(ref dp) = config.default_project {
        let id = resolve_project(conn, dp)?;
        return Ok(vec![id]);
    }
    Ok(Vec::new())
}

/// Run the `lore me` personal dashboard command.
///
/// Orchestrates: username resolution → project scope → query execution →
/// summary computation → dashboard assembly → rendering.
pub fn run_me(config: &Config, args: &MeArgs, robot_mode: bool) -> Result<()> {
    let start = std::time::Instant::now();
    let username = resolve_username(args, config)?;

    // 0. Handle --reset-cursor early return
    if args.reset_cursor {
        cursor::reset_cursor(username)
            .map_err(|e| LoreError::Other(format!("reset cursor: {e}")))?;
        let elapsed_ms = start.elapsed().as_millis() as u64;
        if robot_mode {
            render_robot::print_cursor_reset_json(elapsed_ms)?;
        } else {
            println!("Cursor reset for @{username}. Next `lore me` will establish a new baseline.");
        }
        return Ok(());
    }

    // 1. Open DB
    let db_path = get_db_path(config.storage.db_path.as_deref());
    let conn = create_connection(&db_path)?;

    // 2. Check for synced data (AC-10.2)
    let has_data: bool = conn
        .query_row("SELECT EXISTS(SELECT 1 FROM projects LIMIT 1)", [], |row| {
            row.get(0)
        })
        .unwrap_or(false);
    if !has_data {
        return Err(LoreError::NotFound(
            "No synced data found. Run `lore sync` first to fetch your GitLab data.".to_string(),
        ));
    }

    // 3. Resolve project scope
    let project_ids = resolve_project_scope(&conn, args, config)?;
    let single_project = project_ids.len() == 1;

    // 4. Parse --since (default 1d for activity feed)
    let since_ms = match args.since.as_deref() {
        Some(raw) => parse_since(raw).ok_or_else(|| {
            LoreError::Other(format!(
                "Invalid --since value '{raw}'. Expected: 7d, 2w, 3m, YYYY-MM-DD, or Unix-ms timestamp."
            ))
        })?,
        None => crate::core::time::now_ms() - DEFAULT_ACTIVITY_SINCE_DAYS * MS_PER_DAY,
    };

    // 5. Determine which sections to query
    let show_all = args.show_all_sections();
    let want_issues = show_all || args.issues;
    let want_mrs = show_all || args.mrs;
    let want_activity = show_all || args.activity;
    let want_mentions = show_all || args.mentions;

    // 6. Run queries for requested sections
    let open_issues = if want_issues {
        query_open_issues(&conn, username, &project_ids)?
    } else {
        Vec::new()
    };

    let open_mrs_authored = if want_mrs {
        query_authored_mrs(&conn, username, &project_ids)?
    } else {
        Vec::new()
    };

    let reviewing_mrs = if want_mrs {
        query_reviewing_mrs(&conn, username, &project_ids)?
    } else {
        Vec::new()
    };

    let mentioned_in = if want_mentions {
        let recency_cutoff = crate::core::time::now_ms() - RECENCY_WINDOW_MS;
        let mention_cutoff = crate::core::time::now_ms() - MENTION_WINDOW_MS;
        query_mentioned_in(
            &conn,
            username,
            &project_ids,
            recency_cutoff,
            mention_cutoff,
        )?
    } else {
        Vec::new()
    };

    let activity = if want_activity {
        query_activity(&conn, username, &project_ids, since_ms)?
    } else {
        Vec::new()
    };

    // 6b. Since-last-check (cursor-based inbox)
    let cursor_ms = cursor::read_cursor(username);
    // Capture global watermark BEFORE project filtering so --project doesn't
    // permanently skip events from other projects.
    let mut global_watermark: Option<i64> = None;
    let since_last_check = if let Some(prev_cursor) = cursor_ms {
        let groups = query_since_last_check(&conn, username, prev_cursor)?;
        // Watermark from ALL groups (unfiltered) — this is the true high-water mark
        global_watermark = groups.iter().map(|g| g.latest_timestamp).max();
        // If --project was passed, filter groups by project for display only
        let groups = if !project_ids.is_empty() {
            filter_groups_by_project_ids(&conn, &groups, &project_ids)
        } else {
            groups
        };
        let total = groups.iter().map(|g| g.events.len()).sum();
        Some(SinceLastCheck {
            cursor_ms: prev_cursor,
            groups,
            total_event_count: total,
        })
    } else {
        None // First run — no section shown
    };

    // 7. Compute summary
    let needs_attention_count = open_issues
        .iter()
        .filter(|i| i.attention_state == AttentionState::NeedsAttention)
        .count()
        + open_mrs_authored
            .iter()
            .filter(|m| m.attention_state == AttentionState::NeedsAttention)
            .count()
        + reviewing_mrs
            .iter()
            .filter(|m| m.attention_state == AttentionState::NeedsAttention)
            .count()
        + mentioned_in
            .iter()
            .filter(|m| m.attention_state == AttentionState::NeedsAttention)
            .count();

    // Count distinct projects across all items
    let mut project_paths: HashSet<&str> = HashSet::new();
    for i in &open_issues {
        project_paths.insert(&i.project_path);
    }
    for m in &open_mrs_authored {
        project_paths.insert(&m.project_path);
    }
    for m in &reviewing_mrs {
        project_paths.insert(&m.project_path);
    }
    for m in &mentioned_in {
        project_paths.insert(&m.project_path);
    }

    let summary = MeSummary {
        project_count: project_paths.len(),
        open_issue_count: open_issues.len(),
        authored_mr_count: open_mrs_authored.len(),
        reviewing_mr_count: reviewing_mrs.len(),
        mentioned_in_count: mentioned_in.len(),
        needs_attention_count,
    };

    // 8. Assemble dashboard
    let dashboard = MeDashboard {
        username: username.to_string(),
        since_ms: Some(since_ms),
        summary,
        open_issues,
        open_mrs_authored,
        reviewing_mrs,
        mentioned_in,
        activity,
        since_last_check,
    };

    // 9. Render
    let elapsed_ms = start.elapsed().as_millis() as u64;

    if robot_mode {
        let fields = args.fields.as_deref();
        render_robot::print_me_json(&dashboard, elapsed_ms, fields, &config.gitlab.base_url)?;
    } else if show_all {
        render_human::print_me_dashboard(&dashboard, single_project);
    } else {
        render_human::print_me_dashboard_filtered(
            &dashboard,
            single_project,
            want_issues,
            want_mrs,
            want_mentions,
            want_activity,
        );
    }

    // 10. Advance cursor AFTER successful render (watermark pattern)
    // Uses max event timestamp from UNFILTERED results so --project filtering
    // doesn't permanently skip events from other projects.
    let watermark = global_watermark.unwrap_or_else(crate::core::time::now_ms);
    cursor::write_cursor(username, watermark)
        .map_err(|e| LoreError::Other(format!("write cursor: {e}")))?;

    Ok(())
}

/// Filter since-last-check groups to only those matching the given project IDs.
/// Used when --project narrows the display scope (cursor is still global).
fn filter_groups_by_project_ids(
    conn: &Connection,
    groups: &[types::SinceCheckGroup],
    project_ids: &[i64],
) -> Vec<types::SinceCheckGroup> {
    // Resolve project IDs to paths for matching
    let paths: HashSet<String> = project_ids
        .iter()
        .filter_map(|pid| {
            conn.query_row(
                "SELECT path_with_namespace FROM projects WHERE id = ?1",
                rusqlite::params![pid],
                |row| row.get::<_, String>(0),
            )
            .ok()
        })
        .collect();

    groups
        .iter()
        .filter(|g| paths.contains(&g.project_path))
        .cloned()
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::core::config::{
        EmbeddingConfig, GitLabConfig, LoggingConfig, ProjectConfig, ScoringConfig, StorageConfig,
        SyncConfig,
    };
    use crate::core::db::{create_connection, run_migrations};
    use std::path::Path;

    fn test_config(username: Option<&str>) -> Config {
        Config {
            gitlab: GitLabConfig {
                base_url: "https://gitlab.example.com".to_string(),
                token_env_var: "GITLAB_TOKEN".to_string(),
                token: None,
                username: username.map(String::from),
            },
            projects: vec![ProjectConfig {
                path: "group/project".to_string(),
            }],
            default_project: None,
            sync: SyncConfig::default(),
            storage: StorageConfig::default(),
            embedding: EmbeddingConfig::default(),
            logging: LoggingConfig::default(),
            scoring: ScoringConfig::default(),
        }
    }

    fn test_args(user: Option<&str>) -> MeArgs {
        MeArgs {
            issues: false,
            mrs: false,
            activity: false,
            mentions: false,
            since: None,
            project: None,
            all: false,
            user: user.map(String::from),
            fields: None,
            reset_cursor: false,
        }
    }

    #[test]
    fn resolve_username_cli_flag_wins() {
        let config = test_config(Some("config-user"));
        let args = test_args(Some("cli-user"));
        let result = resolve_username(&args, &config).unwrap();
        assert_eq!(result, "cli-user");
    }

    #[test]
    fn resolve_username_falls_back_to_config() {
        let config = test_config(Some("config-user"));
        let args = test_args(None);
        let result = resolve_username(&args, &config).unwrap();
        assert_eq!(result, "config-user");
    }

    #[test]
    fn resolve_username_errors_when_both_absent() {
        let config = test_config(None);
        let args = test_args(None);
        let err = resolve_username(&args, &config).unwrap_err();
        let msg = err.to_string();
        assert!(msg.contains("username"), "unexpected error: {msg}");
        assert!(msg.contains("--user"), "should suggest --user flag: {msg}");
    }

    fn test_config_with_default_project(
        username: Option<&str>,
        default_project: Option<&str>,
    ) -> Config {
        Config {
            gitlab: GitLabConfig {
                base_url: "https://gitlab.example.com".to_string(),
                token_env_var: "GITLAB_TOKEN".to_string(),
                token: None,
                username: username.map(String::from),
            },
            projects: vec![
                ProjectConfig {
                    path: "group/project".to_string(),
                },
                ProjectConfig {
                    path: "other/repo".to_string(),
                },
            ],
            default_project: default_project.map(String::from),
            sync: SyncConfig::default(),
            storage: StorageConfig::default(),
            embedding: EmbeddingConfig::default(),
            logging: LoggingConfig::default(),
            scoring: ScoringConfig::default(),
        }
    }

    fn setup_test_db() -> Connection {
        let conn = create_connection(Path::new(":memory:")).unwrap();
        run_migrations(&conn).unwrap();
        conn.execute(
            "INSERT INTO projects (gitlab_project_id, path_with_namespace, web_url)
             VALUES (1, 'group/project', 'https://gitlab.example.com/group/project')",
            [],
        )
        .unwrap();
        conn.execute(
            "INSERT INTO projects (gitlab_project_id, path_with_namespace, web_url)
             VALUES (2, 'other/repo', 'https://gitlab.example.com/other/repo')",
            [],
        )
        .unwrap();
        conn
    }

    #[test]
    fn resolve_project_scope_all_flag_returns_empty() {
        let conn = setup_test_db();
        let config = test_config(Some("jdoe"));
        let mut args = test_args(None);
        args.all = true;
        let ids = resolve_project_scope(&conn, &args, &config).unwrap();
        assert!(ids.is_empty(), "expected empty for --all, got {ids:?}");
    }

    #[test]
    fn resolve_project_scope_project_flag_resolves() {
        let conn = setup_test_db();
        let config = test_config(Some("jdoe"));
        let mut args = test_args(None);
        args.project = Some("group/project".to_string());
        let ids = resolve_project_scope(&conn, &args, &config).unwrap();
        assert_eq!(ids.len(), 1);
    }

    #[test]
    fn resolve_project_scope_default_project() {
        let conn = setup_test_db();
        let config = test_config_with_default_project(Some("jdoe"), Some("other/repo"));
        let args = test_args(None);
        let ids = resolve_project_scope(&conn, &args, &config).unwrap();
        assert_eq!(ids.len(), 1);
    }

    #[test]
    fn resolve_project_scope_no_default_returns_empty() {
        let conn = setup_test_db();
        let config = test_config(Some("jdoe"));
        let args = test_args(None);
        let ids = resolve_project_scope(&conn, &args, &config).unwrap();
        assert!(ids.is_empty(), "expected empty, got {ids:?}");
    }

    #[test]
    fn resolve_project_scope_project_flag_fuzzy_match() {
        let conn = setup_test_db();
        let config = test_config(Some("jdoe"));
        let mut args = test_args(None);
        args.project = Some("project".to_string());
        let ids = resolve_project_scope(&conn, &args, &config).unwrap();
        assert_eq!(ids.len(), 1);
    }

    #[test]
    fn resolve_project_scope_all_overrides_default_project() {
        let conn = setup_test_db();
        let config = test_config_with_default_project(Some("jdoe"), Some("group/project"));
        let mut args = test_args(None);
        args.all = true;
        let ids = resolve_project_scope(&conn, &args, &config).unwrap();
        assert!(
            ids.is_empty(),
            "expected --all to override default_project, got {ids:?}"
        );
    }

    #[test]
    fn resolve_project_scope_project_flag_overrides_default() {
        let conn = setup_test_db();
        let config = test_config_with_default_project(Some("jdoe"), Some("group/project"));
        let mut args = test_args(None);
        args.project = Some("other/repo".to_string());
        let ids = resolve_project_scope(&conn, &args, &config).unwrap();
        assert_eq!(ids.len(), 1, "expected --project to override default");
        // Verify it resolved the explicit project, not the default
        let resolved_path: String = conn
            .query_row(
                "SELECT path_with_namespace FROM projects WHERE id = ?1",
                rusqlite::params![ids[0]],
                |row| row.get(0),
            )
            .unwrap();
        assert_eq!(resolved_path, "other/repo");
    }

    #[test]
    fn resolve_project_scope_unknown_project_errors() {
        let conn = setup_test_db();
        let config = test_config(Some("jdoe"));
        let mut args = test_args(None);
        args.project = Some("nonexistent/project".to_string());
        let err = resolve_project_scope(&conn, &args, &config).unwrap_err();
        let msg = err.to_string();
        assert!(msg.contains("not found"), "expected not found error: {msg}");
    }

    #[test]
    fn show_all_sections_true_when_no_flags() {
        let args = test_args(None);
        assert!(args.show_all_sections());
    }

    #[test]
    fn show_all_sections_false_with_issues_flag() {
        let mut args = test_args(None);
        args.issues = true;
        assert!(!args.show_all_sections());
    }
}
