use chrono::{Datelike, NaiveDate, NaiveTime, Utc};
use chrono_tz::Tz;
use serde::Serialize;

#[derive(Debug, Serialize, Default)]
pub struct UsageData {
    pub gauges: Vec<UsageGauge>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub contributions: Option<ContributionInfo>,
}

#[derive(Debug, Serialize)]
pub struct UsageGauge {
    pub name: String,
    pub percent_used: u32,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub resets: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub resets_iso: Option<String>,
}

#[derive(Debug, Serialize, Default)]
pub struct ContributionInfo {
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub factors: Vec<ContributionFactor>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub subagents: Vec<SubagentUsage>,
}

#[derive(Debug, Serialize)]
pub struct ContributionFactor {
    pub description: String,
    pub percent: u32,
}

#[derive(Debug, Serialize)]
pub struct SubagentUsage {
    pub name: String,
    pub percent: u32,
}

pub struct ProcessedOutput {
    pub raw_screen: String,
    pub data: UsageData,
}

pub fn process_usage_screen(raw: &str) -> ProcessedOutput {
    let lines: Vec<&str> = raw.lines().collect();

    let start = lines
        .iter()
        .position(|l| {
            let t = l.trim();
            t.starts_with('─') && t.chars().filter(|&c| c == '─').count() >= 6
        })
        .unwrap_or(0);

    let end = lines
        .iter()
        .rposition(|l| !l.trim().is_empty())
        .map(|i| i + 1)
        .unwrap_or(lines.len());

    let clean_lines = &lines[start..end];
    let raw_screen = clean_lines.join("\n");

    let data = extract_usage_data(clean_lines);

    ProcessedOutput { raw_screen, data }
}

fn extract_usage_data(lines: &[&str]) -> UsageData {
    let mut data = UsageData::default();
    let mut i = 0;

    while i < lines.len() {
        let trimmed = lines[i].trim();

        if (trimmed.starts_with("Current session") || trimmed.starts_with("Current week"))
            && !trimmed.contains('%')
        {
            let name = trimmed.to_string();
            if let Some(gauge) = find_gauge(&lines[i..], &name) {
                data.gauges.push(gauge);
            }
        }

        if let Some(factor) = parse_contribution_factor(trimmed) {
            data.contributions
                .get_or_insert_with(ContributionInfo::default)
                .factors
                .push(factor);
        }

        if trimmed.starts_with("Subagents") {
            let subs = parse_subagent_table(&lines[i + 1..]);
            if !subs.is_empty() {
                data.contributions
                    .get_or_insert_with(ContributionInfo::default)
                    .subagents = subs;
            }
        }

        i += 1;
    }

    data
}

fn find_gauge(lines: &[&str], name: &str) -> Option<UsageGauge> {
    let mut percent = None;
    let mut resets_raw = None;

    for line in lines.iter().skip(1).take(4) {
        let t = line.trim();
        if let Some(pct) = extract_percent_used(t) {
            percent = Some(pct);
        }
        if t.starts_with("Resets ") {
            resets_raw = Some(t.trim_start_matches("Resets ").to_string());
        }
    }

    percent.map(|p| {
        let resets_iso = resets_raw.as_deref().and_then(parse_reset_to_iso);
        UsageGauge {
            name: name.to_string(),
            percent_used: p,
            resets: resets_raw,
            resets_iso,
        }
    })
}

/// Parse reset strings like:
///   "12:30pm (Europe/Vienna)"          → today at 12:30 in that tz
///   "May 12, 9am (Europe/Vienna)"      → May 12 at 09:00
///   "May 12, 9:30am (Europe/Vienna)"   → May 12 at 09:30
///   "Jun 1, 12pm (America/New_York)"   → Jun 1 at 12:00
///
/// Claude Code uses JS `Intl.DateTimeFormat` style output.
fn parse_reset_to_iso(s: &str) -> Option<String> {
    // Split off the timezone from parentheses
    let (datetime_part, tz_str) = {
        let open = s.rfind('(')?;
        let close = s.rfind(')')?;
        let tz = s[open + 1..close].trim();
        let dt = s[..open].trim();
        (dt, tz)
    };

    let tz: Tz = tz_str.parse().ok()?;
    let now = Utc::now().with_timezone(&tz);

    let (date, time_str) = if datetime_part.contains(',') {
        // "May 12, 9am" or "May 12, 9:30am"
        let comma_pos = datetime_part.find(',')?;
        let date_part = datetime_part[..comma_pos].trim();
        let time_part = datetime_part[comma_pos + 1..].trim();

        let date = parse_month_day(date_part, now.year())?;
        (date, time_part)
    } else {
        // "12:30pm" — today in the given timezone
        (now.date_naive(), datetime_part)
    };

    let time = parse_12h_time(time_str)?;
    let naive = date.and_time(time);
    let local = naive.and_local_timezone(tz).earliest()?;
    let utc = local.with_timezone(&Utc);

    Some(utc.to_rfc3339())
}

