use anyhow::Result;
use rusqlite::Connection;
use std::io::{IsTerminal, Write};
use std::process::{Command, Stdio};

use crate::analytics;
use crate::cli::{GroupBy, ReportArgs, SessionReportArgs};
use crate::commands::report_layout::{
    ScorecardRow, append_legend, classify_lower_better, classify_ratio_higher_better,
    classify_ratio_lower_better, fmt_opt_decimal, fmt_ratio, fmt_ratio_percent, group_label,
    render_scorecard, truncate,
};
use crate::commands::report_scope;
use crate::db;

pub fn run(args: SessionReportArgs) -> Result<()> {
    let db = db::open()?;
    analytics::create_reporting_views(&db)?;
    let resolved = report_scope::resolve_main_report_args(&args.report, args.overall);
    let options = analytics::ReportQueryOptions {
        implicit_model_default: resolved.implicit_model_default,
    };

    let output = if args.list_sessions {
        let rows = analytics::query_session_list_rows(&db, &resolved.report)?;
        render_session_list(&rows)
    } else {
        let rows = analytics::query_session_report_with_options(&db, &resolved.report, options)?;
        let show_branch_hint = rows.is_empty()
            && task_report_hidden_branch_rows_exist(&db, &resolved.report, options)?;
        render_session_report(&rows, &resolved.report, show_branch_hint)
    };

    if std::io::stdout().is_terminal() && output.lines().count() > terminal_height() {
        pipe_to_pager(&output)
    } else {
        print!("{output}");
        Ok(())
    }
}

fn render_session_report(
    rows: &[analytics::SessionReportRow],
    report: &ReportArgs,
    show_branch_hint: bool,
) -> String {
    let mut out = String::new();
    out.push_str("Session Metrics\n");

    if rows.is_empty() {
        if show_branch_hint {
            out.push_str(
                "No ticket-style task rows matched. Try `paceflow session --group-by branch` or `--overall`.\n",
            );
        } else {
            out.push_str("No session rows found. Run `paceflow ingest` first.\n");
        }
        return out;
    }

    if !report.weekly && report.group_by.is_none() {
        let row = &rows[0];
        out.push_str(&format!("Sessions analyzed: {}\n\n", row.session_count));
        let scorecard = [
            ScorecardRow {
                label: "Time to first change",
                value: match row.avg_minutes_to_first_accepted_change {
                    Some(value) => format!("{value:.2} min"),
                    None => "N/A".to_string(),
                },
                status: classify_lower_better(row.avg_minutes_to_first_accepted_change, 15.0, 45.0),
            },
            ScorecardRow {
                label: "No-output sessions",
                value: fmt_ratio(&row.no_output_session_rate, 2),
                status: classify_ratio_lower_better(&row.no_output_session_rate, 15.0, 35.0),
            },
            ScorecardRow {
                label: "Sessions to commit",
                value: fmt_ratio(&row.s9_rate, 2),
                status: classify_ratio_higher_better(&row.s9_rate, 50.0, 25.0),
            },
            ScorecardRow {
                label: "Error pastes",
                value: fmt_ratio(&row.s6_rate, 2),
                status: classify_ratio_lower_better(&row.s6_rate, 10.0, 25.0),
            },
            ScorecardRow {
                label: "Avg prompts",
                value: fmt_opt_decimal(row.s2_avg, 2),
                status: classify_lower_better(row.s2_avg, 4.0, 7.0),
            },
            ScorecardRow {
                label: "Debug loops",
                value: fmt_ratio(&row.debug_loop_rate, 2),
                status: classify_ratio_lower_better(&row.debug_loop_rate, 10.0, 25.0),
            },
        ];
        out.push_str(&render_scorecard(&scorecard));
        append_legend(
            &mut out,
            &[
                "Avg prompts: average number of user prompts per session.",
                "Time to first change: average minutes from session start to first accepted code change.",
                "Debug loops: share of sessions that look like repeated fix-retry loops.",
                "Error pastes: share of sessions where an error message was pasted mid-session.",
                "Sessions to commit: share of sessions followed by a commit within 4 hours.",
                "No-output sessions: share of sessions with no accepted code changes.",
                "Status: lower is better except Sessions to commit.",
            ],
        );
        return out;
    }

    let show_week = report.weekly;
    let show_group = report.group_by.is_some();
    let show_branch = matches!(report.group_by, Some(GroupBy::Task));
    let mut headers = vec![];
    out.push('\n');
    if show_week {
        headers.push(format!("{:<10}", "Week"));
    }
    if show_group {
        headers.push(format!("{:<28}", group_label(report.group_by)));
    }
    if show_branch {
        headers.push(format!("{:<26}", "Branch"));
    }
    headers.push(format!("{:>8}", "Sessions"));
    headers.push(format!("{:>10}", "Prompts"));
    headers.push(format!("{:>11}", "First Chg"));
    headers.push(format!("{:>14}", "Loop"));
    headers.push(format!("{:>14}", "Error"));
    headers.push(format!("{:>14}", "To Commit"));
    headers.push(format!("{:>14}", "No Output"));
    out.push_str(&format!("{}\n", headers.join("  ")));

    for row in rows {
        let mut cols = vec![];
        if show_week {
            cols.push(format!("{:<10}", row.week_start.as_deref().unwrap_or("-")));
        }
        if show_group {
            cols.push(format!(
                "{:<28}",
                truncate(row.group_value.as_deref().unwrap_or("(all)"), 28)
            ));
        }
        if show_branch {
            cols.push(format!(
                "{:<26}",
                truncate(row.branch_name.as_deref().unwrap_or("-"), 26)
            ));
        }
        cols.push(format!("{:>8}", row.session_count));
        cols.push(format!("{:>10}", fmt_opt_decimal(row.s2_avg, 2)));
        cols.push(format!(
            "{:>11}",
            fmt_opt_decimal(row.avg_minutes_to_first_accepted_change, 1)
        ));
        cols.push(format!(
            "{:>14}",
            fmt_ratio_percent(&row.debug_loop_rate, 1)
        ));
        cols.push(format!("{:>14}", fmt_ratio_percent(&row.s6_rate, 1)));
        cols.push(format!("{:>14}", fmt_ratio_percent(&row.s9_rate, 1)));
        cols.push(format!(
            "{:>14}",
            fmt_ratio_percent(&row.no_output_session_rate, 1)
        ));
        out.push_str(&format!("{}\n", cols.join("  ")));
    }

    append_legend(
        &mut out,
        &[
            "Prompts = avg prompts per session.",
            "First Chg = avg minutes to first accepted code change.",
            "Loop, Error, To Commit, and No Output = percentage rates.",
        ],
    );

    out
}

