use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use tauri::{
    App, AppHandle, LogicalPosition, LogicalSize, Manager, Position, RunEvent, Size, WebviewWindow,
    WindowEvent,
};

pub(crate) const MAIN_WINDOW_LABEL: &str = "main";
const WINDOW_STATE_FILE: &str = "window-state.json";
const MIN_WINDOW_WIDTH: u32 = 480;
const MIN_WINDOW_HEIGHT: u32 = 400;

#[derive(Debug, Default)]
pub(crate) struct MainWindowFrameState(Mutex<Option<WindowFrame>>);

#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
struct WindowFrame {
    x: i32,
    y: i32,
    width: u32,
    height: u32,
}

#[derive(Debug, Clone, Copy, Eq, PartialEq)]
struct ScreenArea {
    x: i32,
    y: i32,
    width: u32,
    height: u32,
}

#[derive(Debug, Default, Serialize, Deserialize)]
struct PersistedWindowState {
    main: Option<WindowFrame>,
    #[serde(default)]
    coordinate_space: WindowFrameCoordinateSpace,
}

#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
enum WindowFrameCoordinateSpace {
    #[default]
    Physical,
    Logical,
}

pub(crate) fn restore_main_window_state(app: &mut App) {
    let Some(window) = app.get_webview_window(MAIN_WINDOW_LABEL) else {
        return;
    };
    restore_main_window_frame(app.handle(), &window, "during setup");
}

pub(crate) fn handle_run_event(app_handle: &AppHandle, event: &RunEvent) {
    match event {
        event if restores_window_frame_after_runtime_ready(event) => {
            restore_main_window_state_from_handle(app_handle)
        }
        RunEvent::WindowEvent {
            label,
            event:
                WindowEvent::Moved(_) | WindowEvent::Resized(_) | WindowEvent::ScaleFactorChanged { .. },
            ..
        } if label == MAIN_WINDOW_LABEL => cache_current_normal_frame(app_handle),
        RunEvent::WindowEvent { label, event, .. }
            if label == MAIN_WINDOW_LABEL && saves_live_main_window_frame_on_event(event) =>
        {
            save_main_window_frame(app_handle)
        }
        RunEvent::WindowEvent {
            label,
            event: WindowEvent::Destroyed,
            ..
        } if label == MAIN_WINDOW_LABEL => save_cached_main_window_frame(app_handle),
        RunEvent::Exit => save_cached_main_window_frame(app_handle),
        _ => {}
    }
}

fn restores_window_frame_after_runtime_ready(event: &RunEvent) -> bool {
    matches!(event, RunEvent::Ready)
}

fn saves_live_main_window_frame_on_event(event: &WindowEvent) -> bool {
    matches!(event, WindowEvent::CloseRequested { .. })
}

fn restore_main_window_state_from_handle(app_handle: &AppHandle) {
    let Some(window) = app_handle.get_webview_window(MAIN_WINDOW_LABEL) else {
        return;
    };
    restore_main_window_frame(app_handle, &window, "after runtime ready");
}

fn restore_main_window_frame(app_handle: &AppHandle, window: &WebviewWindow, phase: &str) {
    let Some(frame) = read_main_window_frame(window_scale_factor(window)) else {
        return;
    };
    let areas = current_screen_areas(window);
    let Some(restored_frame) = fit_frame_to_screens(frame, &areas) else {
        return;
    };

    if let Err(err) = apply_window_frame(window, restored_frame) {
        log::warn!("Failed to restore main window state {phase}: {err}");
        return;
    }

    cache_frame(app_handle, restored_frame);
}

fn cache_current_normal_frame(app_handle: &AppHandle) {
    if let Some(frame) = current_normal_main_window_frame(app_handle) {
        cache_frame(app_handle, frame);
    }
}

fn save_main_window_frame(app_handle: &AppHandle) {
    let frame = current_normal_main_window_frame(app_handle).or_else(|| cached_frame(app_handle));
    write_main_window_frame_if_available(frame);
}

