use crate::{leveltracker, AppState};
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::time::Duration;
use tauri::{AppHandle, Emitter, Manager};

/// Parsed character info from an "is now level" line.
struct CharInfo {
    character: String,
    class: String,
    level: u32,
}

fn parse_level_line(line: &str) -> Option<CharInfo> {
    if !line.contains("is now level") {
        return None;
    }
    // Take everything after the last ':' (the " : <msg>" separator; timestamps
    // contain ':' too, but the message itself has none after it).
    let after = line.rsplit_once(':').map(|(_, b)| b).unwrap_or(line).trim();
    // after looks like: "CharName (Class) is now level 12"
    let level: u32 = after
        .chars()
        .rev()
        .take_while(|c| c.is_ascii_digit() || *c == ' ')
        .collect::<String>()
        .chars()
        .rev()
        .collect::<String>()
        .trim()
        .parse()
        .ok()?;
    let open = after.find('(');
    let close = after.find(')');
    let (character, class) = match (open, close) {
        (Some(o), Some(c)) if c > o => (
            after[..o].trim().to_string(),
            after[o + 1..c].trim().to_string(),
        ),
        _ => (after.split_whitespace().next().unwrap_or("").to_string(), String::new()),
    };
    Some(CharInfo {
        character,
        class,
        level,
    })
}

/// Parsed area info from a "Generating level" line.
struct AreaInfo {
    id: String,
    level: u32,
    seed: String,
}

fn parse_area_line(line: &str) -> Option<AreaInfo> {
    let gen = line.find("Generating level ")?;
    let rest = &line[gen + "Generating level ".len()..];
    // rest: "12 area \"g1_5\" with seed 1234567"
    let level: u32 = rest
        .chars()
        .take_while(|c| c.is_ascii_digit())
        .collect::<String>()
        .parse()
        .ok()?;
    let area_kw = rest.find("area \"")?;
    let after = &rest[area_kw + "area \"".len()..];
    let id_end = after.find('"')?;
    // The game capitalizes the area-id prefix (e.g. "G1_6") while the bundled
    // guide/area data uses lowercase ids, so normalize for matching.
    let mut id = after[..id_end].to_lowercase();
    // known bugged PoE2 area ids carry a trailing underscore
    if id == "c_g2_9_2_" || id == "c_g3_16_" {
        id.pop();
    }
    let seed = rest
        .find("with seed ")
        .map(|p| rest[p + "with seed ".len()..].trim().to_string())
        .unwrap_or_default();
    Some(AreaInfo {
        id,
        level,
        seed,
    })
}

fn handle_line(app: &AppHandle, line: &str) {
    let state = app.state::<AppState>();

    if let Some(ci) = parse_level_line(line) {
        {
            let mut s = state.status.lock().unwrap();
            s.character = ci.character.clone();
            s.class = ci.class.clone();
            s.level = ci.level;
        }
        emit_status(app);
        // If a profile exists for the character we're playing, make it active so
        // its saved progress is tracked.
        if !ci.character.is_empty() {
            crate::activate_profile_if_exists(app, &ci.character);
        }
        return;
    }

    if let Some(ai) = parse_area_line(line) {
        let (area_name, act) = {
            let guide = state.guide.lock().unwrap();
            match guide.areas.get(&ai.id) {
                Some(a) => (a.name.clone(), a.group + 1),
                None => (String::new(), 0),
            }
        };
        {
            let mut s = state.status.lock().unwrap();
            s.area_id = ai.id.clone();
            s.area_name = area_name;
            s.area_level = ai.level;
            s.area_seed = ai.seed.clone();
            s.act = act;
        }
        emit_status(app);

        // Campaign timer: auto-start / advance acts / finish on area change.
        let timer_enabled = state.config.lock().unwrap().timer_enabled;
        let timer_changed = {
            let mut t = state.timer.lock().unwrap();
            t.on_area(&ai.id, act, timer_enabled, crate::now_string)
        };
        if timer_changed {
            crate::persist_timer(app);
            let t = state.timer.lock().unwrap().clone();
            let _ = app.emit("timer://update", t);
        }

        // Auto-advance the leveling guide when entering a step's target area.
        let new_step = {
            let guide = state.guide.lock().unwrap();
            let cur = state.config.lock().unwrap().current_step;
            leveltracker::step_for_area(&guide.steps, &ai.id, cur)
                .map(|i| (i + 1).min(guide.steps.len().saturating_sub(1)))
        };
        if let Some(ns) = new_step {
            let changed = {
                let mut cfg = state.config.lock().unwrap();
                if cfg.current_step != ns {
                    cfg.current_step = ns;
                    cfg.save();
                    true
                } else {
                    false
                }
            };
            if changed {
                let _ = app.emit("tracker://step", ns);
            }
        }
    }
}