/// Parse "May 12", "Jun 1", "December 25", etc.
fn parse_month_day(s: &str, year: i32) -> Option<NaiveDate> {
    let parts: Vec<&str> = s.split_whitespace().collect();
    if parts.len() != 2 {
        return None;
    }
    let month = match parts[0].to_lowercase().as_str() {
        "jan" | "january" => 1,
        "feb" | "february" => 2,
        "mar" | "march" => 3,
        "apr" | "april" => 4,
        "may" => 5,
        "jun" | "june" => 6,
        "jul" | "july" => 7,
        "aug" | "august" => 8,
        "sep" | "september" => 9,
        "oct" | "october" => 10,
        "nov" | "november" => 11,
        "dec" | "december" => 12,
        _ => return None,
    };
    let day: u32 = parts[1].parse().ok()?;
    NaiveDate::from_ymd_opt(year, month, day)
}

/// Parse "9am", "12pm", "9:30am", "12:30pm"
fn parse_12h_time(s: &str) -> Option<NaiveTime> {
    let s = s.trim().to_lowercase();
    let is_pm = s.ends_with("pm");
    let is_am = s.ends_with("am");
    if !is_pm && !is_am {
        return None;
    }

    let num_part = &s[..s.len() - 2];

    let (hour, minute) = if let Some((h, m)) = num_part.split_once(':') {
        (h.parse::<u32>().ok()?, m.parse::<u32>().ok()?)
    } else {
        (num_part.parse::<u32>().ok()?, 0)
    };

    let hour_24 = match (hour, is_pm) {
        (12, true) => 12,
        (12, false) => 0,
        (h, true) => h + 12,
        (h, false) => h,
    };

    NaiveTime::from_hms_opt(hour_24, minute, 0)
}

fn extract_percent_used(line: &str) -> Option<u32> {
    let line = line.trim();
    if !line.ends_with("% used") {
        return None;
    }
    let before_pct = line.trim_end_matches("% used").trim();
    before_pct
        .rsplit_once(char::is_whitespace)
        .map(|(_, n)| n)
        .unwrap_or(before_pct)
        .parse()
        .ok()
}

fn parse_contribution_factor(line: &str) -> Option<ContributionFactor> {
    if !line.contains("% of your usage") {
        return None;
    }
    let pct_str = line.split('%').next()?;
    let percent: u32 = pct_str.trim().parse().ok()?;
    let description = line.to_string();
    Some(ContributionFactor {
        description,
        percent,
    })
}

fn parse_subagent_table(lines: &[&str]) -> Vec<SubagentUsage> {
    let mut subs = Vec::new();
    for line in lines {
        let t = line.trim();
        if t.is_empty() || t.starts_with('─') || t.contains("to day") || t.contains("to cancel") {
            break;
        }
        if let Some(pos) = t.rfind('%') {
            let num_start = t[..pos]
                .rfind(char::is_whitespace)
                .map(|i| i + 1)
                .unwrap_or(0);
            if let Ok(pct) = t[num_start..pos].parse::<u32>() {
                let name = t[..num_start].trim().to_string();
                if !name.is_empty() && !name.contains("% of") {
                    subs.push(SubagentUsage {
                        name,
                        percent: pct,
                    });
                }
            }
        }
    }
    subs
}

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

    #[test]
    fn parse_time_only() {
        let t = parse_12h_time("12:30pm").unwrap();
        assert_eq!(t, NaiveTime::from_hms_opt(12, 30, 0).unwrap());
    }

    #[test]
    fn parse_time_am() {
        let t = parse_12h_time("9am").unwrap();
        assert_eq!(t, NaiveTime::from_hms_opt(9, 0, 0).unwrap());
    }

    #[test]
    fn parse_time_12am() {
        let t = parse_12h_time("12am").unwrap();
        assert_eq!(t, NaiveTime::from_hms_opt(0, 0, 0).unwrap());
    }

    #[test]
    fn parse_time_with_minutes() {
        let t = parse_12h_time("9:30am").unwrap();
        assert_eq!(t, NaiveTime::from_hms_opt(9, 30, 0).unwrap());
    }

    #[test]
    fn parse_reset_time_only() {
        let iso = parse_reset_to_iso("12:30pm (Europe/Vienna)");
        assert!(iso.is_some());
        let iso = iso.unwrap();
        assert!(iso.contains("T"));
        // Should end in +00:00 (UTC via rfc3339)
        assert!(iso.ends_with("+00:00"));
    }

    #[test]
    fn parse_reset_date_and_time() {
        let iso = parse_reset_to_iso("May 12, 9am (Europe/Vienna)").unwrap();
        assert!(iso.contains("T07:00:00") || iso.contains("T08:00:00"));
        // CEST is UTC+2, CET is UTC+1 — depends on whether May 12 is summer time
    }

    #[test]
    fn parse_month_day_basic() {
        let d = parse_month_day("May 12", 2026).unwrap();
        assert_eq!(d, NaiveDate::from_ymd_opt(2026, 5, 12).unwrap());
    }
}
