mod active;
mod expert;
mod overlap;
mod reviews;
pub mod types;
mod workload;

pub use types::*;

// Re-export submodule functions for tests (tests use `use super::*`).
#[cfg(test)]
use active::query_active;
#[cfg(test)]
use expert::{build_expert_sql_v2, half_life_decay, query_expert};
#[cfg(test)]
use overlap::{format_overlap_role, query_overlap};
#[cfg(test)]
use reviews::{normalize_review_prefix, query_reviews};
#[cfg(test)]
use workload::query_workload;

use rusqlite::Connection;
use serde::Serialize;

use crate::Config;
use crate::cli::WhoArgs;
use crate::cli::render::Theme;
use crate::cli::robot::RobotMeta;
use crate::core::db::create_connection;
use crate::core::error::{LoreError, Result};
use crate::core::path_resolver::normalize_repo_path;
use crate::core::paths::get_db_path;
use crate::core::project::resolve_project;
use crate::core::time::{ms_to_iso, now_ms, parse_since, parse_since_from};

#[cfg(test)]
use crate::core::config::ScoringConfig;
#[cfg(test)]
use crate::core::path_resolver::{SuffixResult, build_path_query, escape_like, suffix_probe};

// ─── Mode Discrimination ────────────────────────────────────────────────────

/// Determines which query mode to run based on args.
/// Path variants own their strings because path normalization produces new `String`s.
/// Username variants borrow from args since no normalization is needed.
enum WhoMode<'a> {
    /// lore who <file-path>  OR  lore who --path <path>
    Expert { path: String },
    /// lore who <username>
    Workload { username: &'a str },
    /// lore who <username> --reviews
    Reviews { username: &'a str },
    /// lore who --active
    Active,
    /// lore who --overlap <path>
    Overlap { path: String },
}

fn resolve_mode<'a>(args: &'a WhoArgs) -> Result<WhoMode<'a>> {
    // Explicit --path flag always wins (handles root files like README.md,
    // LICENSE, Makefile -- anything without a / that can't be auto-detected)
    if let Some(p) = &args.path {
        return Ok(WhoMode::Expert {
            path: normalize_repo_path(p),
        });
    }
    if args.active {
        return Ok(WhoMode::Active);
    }
    if let Some(path) = &args.overlap {
        return Ok(WhoMode::Overlap {
            path: normalize_repo_path(path),
        });
    }
    if let Some(target) = &args.target {
        let clean = target.strip_prefix('@').unwrap_or(target);
        if args.reviews {
            return Ok(WhoMode::Reviews { username: clean });
        }
        // Disambiguation: if target contains '/', it's a file path.
        // GitLab usernames never contain '/'.
        // Root files (no '/') require --path.
        if clean.contains('/') {
            return Ok(WhoMode::Expert {
                path: normalize_repo_path(clean),
            });
        }
        return Ok(WhoMode::Workload { username: clean });
    }
    Err(LoreError::Other(
        "Provide a username, file path, --active, or --overlap <path>.\n\n\
         Examples:\n  \
         lore who src/features/auth/\n  \
         lore who @username\n  \
         lore who --active\n  \
         lore who --overlap src/features/\n  \
         lore who --path README.md\n  \
         lore who --path Makefile"
            .to_string(),
    ))
}

fn validate_mode_flags(mode: &WhoMode<'_>, args: &WhoArgs) -> Result<()> {
    if args.detail && !matches!(mode, WhoMode::Expert { .. }) {
        return Err(LoreError::Other(
            "--detail is only supported in expert mode (`lore who --path <path>` or `lore who <path/with/slash>`).".to_string(),
        ));
    }
    Ok(())
}

// ─── Entry Point ─────────────────────────────────────────────────────────────

