//! Filesystem-backed persistent paired-device store.

use std::sync::Arc;

use async_trait::async_trait;
use serde::{Deserialize, Serialize};

use domain::ports::{DeviceSessionStore, FileSystem, FsError, RemotePath, StoreError};
use domain::PairedDevice;

const DEVICES_DOC_VERSION: u8 = 1;
const SECURITY_DIR: &str = "security";
const DEVICES_FILE: &str = "devices.json";

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
struct DevicesDoc {
    version: u8,
    devices: Vec<PairedDevice>,
}

/// JSON-file implementation for `{app_data_dir}/security/devices.json`.
#[derive(Clone)]
pub struct FsDeviceSessionStore {
    fs: Arc<dyn FileSystem>,
    app_data_dir: String,
}

impl FsDeviceSessionStore {
    /// Builds the store from an injected filesystem port and app-data directory.
    #[must_use]
    pub fn new(fs: Arc<dyn FileSystem>, app_data_dir: impl Into<String>) -> Self {
        Self {
            fs,
            app_data_dir: app_data_dir.into(),
        }
    }

    fn security_dir(&self) -> RemotePath {
        RemotePath::new(format!(
            "{}/{}",
            self.app_data_dir.trim_end_matches(['/', '\\']),
            SECURITY_DIR
        ))
    }

    fn path(&self) -> RemotePath {
        RemotePath::new(format!("{}/{}", self.security_dir().as_str(), DEVICES_FILE))
    }
}

#[async_trait]
impl DeviceSessionStore for FsDeviceSessionStore {
    async fn load_devices(&self) -> Result<Vec<PairedDevice>, StoreError> {
        match self.fs.read(&self.path()).await {
            Ok(bytes) => {
                let doc: DevicesDoc = serde_json::from_slice(&bytes)
                    .map_err(|err| StoreError::Serialization(err.to_string()))?;
                if doc.version != DEVICES_DOC_VERSION {
                    return Err(StoreError::Serialization(format!(
                        "unsupported devices.json version {}",
                        doc.version
                    )));
                }
                Ok(doc.devices)
            }
            Err(FsError::NotFound(_)) => Ok(Vec::new()),
            Err(err) => Err(StoreError::Io(err.to_string())),
        }
    }

    async fn save_devices(&self, devices: &[PairedDevice]) -> Result<(), StoreError> {
        self.fs
            .create_dir_all(&self.security_dir())
            .await
            .map_err(|err| StoreError::Io(err.to_string()))?;
        let doc = DevicesDoc {
            version: DEVICES_DOC_VERSION,
            devices: devices.to_vec(),
        };
        let mut bytes = serde_json::to_vec_pretty(&doc)
            .map_err(|err| StoreError::Serialization(err.to_string()))?;
        bytes.push(b'\n');
        self.fs
            .write(&self.path(), &bytes)
            .await
            .map_err(|err| StoreError::Io(err.to_string()))
    }
}

#[cfg(test)]
mod tests {
    use self::infrastructure_test_support::TempFs;
    use super::*;
    use domain::{DeviceId, DeviceName, SessionTokenHash};
    use uuid::Uuid;

    mod infrastructure_test_support {
        use std::path::{Path, PathBuf};
        use std::sync::Arc;

        use crate::LocalFileSystem;
        use domain::ports::FileSystem;

        pub struct TempFs {
            pub root: PathBuf,
            pub fs: Arc<dyn FileSystem>,
        }

        impl TempFs {
            pub fn new() -> Self {
                let root = std::env::temp_dir()
                    .join(format!("idea-devices-store-{}", uuid::Uuid::new_v4()));
                std::fs::create_dir_all(&root).unwrap();
                Self {
                    root,
                    fs: Arc::new(LocalFileSystem::new()),
                }
            }

            pub fn path(&self, rel: &str) -> PathBuf {
                self.root.join(Path::new(rel))
            }
        }

        impl Drop for TempFs {
            fn drop(&mut self) {
                let _ = std::fs::remove_dir_all(&self.root);
            }
        }
    }

    fn device() -> PairedDevice {
        PairedDevice {
            device_id: DeviceId::from_uuid(Uuid::from_u128(1)),
            name: DeviceName::new("Phone").unwrap(),
            paired_at_ms: 1000,
            last_seen_at_ms: 1000,
            session_token_hash: SessionTokenHash::from_token_bytes(&[1; 32]),
        }
    }

    #[tokio::test]
    async fn missing_file_loads_empty_device_list() {
        let temp = TempFs::new();
        let store = FsDeviceSessionStore::new(Arc::clone(&temp.fs), temp.root.to_string_lossy());

        assert_eq!(store.load_devices().await.unwrap(), Vec::new());
    }

    #[tokio::test]
    async fn devices_round_trip_in_v1_document() {
        let temp = TempFs::new();
        let store = FsDeviceSessionStore::new(Arc::clone(&temp.fs), temp.root.to_string_lossy());

        store.save_devices(&[device()]).await.unwrap();

        let loaded = store.load_devices().await.unwrap();
        assert_eq!(loaded, vec![device()]);
        let raw = std::fs::read_to_string(temp.path("security/devices.json")).unwrap();
        assert!(raw.contains("\"version\": 1"));
        assert!(raw.contains("\"deviceId\""));
        assert!(raw.contains("\"sessionTokenHash\""));
    }

    #[tokio::test]
    async fn corrupted_json_is_a_serialization_error() {
        let temp = TempFs::new();
        std::fs::create_dir_all(temp.path("security")).unwrap();
        std::fs::write(temp.path("security/devices.json"), b"{not json").unwrap();
        let store = FsDeviceSessionStore::new(Arc::clone(&temp.fs), temp.root.to_string_lossy());

        let err = store.load_devices().await.unwrap_err();

        assert!(matches!(err, StoreError::Serialization(_)));
    }
}