fn render_session_list(rows: &[analytics::SessionListRow]) -> String {
    let mut out = String::new();
    out.push_str("Session List\n\n");
    if rows.is_empty() {
        out.push_str("No session rows found. Run `paceflow ingest` first.\n");
        return out;
    }

    let display_paths = rows
        .iter()
        .map(|row| display_project_path(&row.project_path))
        .collect::<Vec<_>>();
    let project_width = rows
        .iter()
        .zip(display_paths.iter())
        .map(|(_, path)| path.chars().count())
        .max()
        .unwrap_or(7)
        .max(7);

    out.push_str(&format!(
        "{:<8}  {:<14}  {:<8}  {:<width$}  {:<16}  {:>6}  {:>7}  {:>7}  {:>9}  {:>10}  {:>9}  {:>9}\n",
        "Provider",
        "Model",
        "Session",
        "Project",
        "Last active",
        "LOC",
        "+Lines",
        "-Lines",
        "Words/LOC",
        "Tokens",
        "Cost",
        "Cost/LOC",
        width = project_width
    ));

    for (row, project_path) in rows.iter().zip(display_paths.iter()) {
        let words_per_loc = if row.total_loc > 0 {
            format!("{:.1}", row.total_words as f64 / row.total_loc as f64)
        } else {
            "N/A".to_string()
        };
        let last_active = row.last_active.as_deref().unwrap_or("?");
        let last_active = &last_active[..last_active.len().min(16)];
        let session_short = &row.session_id[..row.session_id.len().min(8)];
        let cost_per_loc = if row.total_loc > 0 {
            row.total_cost_usd.map(|cost| cost / row.total_loc as f64)
        } else {
            None
        };
        out.push_str(&format!(
            "{:<8}  {:<14}  {:<8}  {:<width$}  {:<16}  {:>6}  {:>7}  {:>7}  {:>9}  {:>10}  {:>9}  {:>9}\n",
            row.provider,
            truncate(&row.model, 14),
            session_short,
            project_path,
            last_active,
            row.total_loc,
            row.total_added,
            row.total_removed,
            words_per_loc,
            fmt_token_count(row.total_tokens),
            fmt_money(row.total_cost_usd),
            fmt_money(cost_per_loc),
            width = project_width
        ));
    }

    out
}