fn save_cached_main_window_frame(app_handle: &AppHandle) {
    write_main_window_frame_if_available(cached_frame(app_handle));
}

fn write_main_window_frame_if_available(frame: Option<WindowFrame>) {
    if let Some(frame) = frame {
        if let Err(err) = write_main_window_frame(frame) {
            log::warn!("Failed to save main window state: {err}");
        }
    }
}

fn current_normal_main_window_frame(app_handle: &AppHandle) -> Option<WindowFrame> {
    let window = app_handle.get_webview_window(MAIN_WINDOW_LABEL)?;
    if !is_normal_window(&window) {
        return None;
    }
    read_window_frame(&window).filter(is_valid_saved_frame)
}

fn is_normal_window(window: &WebviewWindow) -> bool {
    let is_fullscreen = window.is_fullscreen().unwrap_or(false);
    let is_maximized = window.is_maximized().unwrap_or(false);
    let is_minimized = window.is_minimized().unwrap_or(false);
    !is_fullscreen && !is_maximized && !is_minimized
}

fn read_window_frame(window: &WebviewWindow) -> Option<WindowFrame> {
    let scale_factor = window_scale_factor(window);
    let position = window.outer_position().ok()?;
    let size = window.inner_size().ok()?;
    Some(WindowFrame::from_logical_geometry(
        position.to_logical::<f64>(scale_factor),
        size.to_logical::<f64>(scale_factor),
    ))
}

fn apply_window_frame(window: &WebviewWindow, frame: WindowFrame) -> tauri::Result<()> {
    window.set_size(Size::Logical(LogicalSize::new(
        frame.width as f64,
        frame.height as f64,
    )))?;
    window.set_position(Position::Logical(LogicalPosition::new(
        frame.x as f64,
        frame.y as f64,
    )))
}

fn current_screen_areas(window: &WebviewWindow) -> Vec<ScreenArea> {
    let scale_factor = window_scale_factor(window);
    window
        .available_monitors()
        .unwrap_or_default()
        .into_iter()
        .map(|monitor| {
            let area = monitor.work_area();
            let position = area.position.to_logical::<f64>(scale_factor);
            let size = area.size.to_logical::<f64>(scale_factor);
            ScreenArea {
                x: rounded_i32(position.x),
                y: rounded_i32(position.y),
                width: rounded_u32(size.width),
                height: rounded_u32(size.height),
            }
        })
        .filter(ScreenArea::has_area)
        .collect()
}

fn window_scale_factor(window: &WebviewWindow) -> f64 {
    window.scale_factor().unwrap_or(1.0).max(1.0)
}

fn fit_frame_to_screens(frame: WindowFrame, screens: &[ScreenArea]) -> Option<WindowFrame> {
    if frame_is_visible(frame, screens) {
        return Some(frame);
    }

    let screen = best_screen_for_frame(frame, screens)?;
    let width = clamp_dimension(frame.width, MIN_WINDOW_WIDTH, screen.width);
    let height = clamp_dimension(frame.height, MIN_WINDOW_HEIGHT, screen.height);
    Some(WindowFrame {
        x: clamp_axis(frame.x, width, screen.x, screen.width),
        y: clamp_axis(frame.y, height, screen.y, screen.height),
        width,
        height,
    })
}

fn frame_is_visible(frame: WindowFrame, screens: &[ScreenArea]) -> bool {
    frame_corners(frame)
        .into_iter()
        .all(|point| screens.iter().any(|screen| screen.contains(point)))
}

fn frame_corners(frame: WindowFrame) -> [(i32, i32); 4] {
    let right = frame.right() - 1;
    let bottom = frame.bottom() - 1;
    [
        (frame.x, frame.y),
        (right, frame.y),
        (frame.x, bottom),
        (right, bottom),
    ]
}

