use std::collections::HashMap;
use std::error::Error;

type Result<T> = std::result::Result<T, Box<dyn Error>>;

#[derive(Debug, Clone)]
pub(crate) struct TimeSeriesEntry {
    pub time: String,
    pub temperature: f64,
    pub icon_1h: Option<String>,
    pub icon_6h: Option<String>,
    pub icon_12h: Option<String>,
}

#[derive(Debug, Clone)]
pub struct CurrentWeather {
    pub timestamp: i64,
    pub temperature: f64,
    pub min_temperature: f64,
    pub max_temperature: f64,
    pub icon: u8,
}

#[derive(Debug, Clone, Copy)]
pub struct ForecastDay {
    pub min_temperature: f64,
    pub max_temperature: f64,
    pub icon: u8,
}

fn metno_to_infinitime(symbol: &str) -> u8 {
    let base = symbol
        .strip_suffix("_day")
        .or_else(|| symbol.strip_suffix("_night"))
        .or_else(|| symbol.strip_suffix("_polarday"))
        .unwrap_or(symbol);
    match base {
        "clearsky" => 0,
        "fair" => 1,
        "partlycloudy" => 1,
        "cloudy" => 2,
        "heavycloud" | "heavycloudy" => 3,
        "rainshowers" | "lightrainshowers" | "lightrain" => 4,
        "rain" | "heavyrain" | "heavyrainshowers" | "rainandthunder" => 5,
        "thunder"
        | "thundershowers"
        | "rainshowersandthunder"
        | "heavyrainshowersandthunder"
        | "lightrainshowersandthunder" => 6,
        "snow" | "heavysnow" | "snowshowers" | "lightsnow" | "lightsnowshowers"
        | "heavysnowshowers" | "sleet" | "lightsleet" | "heavysleet" | "sleetshowers"
        | "lightsleetshowers" | "heavysleetshowers" => 7,
        "fog" | "mist" => 8,
        _ => 2,
    }
}

fn parse_metno_time(s: &str) -> Result<(i64, (i32, u32, u32))> {
    let parse_err = || format!("invalid MET.NO timestamp: {s}");
    let year: i32 = s[0..4].parse().map_err(|_| parse_err())?;
    let month: u32 = s[5..7].parse().map_err(|_| parse_err())?;
    let day: u32 = s[8..10].parse().map_err(|_| parse_err())?;
    let hour: u32 = s[11..13].parse().map_err(|_| parse_err())?;
    let min: u32 = s[14..16].parse().map_err(|_| parse_err())?;
    let sec: u32 = s[17..19].parse().map_err(|_| parse_err())?;
    let ts = days_from_civil(year, month, day) as i64 * 86400
        + hour as i64 * 3600
        + min as i64 * 60
        + sec as i64;
    Ok((ts, (year, month, day)))
}

fn days_from_civil(y: i32, m: u32, d: u32) -> i32 {
    let y = if m <= 2 { y - 1 } else { y };
    let era = if y >= 0 { y } else { y - 399 } / 400;
    let yoe = y - era * 400;
    let m = if m <= 2 { m + 9 } else { m - 3 };
    let doy = (153 * m as i32 + 2) / 5 + d as i32 - 1;
    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
    era * 146097 + doe - 719468
}

fn civil_from_days(z: i32) -> (i32, u32, u32) {
    let z = z + 719468;
    let era = if z >= 0 { z } else { z - 146096 } / 146097;
    let doe = z - era * 146097;
    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
    let y = yoe + era * 400;
    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
    let mp = (5 * doy + 2) / 153;
    let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
    let m = if mp < 10 { mp + 3 } else { mp - 9 };
    let y = (if m <= 2 { y + 1 } else { y }) as u32;
    (y as i32, m as u32, d)
}

fn advance_date(date: (i32, u32, u32), days: i64) -> (i32, u32, u32) {
    let z = days_from_civil(date.0, date.1, date.2) + days as i32;
    civil_from_days(z)
}