fn fmt_token_count(value: i64) -> String {
    if value >= 1_000_000 {
        format!("{:.1}M", value as f64 / 1_000_000.0)
    } else if value >= 1_000 {
        format!("{:.1}K", value as f64 / 1_000.0)
    } else {
        value.to_string()
    }
}

fn fmt_money(value: Option<f64>) -> String {
    match value {
        Some(value) => format!("${value:.4}"),
        None => "N/A".to_string(),
    }
}

fn task_report_hidden_branch_rows_exist(
    db: &Connection,
    report: &ReportArgs,
    options: analytics::ReportQueryOptions,
) -> Result<bool> {
    if !matches!(report.group_by, Some(GroupBy::Task)) || report.task.is_some() {
        return Ok(false);
    }

    let mut branch_report = report.clone();
    branch_report.group_by = Some(GroupBy::Branch);
    branch_report.task = None;
    branch_report.limit = 1;
    Ok(!analytics::query_session_report_with_options(db, &branch_report, options)?.is_empty())
}

fn shorten_path(path: &str) -> String {
    if let Some(home) = dirs::home_dir() {
        let home_str = home.to_string_lossy();
        if path.starts_with(home_str.as_ref()) {
            return format!("~{}", &path[home_str.len()..]);
        }
    }
    path.to_string()
}

fn display_project_path(path: &str) -> String {
    const MAX_PROJECT_PATH_LEN: usize = 36;
    shorten_path_tail(&shorten_path(path), MAX_PROJECT_PATH_LEN)
}

fn shorten_path_tail(path: &str, max_len: usize) -> String {
    if path.chars().count() <= max_len {
        return path.to_string();
    }

    let (prefix, remainder) = if let Some(stripped) = path.strip_prefix("~/") {
        ("~/", stripped)
    } else if let Some(stripped) = path.strip_prefix('/') {
        ("/", stripped)
    } else {
        ("", path)
    };

    let segments = remainder
        .split('/')
        .filter(|segment| !segment.is_empty())
        .collect::<Vec<_>>();
    if segments.is_empty() {
        return truncate_tail(path, max_len);
    }

    let tail_one = segments.last().copied().unwrap_or_default().to_string();
    let tail_two = if segments.len() >= 2 {
        format!(
            "{}/{}",
            segments[segments.len() - 2],
            segments[segments.len() - 1]
        )
    } else {
        tail_one.clone()
    };

    let mut candidates = Vec::new();
    if let Some(first) = segments.first() {
        candidates.push(format!("{prefix}{first}/.../{tail_two}"));
    }
    candidates.push(format!("{prefix}.../{tail_two}"));
    candidates.push(format!("{prefix}.../{tail_one}"));

    for candidate in candidates {
        if candidate.chars().count() <= max_len {
            return candidate;
        }
    }

    truncate_tail(&tail_one, max_len)
}

fn truncate_tail(value: &str, max_len: usize) -> String {
    let value_chars = value.chars().collect::<Vec<_>>();
    if value_chars.len() <= max_len {
        return value.to_string();
    }
    if max_len <= 3 {
        return "...".to_string();
    }

    let tail = value_chars[value_chars.len() - (max_len - 3)..]
        .iter()
        .collect::<String>();
    format!("...{tail}")
}

fn terminal_height() -> usize {
    terminal_size::terminal_size()
        .map(|(_, terminal_size::Height(height))| height as usize)
        .unwrap_or(24)
}

