use std::{io::Cursor, path::PathBuf};

use anyhow::{Context, Result};
use asterctl::img;
use chrono::Local;
use image::{
    codecs::gif::GifDecoder, imageops::FilterType, AnimationDecoder, DynamicImage, Frame,
    ImageFormat, RgbImage,
};
use tokio::fs;

use crate::{
    system::{DISPLAY_HEIGHT, DISPLAY_WIDTH},
    types::{AppState, GifFrameMeta, GifSetManifest, ImageView},
};

pub(crate) const NYAN_CAT_GIF: &[u8] = include_bytes!("../assets/nyan-cat.gif");
pub(crate) const MIN_GIF_FRAME_DELAY_MS: u32 = 33;
const GIF_FRAME_PREFIX: &str = ".gifframe-";
const GIF_MANIFEST_SUFFIX: &str = ".gifset.json";

pub(crate) async fn list_images(state: &AppState) -> Result<Vec<ImageView>> {
    let mut out = Vec::new();
    let mut dir = fs::read_dir(&state.image_dir).await?;
    while let Some(entry) = dir.next_entry().await? {
        let path = entry.path();
        let file_type = entry.file_type().await?;
        if !file_type.is_file() {
            continue;
        }
        let Some(name) = path.file_name().map(|name| name.to_string_lossy().to_string()) else {
            continue;
        };
        if is_gif_frame_name(&name) {
            continue;
        }
        let meta = entry.metadata().await?;
        if is_gif_manifest_name(&name) {
            let raw = fs::read_to_string(&path).await?;
            let manifest: GifSetManifest = serde_json::from_str(&raw)
                .with_context(|| format!("Failed to parse GIF manifest {}", path.display()))?;
            out.push(ImageView {
                url: format!("/api/images/{}", manifest.preview),
                name,
                label: manifest.label,
                size: meta.len(),
                animated: true,
                frame_count: manifest.source_frame_count.unwrap_or(manifest.frames.len()),
                display_frame_count: manifest.frames.len(),
            });
        } else {
            out.push(ImageView {
                url: format!("/api/images/{name}"),
                label: name.clone(),
                name,
                size: meta.len(),
                animated: false,
                frame_count: 1,
                display_frame_count: 1,
            });
        }
    }
    out.sort_by(|a, b| a.name.cmp(&b.name));
    Ok(out)
}

pub(crate) async fn convert_and_store_image(
    state: &AppState,
    file_name: &str,
    bytes: &[u8],
) -> Result<String> {
    if is_gif_file(file_name, bytes) {
        return convert_and_store_gif(state, file_name, bytes).await;
    }

    let image = image::load_from_memory(bytes)
        .with_context(|| format!("Unsupported image format: {file_name}"))?;
    let target_name = make_image_name(file_name);
    let path = state.image_dir.join(&target_name);
    let rendered = render_display_panel(&image);
    rendered
        .save_with_format(&path, ImageFormat::Jpeg)
        .with_context(|| format!("Failed to save {}", path.display()))?;
    Ok(target_name)
}

