//! Platform-specific persistent storage backends.
//!
//! Native builds persist bytes under the app data directory, while browser
//! builds route the same engine API through `localStorage`.

use std::io;
use std::sync::Arc;

use bevy::prelude::Resource;

#[cfg(not(target_arch = "wasm32"))]
use std::{
    fs,
    path::{Path, PathBuf},
};

#[cfg(target_arch = "wasm32")]
use base64::{Engine as _, engine::general_purpose::STANDARD};
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::JsValue;

/// Abstracts platform-specific key-value / file storage.
///
/// Native: backed by the filesystem (via `solitaire_data`).
/// WASM: backed by `localStorage`.
pub trait StorageBackend: Send + Sync + 'static {
    /// Read bytes for the given key. Returns `None` if the key does not exist.
    fn read(&self, key: &str) -> io::Result<Option<Vec<u8>>>;

    /// Write bytes for the given key atomically.
    fn write(&self, key: &str, data: &[u8]) -> io::Result<()>;

    /// Delete a key. No-op if the key does not exist.
    fn delete(&self, key: &str) -> io::Result<()>;

    /// List all known keys (for migration / debug purposes).
    fn keys(&self) -> io::Result<Vec<String>>;
}

/// Bevy resource that exposes the active platform storage backend.
#[derive(Resource, Clone)]
pub struct StorageBackendResource(pub Arc<dyn StorageBackend>);

/// Construct the default storage backend for the current platform.
pub fn default_storage_backend() -> io::Result<Arc<dyn StorageBackend>> {
    #[cfg(target_arch = "wasm32")]
    {
        let storage = WasmStorage;
        storage.local_storage()?;
        Ok(Arc::new(storage))
    }

    #[cfg(not(target_arch = "wasm32"))]
    {
        Ok(Arc::new(NativeStorage::platform_default()?))
    }
}

/// Filesystem-backed [`StorageBackend`] for native targets.
#[cfg(not(target_arch = "wasm32"))]
#[derive(Debug, Clone)]
pub struct NativeStorage {
    base_dir: PathBuf,
}

#[cfg(not(target_arch = "wasm32"))]
impl NativeStorage {
    /// Create a storage backend rooted at `base_dir`.
    pub fn new(base_dir: impl Into<PathBuf>) -> Self {
        Self {
            base_dir: base_dir.into(),
        }
    }

    /// Create a storage backend rooted at the app's platform data directory.
    pub fn platform_default() -> io::Result<Self> {
        let base_dir = solitaire_data::game_state_file_path()
            .and_then(|path| path.parent().map(|parent| parent.to_path_buf()))
            .ok_or_else(|| {
                io::Error::new(io::ErrorKind::NotFound, "platform data dir unavailable")
            })?;
        Ok(Self::new(base_dir))
    }

    fn key_path(&self, key: &str) -> PathBuf {
        let safe = sanitize_native_key(key);
        self.base_dir.join(safe)
    }
}

#[cfg(not(target_arch = "wasm32"))]
impl StorageBackend for NativeStorage {
    fn read(&self, key: &str) -> io::Result<Option<Vec<u8>>> {
        let path = self.key_path(key);
        match fs::read(&path) {
            Ok(data) => Ok(Some(data)),
            Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
            Err(err) => Err(err),
        }
    }

    fn write(&self, key: &str, data: &[u8]) -> io::Result<()> {
        let path = self.key_path(key);
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)?;
        }

        let tmp_path = tmp_path_for(&path);
        fs::write(&tmp_path, data)?;
        fs::rename(&tmp_path, path)?;
        Ok(())
    }

    fn delete(&self, key: &str) -> io::Result<()> {
        let path = self.key_path(key);
        match fs::remove_file(&path) {
            Ok(()) => Ok(()),
            Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(()),
            Err(err) => Err(err),
        }
    }

    fn keys(&self) -> io::Result<Vec<String>> {
        let mut keys = Vec::new();
        let entries = match fs::read_dir(&self.base_dir) {
            Ok(entries) => entries,
            Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(keys),
            Err(err) => return Err(err),
        };

        for entry in entries {
            let entry = entry?;
            if !entry.file_type()?.is_file() {
                continue;
            }
            if let Some(name) = entry.file_name().to_str() {
                keys.push(name.to_string());
            }
        }
        keys.sort();
        Ok(keys)
    }
}