fn emit_status(app: &AppHandle) {
    let state = app.state::<AppState>();
    let status = state.status.lock().unwrap().clone();
    let _ = app.emit("status://update", status);
}

/// Read the tail of the file once to establish the current character/area.
fn initial_scan(app: &AppHandle, file: &mut File) {
    let len = file.metadata().map(|m| m.len()).unwrap_or(0);
    let start = len.saturating_sub(512 * 1024);
    if file.seek(SeekFrom::Start(start)).is_err() {
        return;
    }
    let mut buf = String::new();
    if file.read_to_string(&mut buf).is_err() {
        // fall through; non-UTF8 bytes shouldn't happen for this log
    }
    let mut last_level: Option<String> = None;
    let mut last_area: Option<String> = None;
    for line in buf.lines() {
        if line.contains("is now level") {
            last_level = Some(line.to_string());
        }
        if line.contains("Generating level ") {
            last_area = Some(line.to_string());
        }
    }
    if let Some(l) = last_level {
        handle_line(app, &l);
    }
    if let Some(a) = last_area {
        handle_line(app, &a);
    }
    let _ = file.seek(SeekFrom::End(0));
}

/// Spawn the background thread that tails the configured log file and the
/// active-window poller that controls overlay visibility.
pub fn spawn(app: AppHandle) {
    std::thread::spawn(move || {
        let mut file: Option<File> = None;
        let mut pos: u64 = 0;
        let mut current_path: Option<String> = None;
        let mut leftover = String::new();
        let mut last_tick = std::time::Instant::now();
        let mut last_save = std::time::Instant::now();
        let mut reassert_ticks: u8 = 0;
        // AFK detection: poll the mouse position; if it hasn't moved for the
        // configured delay, the timer auto-pauses.
        let mut last_mouse: Option<(i32, i32)> = None;
        let mut last_move = std::time::Instant::now();
        let mut mouse_ticks: u8 = 0;

        loop {
            // (Re)open the file if the configured path changed or it appeared.
            let want_path = {
                let state = app.state::<AppState>();
                let cfg = state.config.lock().unwrap();
                cfg.log_path.clone()
            };

            if want_path != current_path {
                current_path = want_path.clone();
                file = None;
                leftover.clear();
                if let Some(p) = &want_path {
                    if let Ok(mut f) = File::open(p) {
                        initial_scan(&app, &mut f);
                        pos = f.metadata().map(|m| m.len()).unwrap_or(0);
                        file = Some(f);
                    }
                }
            }

            // Read any newly appended bytes.
            if let Some(f) = file.as_mut() {
                if let Ok(meta) = f.metadata() {
                    let len = meta.len();
                    if len < pos {
                        // file was truncated/rotated
                        pos = 0;
                        let _ = f.seek(SeekFrom::Start(0));
                        leftover.clear();
                    }
                    if len > pos {
                        if f.seek(SeekFrom::Start(pos)).is_ok() {
                            let mut buf = Vec::new();
                            if f.read_to_end(&mut buf).is_ok() {
                                pos = len;
                                leftover.push_str(&String::from_utf8_lossy(&buf));
                                // process complete lines
                                while let Some(nl) = leftover.find('\n') {
                                    let line: String =
                                        leftover.drain(..=nl).collect::<String>();
                                    let line = line.trim_end_matches(['\n', '\r']);
                                    if !line.is_empty() {
                                        handle_line(&app, line);
                                    }
                                }
                            }
                        }
                    }
                }
            }

            update_focus(&app);

            // Keep the overlay above a self-raising game. Clicking inside PoE2
            // (especially fullscreen / borderless under Wayland+XWayland) raises
            // the game window in the stack and buries the overlay; the WM only
            // honors our always-on-top at the moment it's set. Re-assert it
            // every ~600ms while the game is focused so the overlay pops back.
            reassert_ticks = reassert_ticks.wrapping_add(1);
            if reassert_ticks >= 2 {
                reassert_ticks = 0;
                if app.state::<AppState>().status.lock().unwrap().game_focused {
                    if let Some(w) = app.get_webview_window("overlay") {
                        // Toggling forces the WM to restack ABOVE without
                        // stealing focus from the game.
                        let _ = w.set_always_on_top(false);
                        let _ = w.set_always_on_top(true);
                    }
                }
            }

            // Poll the mouse roughly once a second to track activity.
            mouse_ticks = mouse_ticks.wrapping_add(1);
            if mouse_ticks >= 3 {
                mouse_ticks = 0;
                if let Some(p) = crate::poe::mouse_position() {
                    if Some(p) != last_mouse {
                        last_mouse = Some(p);
                        last_move = std::time::Instant::now();
                    }
                }
            }
            let afk = {
                let state = app.state::<AppState>();
                let focused = state.status.lock().unwrap().game_focused;
                let cfg = state.config.lock().unwrap();
                let idle = cfg.timer_afk_enabled
                    && last_move.elapsed().as_secs() >= cfg.timer_afk_seconds as u64;
                let unfocused = cfg.timer_pause_unfocused && !focused;
                idle || unfocused
            };

            tick_timer(&app, &mut last_tick, &mut last_save, afk);
            std::thread::sleep(Duration::from_millis(300));
        }
    });
}

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

    #[test]
    fn parses_area_generation() {
        // real PoE2 0.5 logs capitalize the prefix ("G1_5"); we normalize it.
        let line = "2026/05/29 08:26:16 789879 2caa22d2 [DEBUG Client 312] Generating level 12 area \"G1_5\" with seed 1839472";
        let a = parse_area_line(line).expect("should parse");
        assert_eq!(a.id, "g1_5");
        assert_eq!(a.level, 12);
        assert_eq!(a.seed, "1839472");
    }

    #[test]
    fn fixes_bugged_area_id() {
        let line = "Generating level 30 area \"c_g3_16_\" with seed 42";
        let a = parse_area_line(line).expect("should parse");
        assert_eq!(a.id, "c_g3_16");
    }

    #[test]
    fn parses_level_up() {
        let line = "2024/12/06 21:05:00 123 abc [INFO Client 19340] : MyChar (Sorceress) is now level 2";
        let c = parse_level_line(line).expect("should parse");
        assert_eq!(c.character, "MyChar");
        assert_eq!(c.class, "Sorceress");
        assert_eq!(c.level, 2);
    }

    #[test]
    fn ignores_unrelated_lines() {
        assert!(parse_area_line("just some text").is_none());
        assert!(parse_level_line("connected to instance").is_none());
    }
}