pub(crate) fn process_current(entries: &[TimeSeriesEntry]) -> Result<CurrentWeather> {
    let first = entries.first().ok_or("empty timeseries")?;
    let (ts, today) = parse_metno_time(&first.time)?;
    let temperature = first.temperature;

    let mut min_temp = f64::MAX;
    let mut max_temp = f64::MIN;

    for entry in entries {
        let (_, date) = parse_metno_time(&entry.time)?;
        if date == today {
            let t = entry.temperature;
            if t < min_temp {
                min_temp = t;
            }
            if t > max_temp {
                max_temp = t;
            }
        }
    }

    if min_temp == f64::MAX {
        min_temp = temperature;
        max_temp = temperature;
    }

    let icon = first
        .icon_1h
        .as_deref()
        .or_else(|| first.icon_6h.as_deref())
        .or_else(|| first.icon_12h.as_deref())
        .map(metno_to_infinitime)
        .unwrap_or(2);

    Ok(CurrentWeather {
        timestamp: ts,
        temperature,
        min_temperature: min_temp,
        max_temperature: max_temp,
        icon,
    })
}

pub(crate) fn process_forecast(entries: &[TimeSeriesEntry]) -> Result<[ForecastDay; 5]> {
    if entries.is_empty() {
        return Ok([ForecastDay {
            min_temperature: 0.0,
            max_temperature: 0.0,
            icon: 0,
        }; 5]);
    }

    let (_, base_date) = parse_metno_time(&entries[0].time)?;

    let mut day_data: HashMap<(i32, u32, u32), (f64, f64, Vec<u8>)> = HashMap::new();

    for entry in entries {
        let (_, date) = parse_metno_time(&entry.time)?;

        let day_num = days_between(base_date, date);
        if day_num < 1 || day_num > 5 {
            continue;
        }

        let t = entry.temperature;
        let entry_icon = entry
            .icon_12h
            .as_deref()
            .or_else(|| entry.icon_6h.as_deref())
            .or_else(|| entry.icon_1h.as_deref())
            .map(metno_to_infinitime)
            .unwrap_or(2);

        let data = day_data
            .entry(date)
            .or_insert_with(|| (f64::MAX, f64::MIN, Vec::new()));
        if t < data.0 {
            data.0 = t;
        }
        if t > data.1 {
            data.1 = t;
        }
        data.2.push(entry_icon);
    }

    let mut forecast = [ForecastDay {
        min_temperature: 0.0,
        max_temperature: 0.0,
        icon: 0,
    }; 5];
    for day_offset in 1..6 {
        let target = advance_date(base_date, day_offset);
        if let Some((min_t, max_t, icons)) = day_data.get(&target) {
            let icon = most_common(icons).unwrap_or(2);
            forecast[(day_offset - 1) as usize] = ForecastDay {
                min_temperature: *min_t,
                max_temperature: *max_t,
                icon,
            };
        }
    }

    Ok(forecast)
}

fn days_between(a: (i32, u32, u32), b: (i32, u32, u32)) -> i64 {
    (days_from_civil(b.0, b.1, b.2) - days_from_civil(a.0, a.1, a.2)) as i64
}

fn most_common(items: &[u8]) -> Option<u8> {
    let counts = items.iter().fold(HashMap::new(), |mut acc, &item| {
        *acc.entry(item).or_insert(0) += 1;
        acc
    });
    counts
        .into_iter()
        .max_by_key(|&(_, count)| count)
        .map(|(item, _)| item)
}

pub struct MetNoClient {}

impl MetNoClient {
    pub fn new() -> Self {
        Self {}
    }

    pub(crate) fn fetch(&self, lat: f64, lon: f64) -> Result<Vec<TimeSeriesEntry>> {
        let url = format!(
            "https://api.met.no/weatherapi/locationforecast/2.0/compact?lat={}&lon={}",
            lat, lon
        );

        let body = ureq::get(&url)
            .set("User-Agent", "InfiniTimeWeather/0.1")
            .call()
            .map_err(|e| format!("MET.NO request failed: {e}"))?
            .into_string()
            .map_err(|e| format!("failed to read response body: {e}"))?;

        parse_timeseries(&body)
    }
}

fn parse_timeseries(body: &str) -> Result<Vec<TimeSeriesEntry>> {
    let mut i = 0;
    let val = parse_value(body, &mut i)?;
    extract_timeseries(val)
}