#[cfg(not(target_arch = "wasm32"))]
fn sanitize_native_key(key: &str) -> String {
    let safe: String = key
        .chars()
        .map(|ch| match ch {
            '/' | '\\' | ':' => '_',
            _ => ch,
        })
        .collect();

    if safe.is_empty() || safe == "." || safe == ".." {
        String::from("_")
    } else {
        safe
    }
}

#[cfg(not(target_arch = "wasm32"))]
fn tmp_path_for(path: &Path) -> PathBuf {
    match path.extension().and_then(|ext| ext.to_str()) {
        Some(ext) => path.with_extension(format!("{ext}.tmp")),
        None => path.with_extension("tmp"),
    }
}

/// `localStorage`-backed [`StorageBackend`] for browser builds.
#[cfg(target_arch = "wasm32")]
#[derive(Debug, Default, Clone, Copy)]
pub struct WasmStorage;

#[cfg(target_arch = "wasm32")]
impl WasmStorage {
    fn local_storage(&self) -> io::Result<web_sys::Storage> {
        let window = web_sys::window().ok_or_else(|| io::Error::other("window unavailable"))?;
        let storage = window
            .local_storage()
            .map_err(js_error)?
            .ok_or_else(|| io::Error::other("localStorage unavailable"))?;
        Ok(storage)
    }
}

#[cfg(target_arch = "wasm32")]
impl StorageBackend for WasmStorage {
    fn read(&self, key: &str) -> io::Result<Option<Vec<u8>>> {
        match self.local_storage()?.get_item(key).map_err(js_error)? {
            Some(encoded) => STANDARD
                .decode(encoded)
                .map(Some)
                .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err)),
            None => Ok(None),
        }
    }

    fn write(&self, key: &str, data: &[u8]) -> io::Result<()> {
        let encoded = STANDARD.encode(data);
        let storage = self.local_storage()?;
        storage.set_item(key, &encoded).map_err(js_error)
    }

    fn delete(&self, key: &str) -> io::Result<()> {
        let storage = self.local_storage()?;
        storage.remove_item(key).map_err(js_error)
    }

    fn keys(&self) -> io::Result<Vec<String>> {
        let storage = self.local_storage()?;
        let len = storage.length().map_err(js_error)?;
        let mut keys = Vec::with_capacity(len as usize);
        for idx in 0..len {
            let key = storage.key(idx).map_err(js_error)?.ok_or_else(|| {
                io::Error::new(
                    io::ErrorKind::NotFound,
                    format!("localStorage key missing at index {idx}"),
                )
            })?;
            keys.push(key);
        }
        keys.sort();
        Ok(keys)
    }
}

#[cfg(target_arch = "wasm32")]
fn js_error(err: JsValue) -> io::Error {
    let message = err
        .as_string()
        .map_or_else(|| format!("{err:?}"), |value| value);
    io::Error::other(message)
}

#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
    use tempfile::tempdir;

    use super::{NativeStorage, StorageBackend};

    #[test]
    fn native_storage_round_trips_binary_bytes() {
        let dir = tempdir().expect("tempdir should be available");
        let storage = NativeStorage::new(dir.path());
        let key = "state/save:1.json";
        let data = [0_u8, 1, 2, 127, 255];

        storage.write(key, &data).expect("write should succeed");
        let loaded = storage
            .read(key)
            .expect("read should succeed")
            .expect("key should exist");

        assert_eq!(loaded, data);
        assert_eq!(
            storage.keys().expect("keys should succeed"),
            vec!["state_save_1.json"]
        );
    }

    #[test]
    fn native_storage_delete_and_missing_keys_are_noops() {
        let dir = tempdir().expect("tempdir should be available");
        let storage = NativeStorage::new(dir.path());

        assert_eq!(
            storage.keys().expect("keys should succeed"),
            Vec::<String>::new()
        );
        assert_eq!(storage.read("missing").expect("read should succeed"), None);
        storage.delete("missing").expect("delete should succeed");

        storage
            .write("session.bin", &[1, 2, 3])
            .expect("write should succeed");
        storage
            .delete("session.bin")
            .expect("delete should succeed");

        assert_eq!(
            storage.read("session.bin").expect("read should succeed"),
            None
        );
    }
}
