use std::{
    sync::{Arc, RwLock},
    thread,
    time::{Duration, Instant},
};

use anyhow::{Context, Result};
use asterctl_lcd::{AooScreen, AooScreenBuilder};
use chrono::Utc;
use tracing::{error, info, warn};

use crate::{
    images::{is_gif_manifest_name, load_gif_manifest, load_panel_rgb, MIN_GIF_FRAME_DELAY_MS},
    monitor::{load_monitor_json_sync, rotation_snapshot},
    system::{overlay_system_specs, render_system_panel},
    types::{ActiveFrame, AppState, DisplayConfig, DisplayStatus, RotationSlot, RotationSnapshot},
};

const SYSTEM_FRAME_NAME: &str = "System Specs";
const IDLE_SLEEP_MS: u64 = 750;

#[derive(Clone, Debug)]
struct PlaybackCursor {
    slot_idx: usize,
    frame_idx: usize,
    slot_started_at: Instant,
    frame_started_at: Instant,
}

pub(crate) fn initial_display_status(config: &DisplayConfig) -> DisplayStatus {
    let mut status = DisplayStatus {
        native_enabled: config.native_enabled,
        connected: false,
        mode: display_mode(config),
        device: display_target(config),
        custom_panel: false,
        specs_enabled: false,
        memes_enabled: false,
        gifs_enabled: false,
        rotation_active: false,
        switch_time: "10".into(),
        active_images: Vec::new(),
        current_image: None,
        last_error: None,
        updated_at: Some(stamp_now()),
    };

    if !config.native_enabled {
        status.last_error = Some("Native display loop disabled via --disable-display".into());
    }

    status
}

pub(crate) fn spawn_display_worker(state: Arc<AppState>, config: DisplayConfig) {
    thread::spawn(move || run_display_worker(state, config));
}

fn run_display_worker(state: Arc<AppState>, config: DisplayConfig) {
    if !config.native_enabled {
        info!("Native display loop disabled");
        return;
    }

    loop {
        match open_screen(&config) {
            Ok(mut screen) => {
                info!("Display target opened: {}", display_target(&config));
                if let Err(err) = screen.init() {
                    warn!("Display init failed: {err}");
                    set_display_error(
                        &state.display_status,
                        false,
                        format!("Display init failed: {err}"),
                    );
                    thread::sleep(Duration::from_secs(3));
                    continue;
                }

                update_display_status(&state.display_status, |status| {
                    status.connected = true;
                    status.last_error = None;
                    status.updated_at = Some(stamp_now());
                });

                if let Err(err) = run_display_session(&state, &mut screen) {
                    error!("Display loop error: {err}");
                    set_display_error(
                        &state.display_status,
                        false,
                        format!("Display loop error: {err}"),
                    );
                }
            }
            Err(err) => {
                warn!("Failed to open display target: {err}");
                set_display_error(
                    &state.display_status,
                    false,
                    format!("Failed to open display target: {err}"),
                );
            }
        }

        thread::sleep(Duration::from_secs(3));
    }
}

fn open_screen(config: &DisplayConfig) -> Result<AooScreen> {
    let mut builder = AooScreenBuilder::new();
    builder.no_init_check(config.write_only);

    if config.simulate {
        builder.simulate()
    } else if let Some(device) = &config.device {
        builder.open_device(device)
    } else if let Some(usb) = &config.usb {
        builder.open_usb_id(usb)
    } else {
        builder.open_default()
    }
}