fn extract_timeseries(val: Val) -> Result<Vec<TimeSeriesEntry>> {
    let fields = obj_val(val)?;
    let mut entries = Vec::new();
    for (k, v) in fields {
        if k == "properties" {
            let props = obj_val(v)?;
            for (pk, pv) in props {
                if pk == "timeseries" {
                    let arr = arr_val(pv)?;
                    for item in arr {
                        entries.push(extract_entry(item)?);
                    }
                }
            }
        }
    }
    Ok(entries)
}

fn extract_entry(val: Val) -> Result<TimeSeriesEntry> {
    let fields = obj_val(val)?;
    let mut time = String::new();
    let mut temperature = f64::NAN;
    let mut icon_1h = None;
    let mut icon_6h = None;
    let mut icon_12h = None;

    for (k, v) in fields {
        match k.as_str() {
            "time" => time = str_val(v),
            "data" => {
                if let Ok(data_fields) = obj_val(v) {
                    for (dk, dv) in data_fields {
                        match dk.as_str() {
                            "instant" => {
                                if let Ok(inst) = obj_val(dv) {
                                    for (ik, iv) in inst {
                                        if ik == "details" {
                                            if let Ok(det) = obj_val(iv) {
                                                for (detk, detv) in det {
                                                    if detk == "air_temperature" {
                                                        temperature = num_val(detv);
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                            "next_1_hours" => icon_1h = extract_symbol(dv),
                            "next_6_hours" => icon_6h = extract_symbol(dv),
                            "next_12_hours" => icon_12h = extract_symbol(dv),
                            _ => {}
                        }
                    }
                }
            }
            _ => {}
        }
    }

    Ok(TimeSeriesEntry {
        time,
        temperature,
        icon_1h,
        icon_6h,
        icon_12h,
    })
}

fn extract_symbol(v: Val) -> Option<String> {
    let fields = obj_val(v).ok()?;
    for (k, v) in fields {
        if k == "summary" {
            let sf = obj_val(v).ok()?;
            for (sk, sv) in sf {
                if sk == "symbol_code" {
                    return Some(str_val(sv));
                }
            }
        }
    }
    None
}

fn str_val(v: Val) -> String {
    match v {
        Val::Str(s) => s,
        _ => String::new(),
    }
}

fn num_val(v: Val) -> f64 {
    match v {
        Val::Num(n) => n,
        _ => f64::NAN,
    }
}

fn obj_val(v: Val) -> Result<Vec<(String, Val)>> {
    match v {
        Val::Obj(f) => Ok(f),
        _ => Err("expected object".into()),
    }
}

fn arr_val(v: Val) -> Result<Vec<Val>> {
    match v {
        Val::Arr(a) => Ok(a),
        _ => Err("expected array".into()),
    }
}

enum Val {
    Str(String),
    Num(f64),
    Arr(Vec<Val>),
    Obj(Vec<(String, Val)>),
    Null,
}

fn skip_ws(s: &str, i: &mut usize) {
    let bytes = s.as_bytes();
    while *i < bytes.len() && bytes[*i].is_ascii_whitespace() {
        *i += 1;
    }
}

fn expect(s: &str, i: &mut usize, c: u8) -> Result<()> {
    skip_ws(s, i);
    if s.as_bytes().get(*i) != Some(&c) {
        return Err(format!("expected '{}' at position {}", c as char, *i).into());
    }
    *i += 1;
    Ok(())
}

fn parse_string(s: &str, i: &mut usize) -> Result<String> {
    skip_ws(s, i);
    let bytes = s.as_bytes();
    if bytes.get(*i) != Some(&b'"') {
        return Err(format!("expected string at position {}", *i).into());
    }
    *i += 1;
    let mut result = String::new();
    while *i < bytes.len() {
        match bytes[*i] {
            b'"' => {
                *i += 1;
                return Ok(result);
            }
            b'\\' => {
                *i += 1;
                if *i >= bytes.len() {
                    return Err("unterminated string escape".into());
                }
                match bytes[*i] {
                    b'"' => result.push('"'),
                    b'\\' => result.push('\\'),
                    b'/' => result.push('/'),
                    b'n' => result.push('\n'),
                    b'r' => result.push('\r'),
                    b't' => result.push('\t'),
                    b'u' => {
                        if *i + 4 >= bytes.len() {
                            return Err("truncated unicode escape".into());
                        }
                        let hex = &s[*i + 1..*i + 5];
                        let code =
                            u32::from_str_radix(hex, 16).map_err(|_| "invalid unicode escape")?;
                        if let Some(c) = char::from_u32(code) {
                            result.push(c);
                        }
                        *i += 4;
                    }
                    _ => return Err(format!("invalid escape: \\{}", bytes[*i] as char).into()),
                }
                *i += 1;
            }
            _ => {
                result.push(bytes[*i] as char);
                *i += 1;
            }
        }
    }
    Err("unterminated string".into())
}

fn parse_number(s: &str, i: &mut usize) -> Result<f64> {
    skip_ws(s, i);
    let bytes = s.as_bytes();
    let start = *i;
    if *i < bytes.len() && bytes[*i] == b'-' {
        *i += 1;
    }
    while *i < bytes.len() && bytes[*i].is_ascii_digit() {
        *i += 1;
    }
    if *i < bytes.len() && bytes[*i] == b'.' {
        *i += 1;
        while *i < bytes.len() && bytes[*i].is_ascii_digit() {
            *i += 1;
        }
    }
    if *i < bytes.len() && (bytes[*i] == b'e' || bytes[*i] == b'E') {
        *i += 1;
        if *i < bytes.len() && (bytes[*i] == b'+' || bytes[*i] == b'-') {
            *i += 1;
        }
        while *i < bytes.len() && bytes[*i].is_ascii_digit() {
            *i += 1;
        }
    }
    s[start..*i]
        .parse::<f64>()
        .map_err(|_| format!("invalid number at position {}", start).into())
}

fn parse_value(s: &str, i: &mut usize) -> Result<Val> {
    skip_ws(s, i);
    let bytes = s.as_bytes();
    match bytes.get(*i) {
        Some(b'"') => parse_string(s, i).map(Val::Str),
        Some(b'{') => parse_object(s, i),
        Some(b'[') => parse_array(s, i),
        Some(b'n') => {
            if s[*i..].starts_with("null") {
                *i += 4;
                Ok(Val::Null)
            } else {
                Err(format!("unexpected token at position {}", *i).into())
            }
        }
        Some(b't') => {
            if s[*i..].starts_with("true") {
                *i += 4;
                Err("unexpected boolean in JSON".into())
            } else {
                Err(format!("unexpected token at position {}", *i).into())
            }
        }
        Some(b'f') => {
            if s[*i..].starts_with("false") {
                *i += 5;
                Err("unexpected boolean in JSON".into())
            } else {
                Err(format!("unexpected token at position {}", *i).into())
            }
        }
        Some(_) => parse_number(s, i).map(Val::Num),
        None => Err("unexpected end of input".into()),
    }
}

fn parse_object(s: &str, i: &mut usize) -> Result<Val> {
    skip_ws(s, i);
    expect(s, i, b'{')?;
    let mut fields = Vec::new();
    loop {
        skip_ws(s, i);
        if s.as_bytes().get(*i) == Some(&b'}') {
            *i += 1;
            return Ok(Val::Obj(fields));
        }
        let key = parse_string(s, i)?;
        expect(s, i, b':')?;
        let value = parse_value(s, i)?;
        fields.push((key, value));
        skip_ws(s, i);
        if s.as_bytes().get(*i) == Some(&b',') {
            *i += 1;
        }
    }
}

fn parse_array(s: &str, i: &mut usize) -> Result<Val> {
    skip_ws(s, i);
    expect(s, i, b'[')?;
    let mut items = Vec::new();
    loop {
        skip_ws(s, i);
        if s.as_bytes().get(*i) == Some(&b']') {
            *i += 1;
            return Ok(Val::Arr(items));
        }
        items.push(parse_value(s, i)?);
        skip_ws(s, i);
        if s.as_bytes().get(*i) == Some(&b',') {
            *i += 1;
        }
    }
}