use chrono::Datelike;
use chrono::Timelike;
use colored::Colorize;
use rusqlite::Connection;
use std::collections::HashMap;

use crate::aliases::{classify, load_aliases};
use crate::db::Event;
use crate::util::find_project_root;

fn extract_files(cmd: &str) -> Vec<String> {
    let mut words = cmd.split_whitespace();
    words.next(); // skip binary name
    words
        .filter(|s| !s.starts_with('-'))
        .map(|s| s.to_string())
        .collect()
}

pub fn fmt_duration(secs: i64) -> String {
    if secs >= 3600 {
        format!("{}h{:02}", secs / 3600, (secs % 3600) / 60)
    } else if secs >= 60 {
        format!("{}m", secs / 60)
    } else {
        format!("{}s", secs)
    }
}

fn compact(cmds: &[String]) -> String {
    let mut counts: HashMap<&str, usize> = HashMap::new();
    for c in cmds {
        *counts.entry(c.as_str()).or_insert(0) += 1;
    }
    let mut parts: Vec<(&str, usize)> = counts.into_iter().collect();
    // Sort by frequency descending, then alphabetically within same count
    parts.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
    parts
        .iter()
        .map(|(cmd, n)| {
            if *n > 1 {
                format!("{} ×{}", cmd, n)
            } else {
                cmd.to_string()
            }
        })
        .collect::<Vec<_>>()
        .join(" · ")
}

pub fn print_recap(events: &[Event]) {
    let aliases = load_aliases();
    if events.is_empty() {
        println!("No activity recorded.");
        return;
    }

    let date = chrono::DateTime::from_timestamp_millis(events[0].ts)
        .unwrap()
        .with_timezone(&chrono::Local)
        .format("%B %d");
    println!("{}", format!("── {} ─────────────────────────────────────\n", date).dimmed());

    let mut blocks: HashMap<(i64, &str), Vec<&Event>> = HashMap::new();
    for e in events {
        let local_dt = chrono::DateTime::from_timestamp_millis(e.ts)
            .unwrap()
            .with_timezone(&chrono::Local);
        // Use local date+hour as key to avoid UTC epoch hour boundaries splitting
        // blocks incorrectly in non-UTC timezones.
        let hour_key = local_dt.date_naive().num_days_from_ce() as i64 * 24
            + local_dt.hour() as i64;
        blocks.entry((hour_key, &e.pwd)).or_default().push(e);
    }

    let mut keys: Vec<(i64, &str)> = blocks.keys().cloned().collect();
    keys.sort();

    for (hour, pwd) in keys {
        let block = &blocks[&(hour, pwd)];
        let local_hour = chrono::DateTime::from_timestamp_millis(block[0].ts)
            .unwrap()
            .with_timezone(&chrono::Local)
            .hour();
        let duration: i64 = block.iter().map(|e| e.duration).sum::<i64>() / 1000;

        let mut edited: Vec<String> = vec![];
        let mut viewed: Vec<String> = vec![];
        let mut ran: Vec<String> = vec![];
        let mut errors: usize = 0;

        for e in block.iter() {
            if e.exit_code != 0 {
                errors += 1;
            }
            match classify(&e.cmd, &aliases) {
                "edit" => edited.extend(extract_files(&e.cmd)),
                "view" => viewed.extend(extract_files(&e.cmd)),
                _ => {
                    ran.push(e.cmd.split_whitespace().next().unwrap_or("").to_string());
                }
            }
        }

        let short_pwd = pwd.replace(&std::env::var("HOME").unwrap_or_default(), "~");
        let project = find_project_root(pwd).map(|root| {
            std::path::Path::new(&root)
                .file_name()
                .unwrap()
                .to_string_lossy()
                .to_string()
        });

        let hour_str = format!("{:02}h", local_hour).cyan().to_string();
        let dur_str = format!("({})", fmt_duration(duration)).dimmed().to_string();
        let pwd_str = short_pwd.dimmed().to_string();

        match &project {
            Some(name) => print!(
                "  {}  {}  {}  {}",
                hour_str,
                name.green().bold(),
                pwd_str,
                dur_str
            ),
            None => print!("  {}  {}  {}", hour_str, pwd_str, dur_str),
        }
        if errors > 0 {
            print!(
                "  {}",
                format!("[{} error{}]", errors, if errors > 1 { "s" } else { "" }).red()
            );
        }
        println!();

        if !edited.is_empty() {
            println!("    {}  {}", "[edit]".yellow(), compact(&edited));
        }
        if !viewed.is_empty() {
            println!("    {}  {}", "[view]".blue(), compact(&viewed));
        }
        if !ran.is_empty() {
            println!("    {}   {}", "[run]".dimmed(), compact(&ran));
        }
        println!();
    }
}