fn best_screen_for_frame(frame: WindowFrame, screens: &[ScreenArea]) -> Option<ScreenArea> {
    screens
        .iter()
        .copied()
        .filter(ScreenArea::has_area)
        .max_by_key(|screen| intersection_area(frame, *screen))
}

fn intersection_area(frame: WindowFrame, screen: ScreenArea) -> u64 {
    let left = frame.x.max(screen.x);
    let top = frame.y.max(screen.y);
    let right = frame.right().min(screen.right());
    let bottom = frame.bottom().min(screen.bottom());
    if right <= left || bottom <= top {
        return 0;
    }
    (right - left) as u64 * (bottom - top) as u64
}

fn clamp_dimension(value: u32, min: u32, max: u32) -> u32 {
    if max < min {
        max
    } else {
        value.clamp(min, max)
    }
}

fn clamp_axis(value: i32, size: u32, area_start: i32, area_size: u32) -> i32 {
    let max_start = area_start + area_size as i32 - size as i32;
    if max_start < area_start {
        return area_start;
    }
    value.clamp(area_start, max_start)
}

fn cache_frame(app_handle: &AppHandle, frame: WindowFrame) {
    let state: tauri::State<'_, MainWindowFrameState> = app_handle.state();
    if let Ok(mut cached_frame) = state.0.lock() {
        *cached_frame = Some(frame);
    };
}

fn cached_frame(app_handle: &AppHandle) -> Option<WindowFrame> {
    let state: tauri::State<'_, MainWindowFrameState> = app_handle.state();
    state.0.lock().ok().and_then(|cached_frame| *cached_frame)
}

fn window_state_path() -> Result<PathBuf, String> {
    crate::settings::preferred_app_config_path(WINDOW_STATE_FILE)
}

fn read_main_window_frame(scale_factor: f64) -> Option<WindowFrame> {
    read_main_window_frame_at(&window_state_path().ok()?, scale_factor)
}

fn read_main_window_frame_at(path: &Path, scale_factor: f64) -> Option<WindowFrame> {
    let content = fs::read_to_string(path).ok()?;
    let persisted: PersistedWindowState = serde_json::from_str(&content).ok()?;
    persisted
        .main
        .map(|frame| {
            persisted
                .coordinate_space
                .to_logical_frame(frame, scale_factor)
        })
        .filter(is_valid_saved_frame)
}

fn write_main_window_frame(frame: WindowFrame) -> Result<(), String> {
    write_main_window_frame_at(&window_state_path()?, frame)
}

fn write_main_window_frame_at(path: &Path, frame: WindowFrame) -> Result<(), String> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)
            .map_err(|e| format!("Failed to create window state directory: {e}"))?;
    }

    let persisted = PersistedWindowState {
        main: Some(frame),
        coordinate_space: WindowFrameCoordinateSpace::Logical,
    };
    let json = serde_json::to_string_pretty(&persisted)
        .map_err(|e| format!("Failed to serialize window state: {e}"))?;
    fs::write(path, json).map_err(|e| format!("Failed to write window state: {e}"))
}

fn is_valid_saved_frame(frame: &WindowFrame) -> bool {
    frame.width >= MIN_WINDOW_WIDTH && frame.height >= MIN_WINDOW_HEIGHT
}

fn rounded_i32(value: f64) -> i32 {
    value.round() as i32
}

fn rounded_u32(value: f64) -> u32 {
    value.round().max(0.0) as u32
}

impl WindowFrame {
    fn from_logical_geometry(position: LogicalPosition<f64>, size: LogicalSize<f64>) -> Self {
        Self {
            x: rounded_i32(position.x),
            y: rounded_i32(position.y),
            width: rounded_u32(size.width),
            height: rounded_u32(size.height),
        }
    }

