use anyhow::Result;
use chrono::{DateTime, Local, TimeZone, Utc};
use tmkpr_lib::models::entry::{Entry, EntryFilter};
use tmkpr_lib::nlp::{parse_datetime_now, TimeFormat};
use tmkpr_lib::service::{EntryService, ProjectService, TaskService};
use tmkpr_lib::storage::Storage;

use crate::cli::ListArgs;
use crate::output;

pub fn run(
    args: ListArgs,
    storage: &dyn Storage,
    user_id: &str,
    date_fmt: &str,
    time_fmt: TimeFormat,
    format: &str,
    color: bool,
) -> Result<()> {
    let from = args
        .from
        .as_deref()
        .map(|s| parse_datetime_now(s, time_fmt))
        .transpose()?;
    let until = args
        .until
        .as_deref()
        .map(|s| parse_datetime_now(s, time_fmt))
        .transpose()?;

    let project_id = match args.project.as_deref() {
        Some(input) => Some(ProjectService::new(storage, user_id).resolve(input)?.id),
        None => None,
    };

    let task_id = match (args.task.as_deref(), &project_id) {
        (Some(input), Some(pid)) => {
            Some(TaskService::new(storage, user_id).resolve(pid, input)?.id)
        }
        _ => None,
    };

    if args.gaps {
        let explicit_from = from.is_some();
        let window_start = from.unwrap_or_else(|| {
            let today = Local::now().date_naive();
            Local
                .from_local_datetime(&today.and_hms_opt(0, 0, 0).unwrap())
                .single()
                .unwrap()
                .with_timezone(&Utc)
        });
        let window_end = until.unwrap_or_else(Utc::now);

        let entries = EntryService::new(storage, user_id).list(EntryFilter {
            user_id: user_id.to_string(),
            project_id,
            task_id,
            from: Some(window_start),
            until: Some(window_end),
            tags: args.tag,
            include_active: true,
            limit: None,
        })?;

        let min_gap_secs = i64::from(args.min_gap) * 60;
        let mut gaps = compute_gaps(&entries, window_start, window_end);
        gaps.retain(|(s, e)| (*e - *s).num_seconds() > min_gap_secs);
        if !explicit_from {
            if let Some(first) = gaps.first() {
                if first.0 == window_start {
                    gaps.remove(0);
                }
            }
        }
        output::print_gaps_table(&gaps, date_fmt, color);
        return Ok(());
    }

    let from = apply_today_default(from, until);

    let filter = EntryFilter {
        user_id: user_id.to_string(),
        project_id,
        task_id,
        from,
        until,
        tags: args.tag,
        limit: args.limit,
        include_active: args.active,
    };

    let entries = EntryService::new(storage, user_id).list(filter)?;

    if entries.is_empty() {
        match format {
            "json" => println!("[]"),
            "csv" => println!("id,project,task,note,tags,started,finished,duration_secs"),
            _ => println!("{}", empty_entries_msg(from, until, date_fmt)),
        }
        return Ok(());
    }

    let (projects, tasks) = output::build_indexes(storage, user_id);

    output::print_entries(&entries, &projects, &tasks, date_fmt, format, color);
    Ok(())
}

fn empty_entries_msg(
    from: Option<DateTime<Utc>>,
    until: Option<DateTime<Utc>>,
    date_fmt: &str,
) -> String {
    match (from, until) {
        (Some(f), Some(u)) => format!(
            "No entries from {} to {}.",
            f.with_timezone(&Local).format(date_fmt),
            u.with_timezone(&Local).format(date_fmt),
        ),
        (Some(f), None) => format!(
            "No entries from {}.",
            f.with_timezone(&Local).format(date_fmt),
        ),
        (None, Some(u)) => format!(
            "No entries until {}.",
            u.with_timezone(&Local).format(date_fmt),
        ),
        (None, None) => "No entries found.".to_string(),
    }
}

/// Defaults `from` to today's local midnight (UTC) when neither bound is set.
fn apply_today_default(
    from: Option<DateTime<Utc>>,
    until: Option<DateTime<Utc>>,
) -> Option<DateTime<Utc>> {
    from.or_else(|| {
        if until.is_none() {
            let today = Local::now().date_naive();
            Local
                .from_local_datetime(&today.and_hms_opt(0, 0, 0).unwrap())
                .single()
                .map(|dt| dt.with_timezone(&Utc))
        } else {
            None
        }
    })
}