pub fn print_search(events: &[Event], query: &str) {
    if events.is_empty() {
        println!("No results for \"{}\".", query);
        return;
    }
    println!(
        "{}",
        format!(
            "── search: {} ({} results) ─────────────────\n",
            query,
            events.len()
        )
        .dimmed()
    );
    let home = std::env::var("HOME").unwrap_or_default();
    for e in events {
        let date = chrono::DateTime::from_timestamp_millis(e.ts)
            .unwrap()
            .with_timezone(&chrono::Local)
            .format("%b %d %H:%M");
        let short_pwd = e.pwd.replace(&home, "~");
        let status = if e.exit_code != 0 {
            " [err]".red().to_string()
        } else {
            String::new()
        };
        println!(
            "  {}  {}  {}{}",
            date.to_string().dimmed(),
            short_pwd.dimmed(),
            e.cmd,
            status
        );
    }
    println!();
}

pub fn print_stats(events: &[Event], weeks_ago: i64) {
    if events.is_empty() {
        let label = if weeks_ago == 0 {
            "this week".to_string()
        } else {
            format!("{} week{} ago", weeks_ago, if weeks_ago == 1 { "" } else { "s" })
        };
        println!("No activity recorded {}.", label);
        return;
    }

    let header_label = if weeks_ago == 0 {
        "This week".to_string()
    } else {
        format!("{} week{} ago", weeks_ago, if weeks_ago == 1 { "" } else { "s" })
    };
    println!(
        "{}",
        format!("── recap stats: {} ─────────────────────────────\n", header_label).dimmed()
    );

    let mut by_day: HashMap<i32, i64> = HashMap::new();
    for e in events {
        let day = chrono::DateTime::from_timestamp_millis(e.ts)
            .unwrap()
            .with_timezone(&chrono::Local)
            .date_naive()
            .num_days_from_ce();
        *by_day.entry(day).or_insert(0) += e.duration;
    }

    println!("  {}", header_label);
    let days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
    let now = chrono::Local::now();
    let offset = weeks_ago * 7;
    for i in (0..7).rev() {
        let day = now - chrono::Duration::days(i + offset);
        let day_key = day.date_naive().num_days_from_ce();
        let secs = by_day.get(&day_key).cloned().unwrap_or(0) / 1000;
        let hours = secs / 3600;
        let mins = (secs % 3600) / 60;
        let bar_len = (hours as usize * 2).min(12);
        let filled = "█".repeat(bar_len).green().to_string();
        let empty = "░".repeat(12 - bar_len).dimmed().to_string();
        let label = days[day.weekday().num_days_from_monday() as usize];
        let today = if weeks_ago == 0 && i == 0 {
            "  ← today".cyan().to_string()
        } else {
            String::new()
        };
        println!("  {}  {}{}  {:01}h{:02}{}", label, filled, empty, hours, mins, today);
    }

    println!("\n  {}", "Top projects".bold());
    let mut by_project: HashMap<String, i64> = HashMap::new();
    for e in events {
        let name = find_project_root(&e.pwd)
            .map(|r| {
                std::path::Path::new(&r)
                    .file_name()
                    .unwrap()
                    .to_string_lossy()
                    .to_string()
            })
            .unwrap_or_else(|| "other".to_string());
        *by_project.entry(name).or_insert(0) += e.duration;
    }
    let max_proj = by_project.values().cloned().max().unwrap_or(1);
    let mut proj_sorted: Vec<_> = by_project.iter().collect();
    proj_sorted.sort_by(|a, b| b.1.cmp(a.1));
    for (name, dur) in proj_sorted.iter().take(5) {
        let secs = *dur / 1000;
        let hours = secs / 3600;
        let mins = (secs % 3600) / 60;
        let bar_len = (*dur * 12 / max_proj) as usize;
        let filled = "█".repeat(bar_len).green().to_string();
        let empty = "░".repeat(12 - bar_len).dimmed().to_string();
        println!("  {:<12}  {}{}  {:01}h{:02}", name, filled, empty, hours, mins);
    }

    println!("\n  {}", "Top commands".bold());
    let aliases = load_aliases();
    let mut by_cmd: HashMap<String, usize> = HashMap::new();
    for e in events {
        let bin = e.cmd.split_whitespace().next().unwrap_or("").to_string();
        let resolved = aliases.get(&bin).cloned().unwrap_or_else(|| bin.clone());
        *by_cmd.entry(resolved).or_insert(0) += 1;
    }
    let max_cmd = by_cmd.values().cloned().max().unwrap_or(1);
    let mut cmd_sorted: Vec<_> = by_cmd.iter().collect();
    cmd_sorted.sort_by(|a, b| b.1.cmp(a.1));
    for (cmd, count) in cmd_sorted.iter().take(5) {
        let bar_len = (*count * 12 / max_cmd).min(12);
        let filled = "█".repeat(bar_len).green().to_string();
        let empty = "░".repeat(12 - bar_len).dimmed().to_string();
        println!("  {:<12}  {}{}  {}x", cmd, filled, empty, count);
    }

    let mut by_hour: HashMap<u32, usize> = HashMap::new();
    for e in events {
        let h = chrono::DateTime::from_timestamp_millis(e.ts)
            .unwrap()
            .with_timezone(&chrono::Local)
            .hour();
        *by_hour.entry(h).or_insert(0) += 1;
    }
    if let Some((peak_hour, _)) = by_hour.iter().max_by_key(|(_, v)| *v) {
        println!(
            "\n  Most active hour:  {}",
            format!("{:02}h – {:02}h", peak_hour, peak_hour + 1).cyan()
        );
    }

    let errors: usize = events.iter().filter(|e| e.exit_code != 0).count();
    if errors > 0 {
        let top_err = {
            let mut err_cmds: HashMap<String, usize> = HashMap::new();
            for e in events.iter().filter(|e| e.exit_code != 0) {
                let bin = e.cmd.split_whitespace().next().unwrap_or("").to_string();
                *err_cmds.entry(bin).or_insert(0) += 1;
            }
            err_cmds
                .into_iter()
                .max_by_key(|(_, v)| *v)
                .map(|(k, _)| k)
                .unwrap_or_default()
        };
        println!("  Errors:  {}  (mostly {})", errors.to_string().red(), top_err);
    }

    println!();
}