    fn to_logical(self, scale_factor: f64) -> Self {
        let scale_factor = scale_factor.max(1.0);
        Self {
            x: rounded_i32(self.x as f64 / scale_factor),
            y: rounded_i32(self.y as f64 / scale_factor),
            width: rounded_u32(self.width as f64 / scale_factor),
            height: rounded_u32(self.height as f64 / scale_factor),
        }
    }

    fn right(self) -> i32 {
        self.x + self.width as i32
    }

    fn bottom(self) -> i32 {
        self.y + self.height as i32
    }
}

impl WindowFrameCoordinateSpace {
    fn to_logical_frame(self, frame: WindowFrame, scale_factor: f64) -> WindowFrame {
        match self {
            Self::Logical => frame,
            Self::Physical => frame.to_logical(scale_factor),
        }
    }
}

impl ScreenArea {
    fn right(self) -> i32 {
        self.x + self.width as i32
    }

    fn bottom(self) -> i32 {
        self.y + self.height as i32
    }

    fn has_area(&self) -> bool {
        self.width > 0 && self.height > 0
    }

    fn contains(&self, point: (i32, i32)) -> bool {
        let (x, y) = point;
        x >= self.x && x < self.right() && y >= self.y && y < self.bottom()
    }
}

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

    fn frame(x: i32, y: i32, width: u32, height: u32) -> WindowFrame {
        WindowFrame {
            x,
            y,
            width,
            height,
        }
    }

    fn screen(x: i32, y: i32, width: u32, height: u32) -> ScreenArea {
        ScreenArea {
            x,
            y,
            width,
            height,
        }
    }

    #[test]
    fn records_logical_window_geometry_for_persistence() {
        let saved = WindowFrame::from_logical_geometry(
            LogicalPosition::new(80.0, 120.0),
            LogicalSize::new(1100.0, 700.0),
        );

        assert_eq!(saved, frame(80, 120, 1100, 700));
    }

    #[test]
    fn migrates_legacy_physical_frames_to_logical_points() {
        let saved = frame(160, 240, 2200, 1400);

        assert_eq!(
            WindowFrameCoordinateSpace::Physical.to_logical_frame(saved, 2.0),
            frame(80, 120, 1100, 700)
        );
    }

    #[test]
    fn keeps_explicit_logical_frames_unscaled() {
        let saved = frame(80, 120, 1100, 700);

        assert_eq!(
            WindowFrameCoordinateSpace::Logical.to_logical_frame(saved, 2.0),
            saved
        );
    }

    #[test]
    fn keeps_valid_frame_unchanged() {
        let saved = frame(120, 80, 1400, 900);
        let screens = [screen(0, 0, 1920, 1080)];

        assert_eq!(fit_frame_to_screens(saved, &screens), Some(saved));
    }

    #[test]
    fn clamps_oversized_frame_to_current_work_area() {
        let saved = frame(-100, -80, 2600, 1800);
        let screens = [screen(0, 0, 1440, 900)];

        assert_eq!(
            fit_frame_to_screens(saved, &screens),
            Some(frame(0, 0, 1440, 900))
        );
    }

    #[test]
    fn moves_offscreen_frame_back_to_a_visible_screen() {
        let saved = frame(3200, 1800, 900, 700);
        let screens = [screen(0, 0, 1440, 900)];

        assert_eq!(
            fit_frame_to_screens(saved, &screens),
            Some(frame(540, 200, 900, 700))
        );
    }

    #[test]
    fn picks_the_screen_with_the_largest_visible_overlap() {
        let saved = frame(1700, 100, 900, 700);
        let screens = [screen(0, 0, 1920, 1080), screen(1920, 0, 1440, 900)];

        assert_eq!(fit_frame_to_screens(saved, &screens), Some(saved));
    }

    #[test]
    fn ignores_empty_screen_areas_when_restoring() {
        let saved = frame(100, 100, 800, 600);
        let screens = [screen(0, 0, 0, 900), screen(0, 0, 1440, 900)];

        assert_eq!(fit_frame_to_screens(saved, &screens), Some(saved));
    }

    #[test]
    fn returns_none_when_no_usable_screens_exist() {
        let saved = frame(100, 100, 800, 600);

        assert_eq!(fit_frame_to_screens(saved, &[]), None);
        assert_eq!(fit_frame_to_screens(saved, &[screen(0, 0, 0, 0)]), None);
    }

    #[test]
    fn fits_to_tiny_work_area_when_it_is_smaller_than_minimum_size() {
        let saved = frame(100, 100, 800, 600);
        let screens = [screen(0, 0, 320, 240)];

        assert_eq!(
            fit_frame_to_screens(saved, &screens),
            Some(frame(0, 0, 320, 240))
        );
    }

    #[test]
    fn reports_visibility_across_adjacent_screens() {
        let screens = [screen(0, 0, 1920, 1080), screen(1920, 0, 1440, 900)];

        assert!(frame_is_visible(frame(1700, 100, 900, 700), &screens));
        assert!(!frame_is_visible(frame(1700, 850, 900, 300), &screens));
    }

    #[test]
    fn computes_frame_and_screen_edges_for_overlap_checks() {
        let saved = frame(10, 20, 800, 600);
        let area = screen(0, 0, 500, 400);

        assert_eq!(saved.right(), 810);
        assert_eq!(saved.bottom(), 620);
        assert_eq!(area.right(), 500);
        assert_eq!(area.bottom(), 400);
        assert_eq!(intersection_area(saved, area), 490 * 380);
        assert_eq!(intersection_area(saved, screen(900, 900, 200, 200)), 0);
    }

    #[test]
    fn rejects_corrupted_tiny_saved_frames() {
        assert!(!is_valid_saved_frame(&frame(100, 100, 1, 900)));
        assert!(!is_valid_saved_frame(&frame(100, 100, 1400, 1)));
    }

    #[test]
    fn restores_again_after_runtime_ready() {
        assert!(restores_window_frame_after_runtime_ready(&RunEvent::Ready));
        assert!(!restores_window_frame_after_runtime_ready(
            &RunEvent::Resumed
        ));
        assert!(!saves_live_main_window_frame_on_event(
            &WindowEvent::Destroyed
        ));
    }

    #[test]
    fn persists_and_reads_logical_frame_from_disk() {
        let dir = tempfile::TempDir::new().unwrap();
        let path = dir.path().join("nested/window-state.json");
        let saved = frame(80, 120, 1100, 700);

        write_main_window_frame_at(&path, saved).unwrap();

        let json = std::fs::read_to_string(&path).unwrap();
        assert!(json.contains("\"coordinate_space\": \"logical\""));
        assert_eq!(read_main_window_frame_at(&path, 2.0), Some(saved));
    }

    #[test]
    fn reads_legacy_physical_frame_from_disk_as_logical_points() {
        let dir = tempfile::TempDir::new().unwrap();
        let path = dir.path().join("window-state.json");
        std::fs::write(
            &path,
            r#"{
              "main": { "x": 160, "y": 240, "width": 2200, "height": 1400 },
              "coordinate_space": "physical"
            }"#,
        )
        .unwrap();

        assert_eq!(
            read_main_window_frame_at(&path, 2.0),
            Some(frame(80, 120, 1100, 700))
        );
    }

    #[test]
    fn ignores_missing_corrupt_or_tiny_window_state_files() {
        let dir = tempfile::TempDir::new().unwrap();
        let path = dir.path().join("window-state.json");

        assert_eq!(read_main_window_frame_at(&path, 1.0), None);

        std::fs::write(&path, "not json").unwrap();
        assert_eq!(read_main_window_frame_at(&path, 1.0), None);

        std::fs::write(&path, r#"{"main":{"x":0,"y":0,"width":100,"height":100}}"#).unwrap();
        assert_eq!(read_main_window_frame_at(&path, 1.0), None);
    }
}