fn pipe_to_pager(output: &str) -> Result<()> {
    let pager = std::env::var("PAGER").unwrap_or_else(|_| "less".to_string());
    let mut command = Command::new(&pager);
    if pager == "less" || pager.ends_with("/less") {
        command.args(["-R", "-F", "-X"]);
    }

    match command.stdin(Stdio::piped()).spawn() {
        Ok(mut child) => {
            if let Some(mut stdin) = child.stdin.take() {
                let _ = stdin.write_all(output.as_bytes());
            }
            child.wait()?;
        }
        Err(_) => print!("{output}"),
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::{display_project_path, render_session_list, render_session_report};
    use crate::analytics::{RatioMetric, SessionListRow, SessionReportRow};
    use crate::cli::{GroupBy, ReportArgs};

    fn row(project_path: &str) -> SessionListRow {
        SessionListRow {
            provider: "cursor".to_string(),
            model: "cursor/claude-3.5-sonnet".to_string(),
            session_id: "12345678-session".to_string(),
            project_path: project_path.to_string(),
            last_active: Some("2026-03-17T09:30:00Z".to_string()),
            total_words: 100,
            total_loc: 20,
            total_added: 10,
            total_removed: 5,
            total_tokens: 12_500,
            total_cost_usd: Some(1.25),
        }
    }

    #[test]
    fn display_project_path_preserves_distinguishing_tail() {
        let rendered =
            display_project_path("/Users/daniel/work/company/apps/mobile/customer-portal");
        assert!(
            rendered.ends_with("apps/mobile/customer-portal")
                || rendered.ends_with("mobile/customer-portal")
        );
        assert!(rendered.contains("..."));
    }

    #[test]
    fn render_session_list_shows_distinct_project_tails() {
        let output = render_session_list(&[
            row("/Users/daniel/work/company/apps/mobile/customer-portal"),
            row("/Users/daniel/work/company/apps/mobile/admin-portal"),
        ]);

        assert!(output.contains("customer-portal"));
        assert!(output.contains("admin-portal"));
        assert!(!output.contains("/Users/daniel/work/company/apps/mobile/customer-portal"));
    }

    #[test]
    fn render_session_report_uses_scorecard_summary_with_footer() {
        let rows = vec![SessionReportRow {
            week_start: None,
            group_value: None,
            branch_name: None,
            session_count: 411,
            s2_avg: Some(5.56),
            avg_minutes_to_first_accepted_change: Some(46.63),
            debug_loop_rate: RatioMetric {
                numerator: 1,
                denominator: 411,
            },
            s6_rate: RatioMetric {
                numerator: 69,
                denominator: 411,
            },
            s9_rate: RatioMetric {
                numerator: 166,
                denominator: 411,
            },
            no_output_session_rate: RatioMetric {
                numerator: 159,
                denominator: 411,
            },
        }];
        let report = ReportArgs {
            weekly: false,
            group_by: None,
            from: None,
            to: None,
            repo: None,
            all_projects: false,
            provider: None,
            task: None,
            branch: None,
            model: None,
            limit: 50,
        };

        let rendered = render_session_report(&rows, &report, false);
        assert!(rendered.contains("Sessions analyzed: 411"));
        assert!(rendered.contains("│ Signal"));
        assert!(rendered.contains("│ Time to first change"));
        assert!(rendered.contains("46.63 min"));
        assert!(rendered.contains("watch"));
        assert!(rendered.contains("risk"));
        assert!(rendered.contains("Legend:"));
        assert!(rendered.contains("Status: lower is better except Sessions to commit."));
        assert!(!rendered.contains("Average user prompts ="));
    }

    #[test]
    fn render_session_report_suggests_branch_view_for_hidden_task_rows() {
        let report = ReportArgs {
            weekly: false,
            group_by: Some(GroupBy::Task),
            from: None,
            to: None,
            repo: None,
            all_projects: false,
            provider: Some("claude".to_string()),
            task: None,
            branch: None,
            model: None,
            limit: 50,
        };

        let rendered = render_session_report(&[], &report, true);
        assert!(rendered.contains("No ticket-style task rows matched."));
        assert!(rendered.contains("`paceflow session --group-by branch`"));
        assert!(!rendered.contains("Run `paceflow ingest` first."));
    }
}