/// Accumulate elapsed time into the campaign timer and emit updates.
fn tick_timer(
    app: &AppHandle,
    last_tick: &mut std::time::Instant,
    last_save: &mut std::time::Instant,
    afk: bool,
) {
    let now = std::time::Instant::now();
    let delta = now.duration_since(*last_tick).as_secs_f64();
    *last_tick = now;

    let state = app.state::<AppState>();
    let (enabled, pause_in_town) = {
        let cfg = state.config.lock().unwrap();
        (cfg.timer_enabled, cfg.timer_pause_in_town)
    };
    if !enabled {
        return;
    }
    let area_id = state.status.lock().unwrap().area_id.clone();
    let changed = {
        let mut t = state.timer.lock().unwrap();
        t.tick(delta, &area_id, pause_in_town, afk)
    };
    if changed {
        let t = state.timer.lock().unwrap().clone();
        let _ = app.emit("timer://update", t);
        // Persist roughly every 15s as a crash backup.
        if now.duration_since(*last_save).as_secs() >= 15 {
            *last_save = now;
            crate::persist_timer(app);
        }
    }
}

/// Detect game focus and show/hide the overlay accordingly.
fn update_focus(app: &AppHandle) {
    let (match_str, only_when_focused) = {
        let state = app.state::<AppState>();
        let cfg = state.config.lock().unwrap();
        (cfg.poe_window_match.clone(), cfg.overlay_only_when_focused)
    };

    let title = crate::poe::active_window_title().unwrap_or_default();
    let focused = title.contains(&match_str);

    let changed = {
        let state = app.state::<AppState>();
        let mut s = state.status.lock().unwrap();
        let c = s.game_focused != focused;
        s.game_focused = focused;
        c
    };

    if changed {
        let _ = app.emit("focus://update", focused);
    }
    let _ = only_when_focused; // visibility is decided in the overlay frontend
}