fn run_display_session(state: &AppState, screen: &mut AooScreen) -> Result<()> {
    let mut snapshot = RotationSnapshot::default();
    let mut current_frame_key: Option<String> = None;
    let mut slots: Vec<RotationSlot> = Vec::new();
    let mut playback: Option<PlaybackCursor> = None;

    loop {
        let monitor = match load_monitor_json_sync(&state.monitor_path) {
            Ok(monitor) => monitor,
            Err(err) => {
                update_display_status(&state.display_status, |status| {
                    status.last_error = Some(format!("Config read failed: {err}"));
                    status.connected = true;
                    status.updated_at = Some(stamp_now());
                });
                thread::sleep(Duration::from_secs(1));
                continue;
            }
        };

        let new_snapshot = rotation_snapshot(&monitor);
        if new_snapshot != snapshot {
            snapshot = new_snapshot;
            current_frame_key = None;
            slots = build_rotation_slots(&snapshot, &state.image_dir);
            playback = new_playback_cursor(&slots, Instant::now());
        }

        update_display_status(&state.display_status, |status| {
            status.custom_panel = snapshot.custom_panel;
            status.specs_enabled = snapshot.specs_enabled;
            status.memes_enabled = snapshot.memes_enabled;
            status.gifs_enabled = snapshot.gifs_enabled;
            status.rotation_active = snapshot.rotation_active();
            status.switch_time = snapshot.switch_time.to_string();
            status.active_images = snapshot.active_images.clone();
            if !snapshot.rotation_active() {
                status.current_image = None;
            }
            status.connected = true;
            status.updated_at = Some(stamp_now());
        });

        if !snapshot.rotation_active() {
            thread::sleep(Duration::from_millis(IDLE_SLEEP_MS));
            continue;
        }

        let specs_only = snapshot.specs_enabled && slots.is_empty();
        if let Some(active_frame) =
            current_slot_frame(&slots, specs_only, &mut playback, Instant::now())
        {
            let send_due = current_frame_key.as_deref() != Some(active_frame.key.as_str());
            if send_due {
                if active_frame.image_name == SYSTEM_FRAME_NAME {
                    let panel_name = SYSTEM_FRAME_NAME.to_string();
                    let rgb_img = render_system_panel(&snapshot.specs_mode);
                    prepare_screen_for_frame(screen, &active_frame);
                    screen
                        .send_image(&rgb_img)
                        .context("Failed to send system specs panel")?;

                    update_display_status(&state.display_status, |status| {
                        status.current_image = Some(panel_name);
                        status.last_error = None;
                        status.connected = true;
                        status.updated_at = Some(stamp_now());
                    });
                } else {
                    match load_panel_rgb(&state.image_dir, &active_frame.image_name) {
                        Ok(mut rgb_img) => {
                            if snapshot.specs_enabled {
                                overlay_system_specs(&mut rgb_img);
                            }
                            prepare_screen_for_frame(screen, &active_frame);
                            screen.send_image(&rgb_img).with_context(|| {
                                format!("Failed to send panel {}", active_frame.image_name)
                            })?;

                            update_display_status(&state.display_status, |status| {
                                status.current_image = Some(active_frame.display_name.clone());
                                status.last_error = None;
                                status.connected = true;
                                status.updated_at = Some(stamp_now());
                            });
                        }
                        Err(err) => {
                            warn!("Skipping panel {}: {err}", active_frame.image_name);
                            update_display_status(&state.display_status, |status| {
                                status.current_image = Some(active_frame.display_name.clone());
                                status.last_error = Some(format!(
                                    "Panel {} could not be rendered: {err}",
                                    active_frame.display_name
                                ));
                                status.connected = true;
                                status.updated_at = Some(stamp_now());
                            });
                        }
                    }
                }
                current_frame_key = Some(active_frame.key);
            }
        }

        thread::sleep(rotation_sleep(
            snapshot.rotation_active(),
            &slots,
            specs_only,
            &playback,
        ));
    }
}

fn build_rotation_slots(
    snapshot: &RotationSnapshot,
    image_dir: &std::path::PathBuf,
) -> Vec<RotationSlot> {
    let mut slots = Vec::new();
    if snapshot.memes_enabled || snapshot.gifs_enabled {
        for name in &snapshot.active_images {
            if is_gif_manifest_name(name) {
                if !snapshot.gifs_enabled {
                    continue;
                }
                match load_gif_manifest(image_dir, name) {
                    Ok(manifest) if !manifest.frames.is_empty() => {
                        let mut frames = Vec::new();
                        for (idx, frame) in manifest.frames.iter().enumerate() {
                            frames.push((
                                ActiveFrame {
                                    key: format!("{name}#{idx}"),
                                    image_name: frame.name.clone(),
                                    display_name: format!("{} [{}]", manifest.label, idx + 1),
                                    animated: true,
                                },
                                Duration::from_millis(
                                    frame.delay_ms.max(MIN_GIF_FRAME_DELAY_MS) as u64
                                ),
                            ));
                        }
                        slots.push(RotationSlot {
                            total_duration: Duration::from_secs(snapshot.switch_time as u64),
                            frames,
                        });
                    }
                    Ok(_) => {}
                    Err(err) => warn!("Skipping GIF set {name}: {err}"),
                }
            } else if snapshot.memes_enabled {
                slots.push(RotationSlot {
                    total_duration: Duration::from_secs(snapshot.switch_time as u64),
                    frames: vec![(
                        ActiveFrame {
                            key: name.clone(),
                            image_name: name.clone(),
                            display_name: name.clone(),
                            animated: false,
                        },
                        Duration::from_secs(snapshot.switch_time as u64),
                    )],
                });
            }
        }
    }
    if slots.is_empty() && snapshot.specs_enabled {
        slots.push(RotationSlot {
            total_duration: Duration::from_secs(snapshot.switch_time as u64),
            frames: vec![(
                ActiveFrame {
                    key: SYSTEM_FRAME_NAME.into(),
                    image_name: SYSTEM_FRAME_NAME.into(),
                    display_name: SYSTEM_FRAME_NAME.into(),
                    animated: false,
                },
                Duration::from_secs(snapshot.switch_time as u64),
            )],
        });
    }
    slots
}

