use std::{
    path::{Path, PathBuf},
    time::{SystemTime, UNIX_EPOCH},
};

use serde::{Deserialize, Serialize};
use tokio::io::AsyncWriteExt;

const INTENT_SCHEMA_VERSION: u32 = 1;
const INTENT_FILE: &str = ".lanspread.json";
const INTENT_TMP_FILE: &str = ".lanspread.json.tmp";

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum InstallIntentState {
    None,
    Installing,
    Updating,
    Uninstalling,
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct InstallIntent {
    pub schema_version: u32,
    pub id: String,
    pub recorded_at: u64,
    pub state: InstallIntentState,
    pub eti_version: Option<String>,
}

impl InstallIntent {
    pub fn new(id: &str, state: InstallIntentState, eti_version: Option<String>) -> Self {
        Self {
            schema_version: INTENT_SCHEMA_VERSION,
            id: id.to_string(),
            recorded_at: now_unix_secs(),
            state,
            eti_version,
        }
    }

    pub fn none(id: &str, eti_version: Option<String>) -> Self {
        Self::new(id, InstallIntentState::None, eti_version)
    }
}

pub fn intent_path(game_root: &Path) -> PathBuf {
    game_root.join(INTENT_FILE)
}

pub fn intent_tmp_path(game_root: &Path) -> PathBuf {
    game_root.join(INTENT_TMP_FILE)
}

pub async fn read_intent(game_root: &Path, id: &str) -> InstallIntent {
    let path = intent_path(game_root);
    let data = match tokio::fs::read_to_string(&path).await {
        Ok(data) => data,
        Err(err) => {
            if err.kind() != std::io::ErrorKind::NotFound {
                log::warn!("Failed to read install intent {}: {err}", path.display());
            }
            return InstallIntent::none(id, None);
        }
    };

    match serde_json::from_str::<InstallIntent>(&data) {
        Ok(intent) if intent.schema_version == INTENT_SCHEMA_VERSION && intent.id == id => intent,
        Ok(intent) => {
            log::warn!(
                "Ignoring install intent {} with schema {} for id {}",
                path.display(),
                intent.schema_version,
                intent.id
            );
            InstallIntent::none(id, None)
        }
        Err(err) => {
            log::warn!("Ignoring corrupt install intent {}: {err}", path.display());
            InstallIntent::none(id, None)
        }
    }
}

pub async fn write_intent(game_root: &Path, intent: &InstallIntent) -> eyre::Result<()> {
    tokio::fs::create_dir_all(game_root).await?;
    let path = intent_path(game_root);
    let tmp_path = intent_tmp_path(game_root);
    let data = serde_json::to_vec_pretty(intent)?;

    let mut file = tokio::fs::File::create(&tmp_path).await?;
    file.write_all(&data).await?;
    file.sync_all().await?;
    drop(file);

    tokio::fs::rename(&tmp_path, &path).await?;
    sync_parent_dir(&path)?;
    Ok(())
}

fn now_unix_secs() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs()
}

#[cfg(unix)]
fn sync_parent_dir(path: &Path) -> std::io::Result<()> {
    if let Some(parent) = path.parent() {
        std::fs::File::open(parent)?.sync_all()?;
    }
    Ok(())
}

#[cfg(not(unix))]
fn sync_parent_dir(_path: &Path) -> std::io::Result<()> {
    Ok(())
}

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

    #[tokio::test]
    async fn tmp_write_without_rename_leaves_previous_intent_intact() {
        let temp = TempDir::new("lanspread-intent");
        let previous = InstallIntent::new(
            "game",
            InstallIntentState::Updating,
            Some("20240101".to_string()),
        );
        write_intent(temp.path(), &previous)
            .await
            .expect("previous intent should be written");

        tokio::fs::write(
            intent_tmp_path(temp.path()),
            serde_json::to_vec(&InstallIntent::new(
                "game",
                InstallIntentState::Installing,
                Some("20250101".to_string()),
            ))
            .expect("intent should serialize"),
        )
        .await
        .expect("tmp intent should be written");

        let recovered = read_intent(temp.path(), "game").await;
        assert_eq!(recovered.state, InstallIntentState::Updating);
        assert_eq!(recovered.eti_version.as_deref(), Some("20240101"));
    }

    #[tokio::test]
    async fn schema_mismatch_is_treated_as_missing() {
        let temp = TempDir::new("lanspread-intent");
        tokio::fs::write(
            intent_path(temp.path()),
            r#"{"schema_version":2,"id":"game","recorded_at":0,"state":"Updating"}"#,
        )
        .await
        .expect("intent should be written");

        let recovered = read_intent(temp.path(), "game").await;
        assert_eq!(recovered.state, InstallIntentState::None);
    }

    #[tokio::test]
    async fn mismatched_id_is_treated_as_missing() {
        let temp = TempDir::new("lanspread-intent");
        tokio::fs::write(
            intent_path(temp.path()),
            r#"{"schema_version":1,"id":"other","recorded_at":0,"state":"Updating"}"#,
        )
        .await
        .expect("intent should be written");

        let recovered = read_intent(temp.path(), "game").await;
        assert_eq!(recovered.state, InstallIntentState::None);
    }

    #[tokio::test]
    async fn corrupt_intent_is_treated_as_missing() {
        let temp = TempDir::new("lanspread-intent");
        tokio::fs::write(intent_path(temp.path()), b"not json")
            .await
            .expect("intent should be written");

        let recovered = read_intent(temp.path(), "game").await;
        assert_eq!(recovered.state, InstallIntentState::None);
    }

    #[tokio::test]
    async fn old_manifest_hash_field_is_ignored_and_new_writes_omit_it() {
        let temp = TempDir::new("lanspread-intent");
        tokio::fs::write(
            intent_path(temp.path()),
            r#"{"schema_version":1,"id":"game","recorded_at":0,"state":"Updating","eti_version":"20240101","manifest_hash":42}"#,
        )
        .await
        .expect("intent should be written");

        let recovered = read_intent(temp.path(), "game").await;
        assert_eq!(recovered.state, InstallIntentState::Updating);
        assert_eq!(recovered.eti_version.as_deref(), Some("20240101"));

        write_intent(temp.path(), &InstallIntent::none("game", None))
            .await
            .expect("intent should be written");
        let written = tokio::fs::read_to_string(intent_path(temp.path()))
            .await
            .expect("intent should be readable");
        assert!(
            serde_json::from_str::<serde_json::Value>(&written)
                .expect("intent should parse")
                .get("manifest_hash")
                .is_none()
        );
    }
}
