use std::path::PathBuf;

use anyhow::{Context, Result};
use serde_json::{json, Value};
use tokio::fs;

use crate::{
    display::read_display_status,
    images::list_images,
    system::collect_system_view,
    types::{AppState, RotationSnapshot, SetupView, StateResponse},
};

pub(crate) async fn ensure_layout(state: &AppState) -> Result<()> {
    fs::create_dir_all(&state.image_dir).await?;

    if fs::try_exists(&state.monitor_path).await? {
        return Ok(());
    }

    let default = json!({
        "credentials": {
            "username": "admin",
            "password": "123456"
        },
        "setup": {
            "type": 1,
            "offDisplay": true,
            "controlParams": true,
            "controlDiskTemp": true,
            "customPanel": false,
            "language": 1,
            "switchTime": "10",
            "nativeSpecs": false,
            "nativeSpecsMode": "cards",
            "nativeMemes": true,
            "nativeGifs": true,
            "operationMode": 0,
            "theme": 1,
            "diskUpdate": 300,
            "ha_url": "",
            "ha_token": "",
            "refresh": 1
        },
        "mianban": [],
        "diy": []
    });

    save_monitor_json(state, &default).await
}

pub(crate) async fn load_monitor_json(state: &AppState) -> Result<Value> {
    let raw = fs::read_to_string(&state.monitor_path)
        .await
        .with_context(|| format!("Failed to read {:?}", state.monitor_path))?;
    serde_json::from_str(&raw).context("Failed to parse Monitor3.json")
}

pub(crate) async fn save_monitor_json(state: &AppState, value: &Value) -> Result<()> {
    let payload = serde_json::to_string_pretty(value)?;
    let tmp_path = temp_monitor_path(&state.monitor_path);

    fs::write(&tmp_path, payload)
        .await
        .with_context(|| format!("Failed to write {:?}", tmp_path))?;
    fs::rename(&tmp_path, &state.monitor_path)
        .await
        .with_context(|| format!("Failed to replace {:?}", state.monitor_path))
}

fn temp_monitor_path(path: &PathBuf) -> PathBuf {
    let file_name = path
        .file_name()
        .map(|name| format!("{}.tmp", name.to_string_lossy()))
        .unwrap_or_else(|| "Monitor3.json.tmp".into());
    path.with_file_name(file_name)
}

pub(crate) async fn build_state_response(state: &AppState) -> Result<StateResponse> {
    let monitor = load_monitor_json(state).await?;
    let setup = monitor
        .get("setup")
        .and_then(Value::as_object)
        .cloned()
        .unwrap_or_default();

    let custom_panel = setup
        .get("customPanel")
        .and_then(Value::as_bool)
        .unwrap_or(false);
    let switch_time = setup
        .get("switchTime")
        .and_then(Value::as_str)
        .unwrap_or("10")
        .to_string();
    let specs_enabled = current_specs_enabled(&monitor);
    let specs_mode = current_specs_mode(&monitor);
    let memes_enabled = current_memes_enabled(&monitor);
    let gifs_enabled = current_gifs_enabled(&monitor);

    Ok(StateResponse {
        monitor_path: state.monitor_path.display().to_string(),
        image_dir: state.image_dir.display().to_string(),
        setup: SetupView {
            custom_panel,
            switch_time,
            specs_enabled,
            specs_mode,
            memes_enabled,
            gifs_enabled,
        },
        active_images: current_active_images(&monitor),
        images: list_images(state).await?,
        display: read_display_status(&state.display_status),
        system: collect_system_view(),
    })
}

pub(crate) fn current_active_images(monitor: &Value) -> Vec<String> {
    monitor
        .get("diy")
        .and_then(Value::as_array)
        .into_iter()
        .flatten()
        .filter_map(|panel| panel.get("img").and_then(Value::as_str))
        .map(ToString::to_string)
        .collect()
}

pub(crate) fn current_switch_time(monitor: &Value) -> u32 {
    monitor
        .get("setup")
        .and_then(Value::as_object)
        .and_then(|setup| setup.get("switchTime"))
        .and_then(Value::as_str)
        .and_then(|value| value.parse::<u32>().ok())
        .filter(|value| (1..=600).contains(value))
        .unwrap_or(10)
}