fn current_slot_frame(
    slots: &[RotationSlot],
    specs_only: bool,
    playback: &mut Option<PlaybackCursor>,
    now: Instant,
) -> Option<ActiveFrame> {
    if specs_only {
        return Some(ActiveFrame {
            key: SYSTEM_FRAME_NAME.into(),
            image_name: SYSTEM_FRAME_NAME.into(),
            display_name: SYSTEM_FRAME_NAME.into(),
            animated: false,
        });
    }
    if slots.is_empty() {
        return None;
    }

    if playback.is_none() {
        *playback = new_playback_cursor(slots, now);
    }

    let cursor = playback.as_mut()?;
    advance_playback_cursor(cursor, slots, now);
    slots
        .get(cursor.slot_idx)
        .and_then(|slot| slot.frames.get(cursor.frame_idx))
        .map(|(frame, _)| frame.clone())
}

fn new_playback_cursor(slots: &[RotationSlot], now: Instant) -> Option<PlaybackCursor> {
    if slots.is_empty() {
        None
    } else {
        Some(PlaybackCursor {
            slot_idx: 0,
            frame_idx: 0,
            slot_started_at: now,
            frame_started_at: now,
        })
    }
}

fn advance_playback_cursor(cursor: &mut PlaybackCursor, slots: &[RotationSlot], now: Instant) {
    if slots.is_empty() {
        return;
    }

    let mut guard = 0usize;
    while guard < slots.len().saturating_mul(2).max(1) {
        guard += 1;
        let slot = &slots[cursor.slot_idx];

        if now.duration_since(cursor.slot_started_at) >= slot.total_duration {
            cursor.slot_idx = (cursor.slot_idx + 1) % slots.len();
            cursor.frame_idx = 0;
            cursor.slot_started_at = now;
            cursor.frame_started_at = now;
            continue;
        }

        if slot.frames.len() <= 1 {
            return;
        }

        let current_delay = slot.frames[cursor.frame_idx].1;
        if now.duration_since(cursor.frame_started_at) >= current_delay {
            cursor.frame_idx = (cursor.frame_idx + 1) % slot.frames.len();
            cursor.frame_started_at = now;
            continue;
        }

        return;
    }
}

fn prepare_screen_for_frame(screen: &mut AooScreen, frame: &ActiveFrame) {
    let use_cache = !frame.animated;
    if screen.is_cache_enabled() != use_cache {
        screen.enable_cache(use_cache);
    }
}

fn rotation_sleep(
    rotation_active: bool,
    slots: &[RotationSlot],
    specs_only: bool,
    playback: &Option<PlaybackCursor>,
) -> Duration {
    if !rotation_active || specs_only || slots.is_empty() {
        return Duration::from_millis(IDLE_SLEEP_MS);
    }

    let Some(cursor) = playback.as_ref() else {
        return Duration::from_millis(IDLE_SLEEP_MS);
    };

    let slot = &slots[cursor.slot_idx];
    if slot.frames.len() <= 1 {
        let remaining = slot
            .total_duration
            .saturating_sub(cursor.slot_started_at.elapsed());
        return remaining.clamp(
            Duration::from_millis(MIN_GIF_FRAME_DELAY_MS as u64),
            Duration::from_millis(IDLE_SLEEP_MS),
        );
    }

    let frame_delay = slot.frames[cursor.frame_idx].1;
    let frame_remaining = frame_delay.saturating_sub(cursor.frame_started_at.elapsed());
    let slot_remaining = slot
        .total_duration
        .saturating_sub(cursor.slot_started_at.elapsed());
    frame_remaining.min(slot_remaining).clamp(
        Duration::from_millis(MIN_GIF_FRAME_DELAY_MS as u64),
        Duration::from_millis(IDLE_SLEEP_MS),
    )
}