pub fn print_timeline(events: &[Event]) {
    if events.is_empty() {
        println!("No activity recorded.");
        return;
    }

    let date = chrono::DateTime::from_timestamp_millis(events[0].ts)
        .unwrap()
        .with_timezone(&chrono::Local)
        .format("%B %d");
    println!(
        "{}",
        format!("── {} timeline ───────────────────────────────────────\n", date).dimmed()
    );

    let first_hour = chrono::DateTime::from_timestamp_millis(events.first().unwrap().ts)
        .unwrap()
        .with_timezone(&chrono::Local)
        .hour();
    let last_hour = chrono::DateTime::from_timestamp_millis(events.last().unwrap().ts)
        .unwrap()
        .with_timezone(&chrono::Local)
        .hour();

    // Per-hour: (total active ms, ms per project)
    let mut hours: HashMap<u32, (i64, HashMap<String, i64>)> = HashMap::new();
    for e in events {
        let h = chrono::DateTime::from_timestamp_millis(e.ts)
            .unwrap()
            .with_timezone(&chrono::Local)
            .hour();
        let entry = hours.entry(h).or_insert((0, HashMap::new()));
        entry.0 += e.duration;
        let project = find_project_root(&e.pwd)
            .map(|r| {
                std::path::Path::new(&r)
                    .file_name()
                    .unwrap()
                    .to_string_lossy()
                    .to_string()
            })
            .unwrap_or_else(|| {
                std::path::Path::new(&e.pwd)
                    .file_name()
                    .map(|f| f.to_string_lossy().to_string())
                    .unwrap_or_else(|| "other".to_string())
            });
        *entry.1.entry(project).or_insert(0) += e.duration;
    }

    const BAR_WIDTH: usize = 30; // 1 char = 2 min

    for h in first_hour..=last_hour {
        if let Some((active_ms, by_project)) = hours.get(&h) {
            let active_secs = active_ms / 1000;
            let mins = (active_secs / 60).min(60) as usize;
            let filled_len = (mins * BAR_WIDTH / 60).min(BAR_WIDTH);
            let filled = "█".repeat(filled_len).green().to_string();
            let empty = "░".repeat(BAR_WIDTH - filled_len).dimmed().to_string();

            let top_project = by_project
                .iter()
                .max_by_key(|(_, v)| *v)
                .map(|(k, _)| k.as_str())
                .unwrap_or("other");
            let total_mins = active_secs / 60;
            let duration_str = if total_mins >= 60 {
                format!("{}h{:02}", total_mins / 60, total_mins % 60)
            } else {
                format!("{}m", total_mins)
            };
            println!(
                "  {:02}  {}{}  {} {}",
                h,
                filled,
                empty,
                top_project.green().bold(),
                duration_str.dimmed()
            );
        } else {
            println!(
                "  {:02}  {}  {}",
                h,
                "░".repeat(BAR_WIDTH).dimmed(),
                "idle".dimmed()
            );
        }
    }
    println!();
}