/// Returns the untracked gaps within [window_start, window_end].
fn compute_gaps(
    entries: &[Entry],
    window_start: DateTime<Utc>,
    window_end: DateTime<Utc>,
) -> Vec<(DateTime<Utc>, DateTime<Utc>)> {
    let now = Utc::now();

    // Build sorted (start, end) intervals; active entries use now as their end.
    let mut intervals: Vec<(DateTime<Utc>, DateTime<Utc>)> = entries
        .iter()
        .map(|e| (e.started_at, e.finished_at.unwrap_or(now)))
        .collect();
    intervals.sort_by_key(|(s, _)| *s);

    // Merge overlapping / adjacent intervals.
    let mut merged: Vec<(DateTime<Utc>, DateTime<Utc>)> = Vec::new();
    for (start, end) in intervals {
        match merged.last_mut() {
            Some((_, prev_end)) if start <= *prev_end => {
                *prev_end = (*prev_end).max(end);
            }
            _ => merged.push((start, end)),
        }
    }

    // Find gaps between merged intervals and the window boundaries.
    let mut gaps = Vec::new();
    let mut cursor = window_start;

    for (start, end) in &merged {
        let start = (*start).max(window_start);
        if start > cursor {
            gaps.push((cursor, start));
        }
        cursor = cursor.max(*end);
    }

    if cursor < window_end {
        gaps.push((cursor, window_end));
    }

    gaps
}

#[cfg(test)]
mod tests {
    use super::*;
    use chrono::{Duration, TimeZone};
    use tmkpr_lib::models::entry::Entry;

    fn make_entry(started_at: DateTime<Utc>, finished_at: Option<DateTime<Utc>>) -> Entry {
        Entry {
            id: "id".to_string(),
            user_id: "u1".to_string(),
            project_id: None,
            task_id: None,
            note: None,
            started_at,
            finished_at,
            tags: vec![],
            created_at: started_at,
            updated_at: started_at,
        }
    }

    fn t(h: u32, m: u32) -> DateTime<Utc> {
        Utc.with_ymd_and_hms(2024, 1, 1, h, m, 0).unwrap()
    }

    #[test]
    fn no_args_defaults_to_today_midnight() {
        let from = apply_today_default(None, None);
        assert!(from.is_some());
        let today = Local::now().date_naive();
        let expected = Local
            .from_local_datetime(&today.and_hms_opt(0, 0, 0).unwrap())
            .single()
            .unwrap()
            .with_timezone(&Utc);
        assert_eq!(from.unwrap(), expected);
    }

    #[test]
    fn explicit_from_is_preserved() {
        let explicit = Utc::now() - Duration::days(7);
        let from = apply_today_default(Some(explicit), None);
        assert_eq!(from.unwrap(), explicit);
    }

    #[test]
    fn until_without_from_applies_no_default() {
        let until = Utc::now();
        let from = apply_today_default(None, Some(until));
        assert!(from.is_none());
    }

    // ── compute_gaps ─────────────────────────────────────────────────────────

    #[test]
    fn gaps_no_entries_returns_full_window() {
        let gaps = compute_gaps(&[], t(9, 0), t(17, 0));
        assert_eq!(gaps, vec![(t(9, 0), t(17, 0))]);
    }

    #[test]
    fn gaps_entry_fills_window_exactly() {
        let e = make_entry(t(9, 0), Some(t(17, 0)));
        let gaps = compute_gaps(&[e], t(9, 0), t(17, 0));
        assert!(gaps.is_empty());
    }

    #[test]
    fn gaps_entry_in_middle() {
        let e = make_entry(t(10, 0), Some(t(11, 0)));
        let gaps = compute_gaps(&[e], t(9, 0), t(17, 0));
        assert_eq!(gaps, vec![(t(9, 0), t(10, 0)), (t(11, 0), t(17, 0))]);
    }

    #[test]
    fn gaps_overlapping_entries_merged() {
        let e1 = make_entry(t(9, 0), Some(t(11, 0)));
        let e2 = make_entry(t(10, 0), Some(t(12, 0)));
        let gaps = compute_gaps(&[e1, e2], t(9, 0), t(17, 0));
        assert_eq!(gaps, vec![(t(12, 0), t(17, 0))]);
    }