pub(crate) fn read_display_status(status: &Arc<RwLock<DisplayStatus>>) -> DisplayStatus {
    status.read().expect("display status lock poisoned").clone()
}

fn update_display_status(
    status: &Arc<RwLock<DisplayStatus>>,
    apply: impl FnOnce(&mut DisplayStatus),
) {
    let mut guard = status.write().expect("display status lock poisoned");
    apply(&mut guard);
}

fn set_display_error(status: &Arc<RwLock<DisplayStatus>>, connected: bool, message: String) {
    update_display_status(status, |current| {
        current.connected = connected;
        current.last_error = Some(message);
        current.updated_at = Some(stamp_now());
        if !connected {
            current.current_image = None;
        }
    });
}

fn display_mode(config: &DisplayConfig) -> String {
    if !config.native_enabled {
        "disabled".into()
    } else if config.simulate {
        "simulate".into()
    } else if config.device.is_some() {
        "device".into()
    } else if config.usb.is_some() {
        "usb-id".into()
    } else {
        "usb-default".into()
    }
}

fn display_target(config: &DisplayConfig) -> String {
    if !config.native_enabled {
        "native loop disabled".into()
    } else if config.simulate {
        "simulated LCD".into()
    } else if let Some(device) = &config.device {
        device.clone()
    } else if let Some(usb) = &config.usb {
        format!("USB {usb}")
    } else {
        "default AOOSTAR USB UART 0416:90A1".into()
    }
}