/// Main entry point. Resolves mode + resolved inputs once, then dispatches.
pub fn run_who(config: &Config, args: &WhoArgs) -> Result<WhoRun> {
    let db_path = get_db_path(config.storage.db_path.as_deref());
    let conn = create_connection(&db_path)?;

    let project_id = args
        .project
        .as_deref()
        .map(|p| resolve_project(&conn, p))
        .transpose()?;

    let project_path = project_id
        .map(|id| lookup_project_path(&conn, id))
        .transpose()?;

    let mode = resolve_mode(args)?;
    validate_mode_flags(&mode, args)?;

    // since_mode semantics:
    // - expert/reviews/active/overlap: default window applies if args.since is None -> "default"
    // - workload: no default window; args.since None => "none"
    let since_mode_for_defaulted = if args.since.is_some() {
        "explicit"
    } else {
        "default"
    };
    let since_mode_for_workload = if args.since.is_some() {
        "explicit"
    } else {
        "none"
    };

    let limit = args.limit.map_or(usize::MAX, usize::from);

    match mode {
        WhoMode::Expert { path } => {
            // Compute as_of first so --since durations are relative to it.
            let as_of_ms = match &args.as_of {
                Some(v) => parse_since(v).ok_or_else(|| {
                    LoreError::Other(format!(
                        "Invalid --as-of value: '{v}'. Use a duration (30d, 6m) or date (2024-01-15)"
                    ))
                })?,
                None => now_ms(),
            };
            let since_ms = if args.all_history {
                0
            } else {
                resolve_since_from(args.since.as_deref(), "24m", as_of_ms)?
            };
            let result = expert::query_expert(
                &conn,
                &path,
                project_id,
                since_ms,
                as_of_ms,
                limit,
                &config.scoring,
                args.detail,
                args.explain_score,
                args.include_bots,
            )?;
            Ok(WhoRun {
                resolved_input: WhoResolvedInput {
                    mode: "expert".to_string(),
                    project_id,
                    project_path,
                    since_ms: Some(since_ms),
                    since_iso: Some(ms_to_iso(since_ms)),
                    since_mode: since_mode_for_defaulted.to_string(),
                    limit: args.limit,
                },
                result: WhoResult::Expert(result),
            })
        }
        WhoMode::Workload { username } => {
            let since_ms = args
                .since
                .as_deref()
                .map(resolve_since_required)
                .transpose()?;

            let result = workload::query_workload(
                &conn,
                username,
                project_id,
                since_ms,
                limit,
                args.include_closed,
            )?;
            Ok(WhoRun {
                resolved_input: WhoResolvedInput {
                    mode: "workload".to_string(),
                    project_id,
                    project_path,
                    since_ms,
                    since_iso: since_ms.map(ms_to_iso),
                    since_mode: since_mode_for_workload.to_string(),
                    limit: args.limit,
                },
                result: WhoResult::Workload(result),
            })
        }
        WhoMode::Reviews { username } => {
            let since_ms = resolve_since(args.since.as_deref(), "6m")?;
            let result = reviews::query_reviews(&conn, username, project_id, since_ms)?;
            Ok(WhoRun {
                resolved_input: WhoResolvedInput {
                    mode: "reviews".to_string(),
                    project_id,
                    project_path,
                    since_ms: Some(since_ms),
                    since_iso: Some(ms_to_iso(since_ms)),
                    since_mode: since_mode_for_defaulted.to_string(),
                    limit: args.limit,
                },
                result: WhoResult::Reviews(result),
            })
        }
        WhoMode::Active => {
            let since_ms = resolve_since(args.since.as_deref(), "7d")?;

            let result =
                active::query_active(&conn, project_id, since_ms, limit, args.include_closed)?;
            Ok(WhoRun {
                resolved_input: WhoResolvedInput {
                    mode: "active".to_string(),
                    project_id,
                    project_path,
                    since_ms: Some(since_ms),
                    since_iso: Some(ms_to_iso(since_ms)),
                    since_mode: since_mode_for_defaulted.to_string(),
                    limit: args.limit,
                },
                result: WhoResult::Active(result),
            })
        }
        WhoMode::Overlap { path } => {
            let since_ms = resolve_since(args.since.as_deref(), "30d")?;

            let result = overlap::query_overlap(&conn, &path, project_id, since_ms, limit)?;
            Ok(WhoRun {
                resolved_input: WhoResolvedInput {
                    mode: "overlap".to_string(),
                    project_id,
                    project_path,
                    since_ms: Some(since_ms),
                    since_iso: Some(ms_to_iso(since_ms)),
                    since_mode: since_mode_for_defaulted.to_string(),
                    limit: args.limit,
                },
                result: WhoResult::Overlap(result),
            })
        }
    }
}

// ─── Helpers ─────────────────────────────────────────────────────────────────

/// Look up the project path for a resolved project ID.
fn lookup_project_path(conn: &Connection, project_id: i64) -> Result<String> {
    conn.query_row(
        "SELECT path_with_namespace FROM projects WHERE id = ?1",
        rusqlite::params![project_id],
        |row| row.get(0),
    )
    .map_err(|e| LoreError::Other(format!("Failed to look up project path: {e}")))
}

/// Parse --since with a default fallback.
fn resolve_since(input: Option<&str>, default: &str) -> Result<i64> {
    let s = input.unwrap_or(default);
    parse_since(s).ok_or_else(|| {
        LoreError::Other(format!(
            "Invalid --since value: '{s}'. Use a duration (7d, 2w, 6m) or date (2024-01-15)"
        ))
    })
}