    #[test]
    fn gaps_adjacent_entries_no_gap_between_them() {
        let e1 = make_entry(t(9, 0), Some(t(10, 0)));
        let e2 = make_entry(t(10, 0), Some(t(11, 0)));
        let gaps = compute_gaps(&[e1, e2], t(9, 0), t(17, 0));
        assert_eq!(gaps, vec![(t(11, 0), t(17, 0))]);
    }

    #[test]
    fn gaps_entry_starts_before_window() {
        let e = make_entry(t(8, 0), Some(t(10, 0)));
        let gaps = compute_gaps(&[e], t(9, 0), t(17, 0));
        assert_eq!(gaps, vec![(t(10, 0), t(17, 0))]);
    }

    #[test]
    fn gaps_entry_ends_after_window() {
        let e = make_entry(t(9, 0), Some(t(20, 0)));
        let gaps = compute_gaps(&[e], t(9, 0), t(17, 0));
        assert!(gaps.is_empty());
    }

    #[test]
    fn gaps_multiple_non_overlapping_entries() {
        let e1 = make_entry(t(9, 0), Some(t(10, 0)));
        let e2 = make_entry(t(11, 0), Some(t(12, 0)));
        let e3 = make_entry(t(14, 0), Some(t(15, 0)));
        let gaps = compute_gaps(&[e1, e2, e3], t(9, 0), t(17, 0));
        assert_eq!(
            gaps,
            vec![
                (t(10, 0), t(11, 0)),
                (t(12, 0), t(14, 0)),
                (t(15, 0), t(17, 0)),
            ]
        );
    }

    #[test]
    fn gaps_unsorted_entries_handled() {
        let e1 = make_entry(t(11, 0), Some(t(12, 0)));
        let e2 = make_entry(t(9, 0), Some(t(10, 0)));
        let gaps = compute_gaps(&[e1, e2], t(9, 0), t(17, 0));
        assert_eq!(gaps, vec![(t(10, 0), t(11, 0)), (t(12, 0), t(17, 0))]);
    }

    // ── Gap 7: min_gap filter + strip-leading-gap ─────────────────────────────

    #[test]
    fn min_gap_filters_short_gaps() {
        // Gap 1: 30s (t(10,0)..t(10,0)+30s), Gap 2: 5min (t(11,0)..t(11,5))
        let short_end = t(10, 0) + Duration::seconds(30);
        let e1 = make_entry(t(9, 0), Some(t(10, 0)));
        let e2 = make_entry(short_end, Some(t(11, 0)));
        let e3 = make_entry(t(11, 5), Some(t(12, 0)));
        let gaps = compute_gaps(&[e1, e2, e3], t(9, 0), t(12, 0));
        // Two gaps before filter: (10:00..10:00:30) = 30s, (11:00..11:05) = 300s
        assert_eq!(gaps.len(), 2);

        let min_gap_secs: i64 = 60;
        let filtered: Vec<_> = gaps
            .into_iter()
            .filter(|(s, e)| (*e - *s).num_seconds() > min_gap_secs)
            .collect();
        assert_eq!(filtered.len(), 1);
        assert_eq!(filtered[0], (t(11, 0), t(11, 5)));
    }

    #[test]
    fn strip_leading_gap_removed_when_starts_at_window() {
        // Window starts at 9:00. First entry starts at 9:30 — gap at window open.
        let e1 = make_entry(t(9, 30), Some(t(10, 0)));
        let gaps = compute_gaps(&[e1], t(9, 0), t(17, 0));
        // Raw gaps: (9:00..9:30) at window start, (10:00..17:00) after entry
        assert_eq!(gaps.len(), 2);
        assert_eq!(gaps[0].0, t(9, 0));

        // Strip the leading gap (simulate !explicit_from logic from run())
        let mut gaps = gaps;
        if let Some(first) = gaps.first() {
            if first.0 == t(9, 0) {
                gaps.remove(0);
            }
        }
        assert_eq!(gaps.len(), 1);
        assert_eq!(gaps[0], (t(10, 0), t(17, 0)));
    }
}