pub fn print_streak(conn: &Connection) -> Result<(), Box<dyn std::error::Error>> {
    // Fetch all distinct days that have at least one event, most recent first.
    let mut stmt = conn.prepare(
        "SELECT DISTINCT CAST(ts / 86400000 AS INTEGER) as day
         FROM events
         ORDER BY day DESC",
    )?;
    let days: Vec<i64> = stmt
        .query_map([], |row| row.get(0))?
        .flatten()
        .collect();

    if days.is_empty() {
        println!("No activity recorded yet.");
        return Ok(());
    }

    let today_day = chrono::Local::now().timestamp_millis() / 86400000;

    // Count consecutive days ending at today or yesterday.
    let mut streak = 0i64;
    let mut expected = if days[0] == today_day || days[0] == today_day - 1 {
        days[0]
    } else {
        // Most recent activity is older — streak is broken.
        let last_date = chrono::DateTime::from_timestamp_millis(days[0] * 86400000)
            .unwrap()
            .with_timezone(&chrono::Local)
            .format("%B %d");
        println!("  Current streak:  {}", "0 days".dimmed());
        println!("  Last activity:   {}", last_date.to_string().dimmed());
        println!();
        return Ok(());
    };

    for day in &days {
        if *day == expected {
            streak += 1;
            expected -= 1;
        } else {
            break;
        }
    }

    // Longest streak ever
    let mut longest = 0i64;
    let mut current_run = 1i64;
    for i in 1..days.len() {
        if days[i] == days[i - 1] - 1 {
            current_run += 1;
            if current_run > longest {
                longest = current_run;
            }
        } else {
            current_run = 1;
        }
    }
    if current_run > longest {
        longest = current_run;
    }
    if streak > longest {
        longest = streak;
    }

    let streak_str = format!("{} day{}", streak, if streak == 1 { "" } else { "s" });
    let longest_str = format!("{} day{}", longest, if longest == 1 { "" } else { "s" });

    println!("{}", "── streak ───────────────────────────────────────\n".dimmed());
    println!("  Current streak:  {}", streak_str.green().bold());
    println!("  Longest streak:  {}", longest_str.cyan());
    println!("  Total days:      {}", days.len().to_string().dimmed());
    println!();

    Ok(())
}

pub fn print_help() {
    println!("Usage: recap [options]");
    println!();
    println!("Options:");
    println!("  (no args)              Show yesterday's activity (default)");
    println!("  --yesterday            Show yesterday's activity");
    println!("  --today                Show today's activity");
    println!("  --date <YYYY-MM-DD>    Show activity for a specific date");
    println!("  --timeline             Hour-by-hour timeline for yesterday");
    println!("  --timeline --today     Hour-by-hour timeline for today");
    println!("  --timeline --date X    Hour-by-hour timeline for a specific date");
    println!("  --stats                Show this week's stats");
    println!("  --week <n>             Show stats for n weeks ago");
    println!("  --streak               Show current and longest activity streak");
    println!("  --search <query>       Search command history");
    println!("  --project <name>       Filter by project");
    println!("  --export [json|csv]    Export all events (default: json)");
    println!("  --prune [days]         Delete events older than N days (default: 90)");
    println!("  --import               Import from shell history (auto-detects zsh/bash/fish)");
    println!("  --install              Auto-setup shell integration (zsh, bash, fish)");
    println!("  --uninstall            Remove shell integration");
    println!("  --version, -V          Show version");
    println!("  --help, -h             Show this help");
}

pub fn print_first_run_message() {
    println!("  {}", "Welcome to recap!".bold());
    println!();
    println!("  To get started, run:");
    println!("    {}", "recap --install".cyan());
    println!();
    println!("  Then import your existing shell history:");
    println!("    {}", "recap --import".cyan());
    println!();
    println!("  recap will silently record your terminal activity.");
    println!("  Run \"recap\" tomorrow morning to see your first summary.");
}

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

    #[test]
    fn compact_deduplicates_with_count() {
        let cmds = vec!["git".into(), "git".into(), "cargo".into()];
        let out = compact(&cmds);
        assert!(out.contains("git ×2"), "got: {}", out);
        assert!(out.contains("cargo"), "got: {}", out);
    }

    #[test]
    fn compact_single_item() {
        let cmds = vec!["cargo".into()];
        assert_eq!(compact(&cmds), "cargo");
    }

    #[test]
    fn fmt_duration_seconds() {
        assert_eq!(fmt_duration(45), "45s");
    }

    #[test]
    fn fmt_duration_minutes() {
        assert_eq!(fmt_duration(90), "1m");
    }

    #[test]
    fn fmt_duration_hours() {
        assert_eq!(fmt_duration(3661), "1h01");
    }
}