pub(crate) fn current_custom_panel_enabled(monitor: &Value) -> bool {
    monitor
        .get("setup")
        .and_then(Value::as_object)
        .and_then(|setup| setup.get("customPanel"))
        .and_then(Value::as_bool)
        .unwrap_or(false)
}

pub(crate) fn current_specs_enabled(monitor: &Value) -> bool {
    monitor
        .get("setup")
        .and_then(Value::as_object)
        .and_then(|setup| setup.get("nativeSpecs"))
        .and_then(Value::as_bool)
        .unwrap_or(false)
}

pub(crate) fn current_memes_enabled(monitor: &Value) -> bool {
    monitor
        .get("setup")
        .and_then(Value::as_object)
        .and_then(|setup| setup.get("nativeMemes"))
        .and_then(Value::as_bool)
        .unwrap_or_else(|| !current_active_images(monitor).is_empty())
}

pub(crate) fn current_specs_mode(monitor: &Value) -> String {
    monitor
        .get("setup")
        .and_then(Value::as_object)
        .and_then(|setup| setup.get("nativeSpecsMode"))
        .and_then(Value::as_str)
        .map(normalize_specs_mode)
        .unwrap_or_else(|| "cards".into())
}

pub(crate) fn current_gifs_enabled(monitor: &Value) -> bool {
    monitor
        .get("setup")
        .and_then(Value::as_object)
        .and_then(|setup| setup.get("nativeGifs"))
        .and_then(Value::as_bool)
        .unwrap_or(true)
}

pub(crate) fn rotation_snapshot(monitor: &Value) -> RotationSnapshot {
    RotationSnapshot {
        custom_panel: current_custom_panel_enabled(monitor),
        switch_time: current_switch_time(monitor),
        specs_enabled: current_specs_enabled(monitor),
        specs_mode: current_specs_mode(monitor),
        memes_enabled: current_memes_enabled(monitor),
        gifs_enabled: current_gifs_enabled(monitor),
        active_images: current_active_images(monitor),
    }
}

pub(crate) fn set_custom_panels(
    monitor: &mut Value,
    switch_time: u32,
    specs_enabled: bool,
    specs_mode: &str,
    memes_enabled: bool,
    gifs_enabled: bool,
    images: &[String],
) {
    let enabled = specs_enabled || ((memes_enabled || gifs_enabled) && !images.is_empty());
    let setup = monitor
        .as_object_mut()
        .expect("monitor config must be an object")
        .entry("setup")
        .or_insert_with(|| Value::Object(Default::default()));

    if let Some(setup) = setup.as_object_mut() {
        setup.insert("customPanel".into(), Value::Bool(enabled));
        setup.insert("switchTime".into(), Value::String(switch_time.to_string()));
        setup.insert("nativeSpecs".into(), Value::Bool(specs_enabled));
        setup.insert(
            "nativeSpecsMode".into(),
            Value::String(normalize_specs_mode(specs_mode)),
        );
        setup.insert("nativeMemes".into(), Value::Bool(memes_enabled));
        setup.insert("nativeGifs".into(), Value::Bool(gifs_enabled));
    }

    monitor["mianban"] = Value::Array(
        (1..=images.len())
            .map(|index| Value::Number((index as u64).into()))
            .collect(),
    );
    monitor["diy"] = Value::Array(
        images
            .iter()
            .map(|name| json!({"type": 5, "img": name, "sensor": []}))
            .collect(),
    );
}

fn normalize_specs_mode(value: &str) -> String {
    match value {
        "meters" => "meters".into(),
        _ => "cards".into(),
    }
}

pub(crate) fn load_monitor_json_sync(path: &PathBuf) -> Result<Value> {
    let raw = std::fs::read_to_string(path)
        .with_context(|| format!("Failed to read {}", path.display()))?;
    serde_json::from_str(&raw).context("Failed to parse Monitor3.json")
}