fn stamp_now() -> String {
    Utc::now().to_rfc3339()
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use std::time::{SystemTime, UNIX_EPOCH};

    fn test_frame(key: &str) -> ActiveFrame {
        ActiveFrame {
            key: key.into(),
            image_name: format!("{key}.jpg"),
            display_name: "gif".into(),
            animated: true,
        }
    }

    #[test]
    fn prepare_screen_for_frame_disables_cache_for_animated_frames() {
        let mut screen = AooScreenBuilder::new().simulate().unwrap();
        assert!(screen.is_cache_enabled());

        prepare_screen_for_frame(&mut screen, &test_frame("gif#0"));
        assert!(!screen.is_cache_enabled());

        let still = ActiveFrame {
            key: "still".into(),
            image_name: "still.jpg".into(),
            display_name: "still".into(),
            animated: false,
        };
        prepare_screen_for_frame(&mut screen, &still);
        assert!(screen.is_cache_enabled());
    }

    #[test]
    fn playback_cursor_advances_frames_sequentially() {
        let slots = vec![RotationSlot {
            total_duration: Duration::from_secs(10),
            frames: vec![
                (test_frame("gif#0"), Duration::from_millis(100)),
                (test_frame("gif#1"), Duration::from_millis(100)),
                (test_frame("gif#2"), Duration::from_millis(100)),
            ],
        }];
        let start = Instant::now();
        let mut cursor = new_playback_cursor(&slots, start);

        assert_eq!(
            current_slot_frame(
                &slots,
                false,
                &mut cursor,
                start + Duration::from_millis(50)
            )
            .unwrap()
            .key,
            "gif#0"
        );
        assert_eq!(
            current_slot_frame(
                &slots,
                false,
                &mut cursor,
                start + Duration::from_millis(150)
            )
            .unwrap()
            .key,
            "gif#1"
        );
        assert_eq!(
            current_slot_frame(
                &slots,
                false,
                &mut cursor,
                start + Duration::from_millis(250)
            )
            .unwrap()
            .key,
            "gif#2"
        );
        assert_eq!(
            current_slot_frame(
                &slots,
                false,
                &mut cursor,
                start + Duration::from_millis(350)
            )
            .unwrap()
            .key,
            "gif#0"
        );
    }

    #[test]
    fn playback_cursor_does_not_skip_overdue_frames() {
        let slots = vec![RotationSlot {
            total_duration: Duration::from_secs(10),
            frames: vec![
                (test_frame("gif#0"), Duration::from_millis(50)),
                (test_frame("gif#1"), Duration::from_millis(50)),
                (test_frame("gif#2"), Duration::from_millis(50)),
            ],
        }];
        let start = Instant::now();
        let mut cursor = new_playback_cursor(&slots, start);

        assert_eq!(
            current_slot_frame(
                &slots,
                false,
                &mut cursor,
                start + Duration::from_millis(220)
            )
            .unwrap()
            .key,
            "gif#1"
        );
        assert_eq!(
            current_slot_frame(
                &slots,
                false,
                &mut cursor,
                start + Duration::from_millis(221)
            )
            .unwrap()
            .key,
            "gif#1"
        );
        assert_eq!(
            current_slot_frame(
                &slots,
                false,
                &mut cursor,
                start + Duration::from_millis(271)
            )
            .unwrap()
            .key,
            "gif#2"
        );
    }

    #[test]
    fn rotation_sleep_preserves_fast_gif_frames() {
        let slots = vec![RotationSlot {
            total_duration: Duration::from_secs(2),
            frames: vec![
                (test_frame("gif#0"), Duration::from_millis(50)),
                (test_frame("gif#1"), Duration::from_millis(50)),
            ],
        }];
        let playback = new_playback_cursor(&slots, Instant::now());

        let sleep = rotation_sleep(true, &slots, false, &playback);
        assert!(sleep <= Duration::from_millis(50));
        assert!(sleep >= Duration::from_millis(MIN_GIF_FRAME_DELAY_MS as u64));
    }

    #[test]
    fn build_rotation_slots_uses_switch_time_for_gif_slot_duration() {
        let unique = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_nanos();
        let image_dir = std::env::temp_dir().join(format!("aster-webui-gif-test-{unique}"));
        fs::create_dir_all(&image_dir).unwrap();
        let manifest_name = "panel-test.gifset.json";
        fs::write(
            image_dir.join(manifest_name),
            r#"{
              "kind": "gif_set",
              "version": 1,
              "label": "panel-test.gif",
              "preview": ".gifframe-test-000.jpg",
              "frames": [
                { "name": ".gifframe-test-000.jpg", "delay_ms": 100 },
                { "name": ".gifframe-test-001.jpg", "delay_ms": 100 }
              ]
            }"#,
        )
        .unwrap();

        let snapshot = RotationSnapshot {
            custom_panel: true,
            switch_time: 7,
            specs_enabled: false,
            specs_mode: "cards".into(),
            memes_enabled: false,
            gifs_enabled: true,
            active_images: vec![manifest_name.into()],
        };

        let slots = build_rotation_slots(&snapshot, &image_dir);
        fs::remove_dir_all(&image_dir).unwrap();

        assert_eq!(slots.len(), 1);
        assert_eq!(slots[0].total_duration, Duration::from_secs(7));
        assert_eq!(slots[0].frames.len(), 2);
    }

    #[test]
    fn build_rotation_slots_skips_gif_when_gifs_are_disabled() {
        let unique = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_nanos();
        let image_dir =
            std::env::temp_dir().join(format!("aster-webui-gif-disabled-test-{unique}"));
        fs::create_dir_all(&image_dir).unwrap();
        let manifest_name = "panel-test.gifset.json";
        fs::write(
            image_dir.join(manifest_name),
            r#"{
              "kind": "gif_set",
              "version": 1,
              "label": "panel-test.gif",
              "preview": ".gifframe-test-000.jpg",
              "frames": [
                { "name": ".gifframe-test-000.jpg", "delay_ms": 100 },
                { "name": ".gifframe-test-001.jpg", "delay_ms": 100 }
              ]
            }"#,
        )
        .unwrap();

        let snapshot = RotationSnapshot {
            custom_panel: true,
            switch_time: 7,
            specs_enabled: false,
            specs_mode: "cards".into(),
            memes_enabled: false,
            gifs_enabled: false,
            active_images: vec![manifest_name.into()],
        };

        let slots = build_rotation_slots(&snapshot, &image_dir);
        fs::remove_dir_all(&image_dir).unwrap();

        assert!(slots.is_empty());
    }
}