/// Parse --since with a default fallback, relative to a reference timestamp.
/// Durations (7d, 2w, 6m) are computed from `reference_ms` instead of now.
fn resolve_since_from(input: Option<&str>, default: &str, reference_ms: i64) -> Result<i64> {
    let s = input.unwrap_or(default);
    parse_since_from(s, reference_ms).ok_or_else(|| {
        LoreError::Other(format!(
            "Invalid --since value: '{s}'. Use a duration (7d, 2w, 6m) or date (2024-01-15)"
        ))
    })
}

/// Parse --since without a default (returns error if invalid).
fn resolve_since_required(input: &str) -> Result<i64> {
    parse_since(input).ok_or_else(|| {
        LoreError::Other(format!(
            "Invalid --since value: '{input}'. Use a duration (7d, 2w, 6m) or date (2024-01-15)"
        ))
    })
}

// ─── Human Output ────────────────────────────────────────────────────────────

pub fn print_who_human(result: &WhoResult, project_path: Option<&str>) {
    match result {
        WhoResult::Expert(r) => expert::print_expert_human(r, project_path),
        WhoResult::Workload(r) => workload::print_workload_human(r),
        WhoResult::Reviews(r) => reviews::print_reviews_human(r),
        WhoResult::Active(r) => active::print_active_human(r, project_path),
        WhoResult::Overlap(r) => overlap::print_overlap_human(r, project_path),
    }
}

/// Print a dim hint when results aggregate across all projects.
pub(super) fn print_scope_hint(project_path: Option<&str>) {
    if project_path.is_none() {
        println!(
            "  {}",
            Theme::dim().render("(aggregated across all projects; use -p to scope)")
        );
    }
}

// ─── Robot JSON Output ───────────────────────────────────────────────────────

pub fn print_who_json(run: &WhoRun, args: &WhoArgs, elapsed_ms: u64) {
    let (mode, data) = match &run.result {
        WhoResult::Expert(r) => ("expert", expert::expert_to_json(r)),
        WhoResult::Workload(r) => ("workload", workload::workload_to_json(r)),
        WhoResult::Reviews(r) => ("reviews", reviews::reviews_to_json(r)),
        WhoResult::Active(r) => ("active", active::active_to_json(r)),
        WhoResult::Overlap(r) => ("overlap", overlap::overlap_to_json(r)),
    };

    // Raw CLI args -- what the user typed
    let input = serde_json::json!({
        "target": args.target,
        "path": args.path,
        "project": args.project,
        "since": args.since,
        "limit": args.limit,
        "detail": args.detail,
        "as_of": args.as_of,
        "explain_score": args.explain_score,
        "include_bots": args.include_bots,
        "all_history": args.all_history,
    });

    // Resolved/computed values -- what actually ran
    let resolved_input = serde_json::json!({
        "mode": run.resolved_input.mode,
        "project_id": run.resolved_input.project_id,
        "project_path": run.resolved_input.project_path,
        "since_ms": run.resolved_input.since_ms,
        "since_iso": run.resolved_input.since_iso,
        "since_mode": run.resolved_input.since_mode,
        "limit": run.resolved_input.limit,
    });

    let output = WhoJsonEnvelope {
        ok: true,
        data: WhoJsonData {
            mode: mode.to_string(),
            input,
            resolved_input,
            result: data,
        },
        meta: RobotMeta::new(elapsed_ms),
    };

    let mut value = serde_json::to_value(&output).unwrap_or_else(|e| {
        serde_json::json!({"ok":false,"error":{"code":"INTERNAL_ERROR","message":format!("JSON serialization failed: {e}")}})
    });

    if let Some(f) = &args.fields {
        let preset_key = format!("who_{mode}");
        let expanded = crate::cli::robot::expand_fields_preset(f, &preset_key);
        // Each who mode uses a different array key; try all possible keys
        for key in &[
            "experts",
            "assigned_issues",
            "authored_mrs",
            "review_mrs",
            "categories",
            "discussions",
            "users",
        ] {
            crate::cli::robot::filter_fields(&mut value, key, &expanded);
        }
    }

    match serde_json::to_string(&value) {
        Ok(json) => println!("{json}"),
        Err(e) => eprintln!("Error serializing to JSON: {e}"),
    }
}

#[derive(Serialize)]
struct WhoJsonEnvelope {
    ok: bool,
    data: WhoJsonData,
    meta: RobotMeta,
}

#[derive(Serialize)]
struct WhoJsonData {
    mode: String,
    input: serde_json::Value,
    resolved_input: serde_json::Value,
    #[serde(flatten)]
    result: serde_json::Value,
}

// ─── Tests ───────────────────────────────────────────────────────────────────

#[cfg(test)]
#[path = "../who_tests.rs"]
mod tests;