async fn convert_and_store_gif(state: &AppState, file_name: &str, bytes: &[u8]) -> Result<String> {
    let decoder = GifDecoder::new(Cursor::new(bytes))
        .with_context(|| format!("Unsupported GIF format: {file_name}"))?;
    let frames = decoder
        .into_frames()
        .collect_frames()
        .context("Failed to decode GIF frames")?;
    if frames.is_empty() {
        anyhow::bail!("GIF contains no frames");
    }
    let source_frame_count = frames.len();

    let base = make_image_stem(file_name);
    let timestamp = Local::now().format("%Y%m%d-%H%M%S");
    let manifest_name = format!("panel-{timestamp}-{base}{GIF_MANIFEST_SUFFIX}");
    let mut rendered_frames = Vec::new();

    for frame in frames {
        let delay_ms = frame_delay_ms(&frame);
        let rendered = render_display_panel(&DynamicImage::ImageRgba8(frame.into_buffer()));
        rendered_frames.push((rendered, delay_ms));
    }

    let collapsed_frames = collapse_rendered_frames(rendered_frames);
    if collapsed_frames.len() < 2 {
        anyhow::bail!(
            "GIF contains no visible animation after decoding; please upload a truly animated GIF"
        );
    }

    let mut stored_frames = Vec::new();
    for (idx, (rendered, delay_ms)) in collapsed_frames.into_iter().enumerate() {
        let frame_name = format!("{GIF_FRAME_PREFIX}{timestamp}-{base}-{idx:03}.jpg");
        let frame_path = state.image_dir.join(&frame_name);
        rendered
            .save_with_format(&frame_path, ImageFormat::Jpeg)
            .with_context(|| format!("Failed to save {}", frame_path.display()))?;
        stored_frames.push(GifFrameMeta {
            name: frame_name,
            delay_ms,
        });
    }

    let manifest = GifSetManifest {
        kind: "gif_set".into(),
        version: 1,
        label: file_name.to_string(),
        preview: stored_frames
            .first()
            .map(|frame| frame.name.clone())
            .unwrap_or_default(),
        source_frame_count: Some(source_frame_count),
        frames: stored_frames,
    };
    let manifest_path = state.image_dir.join(&manifest_name);
    fs::write(&manifest_path, serde_json::to_vec_pretty(&manifest)?)
        .await
        .with_context(|| format!("Failed to save {}", manifest_path.display()))?;
    Ok(manifest_name)
}

fn collapse_rendered_frames(frames: Vec<(RgbImage, u32)>) -> Vec<(RgbImage, u32)> {
    let mut collapsed: Vec<(RgbImage, u32)> = Vec::new();

    for (frame, delay_ms) in frames {
        if let Some((prev_frame, prev_delay_ms)) = collapsed.last_mut()
            && *prev_frame == frame
        {
            *prev_delay_ms = prev_delay_ms.saturating_add(delay_ms);
            continue;
        }

        collapsed.push((frame, delay_ms));
    }

    collapsed
}

pub(crate) fn render_display_panel(image: &DynamicImage) -> RgbImage {
    let resized = image.resize(DISPLAY_WIDTH, DISPLAY_HEIGHT, FilterType::Lanczos3);
    let rgb = resized.to_rgb8();

    let mut canvas = RgbImage::from_pixel(DISPLAY_WIDTH, DISPLAY_HEIGHT, image::Rgb([8, 10, 12]));

    let offset_x = ((DISPLAY_WIDTH - rgb.width()) / 2) as i64;
    let offset_y = ((DISPLAY_HEIGHT - rgb.height()) / 2) as i64;
    image::imageops::overlay(&mut canvas, &rgb, offset_x, offset_y);
    canvas
}

fn make_image_name(file_name: &str) -> String {
    let clean = make_image_stem(file_name);
    let timestamp = Local::now().format("%Y%m%d-%H%M%S");
    format!("panel-{timestamp}-{clean}.jpg")
}

fn make_image_stem(file_name: &str) -> String {
    let base = file_name
        .rsplit_once('.')
        .map(|(name, _)| name)
        .unwrap_or(file_name);
    let clean: String = base
        .chars()
        .map(|ch| match ch {
            'a'..='z' | 'A'..='Z' | '0'..='9' => ch.to_ascii_lowercase(),
            _ => '-',
        })
        .collect();
    let clean = clean.trim_matches('-');
    if clean.is_empty() {
        "panel".into()
    } else {
        clean.to_string()
    }
}

fn is_gif_file(file_name: &str, bytes: &[u8]) -> bool {
    file_name.to_ascii_lowercase().ends_with(".gif")
        || bytes.starts_with(b"GIF87a")
        || bytes.starts_with(b"GIF89a")
}

pub(crate) fn is_gif_manifest_name(name: &str) -> bool {
    name.ends_with(GIF_MANIFEST_SUFFIX)
}

fn is_gif_frame_name(name: &str) -> bool {
    name.starts_with(GIF_FRAME_PREFIX)
}

fn frame_delay_ms(frame: &Frame) -> u32 {
    let (numer, denom) = frame.delay().numer_denom_ms();
    let delay = if denom == 0 {
        numer
    } else {
        ((numer as f64) / (denom as f64)).round() as u32
    };
    delay.max(MIN_GIF_FRAME_DELAY_MS)
}

pub(crate) async fn delete_image_asset(state: &AppState, name: &str) -> Result<()> {
    let path = state.image_dir.join(name);
    if is_gif_manifest_name(name) {
        let raw = fs::read_to_string(&path)
            .await
            .with_context(|| format!("Failed to read {}", path.display()))?;
        let manifest: GifSetManifest = serde_json::from_str(&raw)
            .with_context(|| format!("Failed to parse GIF manifest {}", path.display()))?;
        for frame in manifest.frames {
            let frame_path = state.image_dir.join(frame.name);
            match fs::remove_file(&frame_path).await {
                Ok(_) => {}
                Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
                Err(err) => return Err(err.into()),
            }
        }
    }

    fs::remove_file(&path)
        .await
        .with_context(|| format!("Failed to remove {}", path.display()))?;
    Ok(())
}

pub(crate) fn load_gif_manifest(image_dir: &PathBuf, name: &str) -> Result<GifSetManifest> {
    let path = image_dir.join(name);
    let raw = std::fs::read_to_string(&path)
        .with_context(|| format!("Failed to read GIF manifest {}", path.display()))?;
    serde_json::from_str(&raw)
        .with_context(|| format!("Failed to parse GIF manifest {}", path.display()))
}

pub(crate) fn load_panel_rgb(image_dir: &PathBuf, image_name: &str) -> Result<RgbImage> {
    let path = image_dir.join(image_name);
    let image = img::load_image(&path, Some(asterctl_lcd::DISPLAY_SIZE))
        .with_context(|| format!("Failed to load panel image {}", path.display()))?;
    Ok(image.to_rgb8())
}

#[cfg(test)]
mod tests {
    use image::{Rgb, RgbImage};
    use serde_json::json;

    use crate::types::GifSetManifest;

    use super::{collapse_rendered_frames, MIN_GIF_FRAME_DELAY_MS};

    #[test]
    fn collapse_rendered_frames_merges_identical_neighbors() {
        let frame_a = RgbImage::from_pixel(2, 2, Rgb([1, 2, 3]));
        let frame_b = RgbImage::from_pixel(2, 2, Rgb([9, 8, 7]));

        let collapsed = collapse_rendered_frames(vec![
            (frame_a.clone(), 100),
            (frame_a, 120),
            (frame_b.clone(), 80),
            (frame_b, 90),
        ]);

        assert_eq!(collapsed.len(), 2);
        assert_eq!(collapsed[0].1, 220);
        assert_eq!(collapsed[1].1, 170);
    }

    #[test]
    fn min_gif_frame_delay_supports_50ms_frames() {
        assert!(MIN_GIF_FRAME_DELAY_MS <= 50);
    }

    #[test]
    fn gif_manifest_without_source_frame_count_stays_compatible() {
        let manifest: GifSetManifest = serde_json::from_value(json!({
            "kind": "gif_set",
            "version": 1,
            "label": "demo.gif",
            "preview": ".gifframe-000.jpg",
            "frames": [
                { "name": ".gifframe-000.jpg", "delay_ms": 50 },
                { "name": ".gifframe-001.jpg", "delay_ms": 50 }
            ]
        }))
        .unwrap();

        assert_eq!(manifest.source_frame_count, None);
        assert_eq!(manifest.frames.len(), 2);
    }
}
