use std::collections::{HashMap, HashSet};
use std::ffi::OsStr;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};

use serde::{Deserialize, Serialize};
use systemless::runner::{FixtureRunner, VfsFileSnapshot, VfsFileStat, VfsFileSummary};

const SAVE_SCAN_FRAME_INTERVAL: u8 = 30;
const METADATA_FILE: &str = "metadata.json";
const DATA_FORK_FILE: &str = "data.fork";
const RESOURCE_FORK_FILE: &str = "resource.fork";

#[derive(Debug)]
pub struct DesktopSaveStore {
    root: PathBuf,
    archive_vfs_stats: HashMap<String, VfsFileStat>,
    last_vfs_fingerprints: HashMap<String, SaveFingerprint>,
    persisted_save_paths: HashSet<String>,
    save_scan_frame: u8,
}

#[derive(Clone, Debug, Eq, PartialEq)]
struct SaveFingerprint {
    data_len: usize,
    resource_len: usize,
    data_hash: u64,
    resource_hash: u64,
    file_type: u32,
    creator: u32,
    finder_flags: u16,
    modified_date: u32,
}

impl From<&VfsFileSummary> for SaveFingerprint {
    fn from(summary: &VfsFileSummary) -> Self {
        Self {
            data_len: summary.data_len,
            resource_len: summary.resource_len,
            data_hash: summary.data_hash,
            resource_hash: summary.resource_hash,
            file_type: summary.file_type,
            creator: summary.creator,
            finder_flags: summary.finder_flags,
            modified_date: summary.modified_date,
        }
    }
}

impl From<&VfsFileSnapshot> for SaveFingerprint {
    fn from(snapshot: &VfsFileSnapshot) -> Self {
        Self {
            data_len: snapshot.data_fork.len(),
            resource_len: snapshot.resource_fork.len(),
            data_hash: save_fork_hash(&snapshot.data_fork),
            resource_hash: save_fork_hash(&snapshot.resource_fork),
            file_type: snapshot.file_type,
            creator: snapshot.creator,
            finder_flags: snapshot.finder_flags,
            modified_date: snapshot.modified_date,
        }
    }
}

#[derive(Debug, Deserialize, Serialize)]
struct StoredSaveMetadata {
    version: u8,
    path: String,
    file_type: u32,
    creator: u32,
    finder_flags: u16,
    created_date: u32,
    modified_date: u32,
}

impl DesktopSaveStore {
    pub fn for_loaded_archive(game_path: &Path, runner: &mut FixtureRunner) -> Self {
        Self {
            root: save_root_for_game_path(game_path),
            archive_vfs_stats: vfs_stats(runner),
            last_vfs_fingerprints: HashMap::new(),
            persisted_save_paths: HashSet::new(),
            save_scan_frame: 0,
        }
    }

    pub fn root(&self) -> &Path {
        &self.root
    }

    pub fn load_saved_files(&mut self) -> Vec<VfsFileSnapshot> {
        let files = match load_saved_files_from_root(&self.root) {
            Ok(files) => files,
            Err(err) => {
                eprintln!(
                    "[SYSTEMLESS] Could not load desktop saves from {}: {}",
                    self.root.display(),
                    err
                );
                Vec::new()
            }
        };
        self.persisted_save_paths = files.iter().map(|file| file.path.clone()).collect();
        self.last_vfs_fingerprints = files
            .iter()
            .map(|file| (file.path.clone(), SaveFingerprint::from(file)))
            .collect();
        files
    }

    pub fn sync_save_files(&mut self, runner: &mut FixtureRunner) {
        self.save_scan_frame = self.save_scan_frame.wrapping_add(1);
        if self.save_scan_frame % SAVE_SCAN_FRAME_INTERVAL != 0 {
            return;
        }
        self.sync_save_files_now(runner);
    }

    pub fn sync_save_files_now(&mut self, runner: &mut FixtureRunner) {
        let stats = runner.vfs_file_stats_where(is_user_save_path);
        let mut next_fingerprints = HashMap::new();
        let mut next_persisted_paths = HashSet::new();

        for stat in stats {
            if !self.persisted_save_paths.contains(&stat.path)
                && self
                    .archive_vfs_stats
                    .get(&stat.path)
                    .is_some_and(|archive| vfs_stats_match(archive, &stat))
            {
                continue;
            }

            let Some(summary) = runner.vfs_file_summary(&stat.path) else {
                continue;
            };
            let fingerprint = SaveFingerprint::from(&summary);
            next_fingerprints.insert(summary.path.clone(), fingerprint.clone());

            let Some(snapshot) = runner.vfs_file_snapshot(&summary.path) else {
                continue;
            };
            if self.last_vfs_fingerprints.get(&summary.path) != Some(&fingerprint)
                || !self.persisted_save_paths.contains(&summary.path)
            {
                match self.persist_save_file(&snapshot) {
                    Ok(()) => {
                        eprintln!(
                            "[SYSTEMLESS] Saved desktop file: {}",
                            self.save_dir_for_vfs_path(&snapshot.path).display()
                        );
                        next_persisted_paths.insert(summary.path.clone());
                    }
                    Err(err) => {
                        eprintln!(
                            "[SYSTEMLESS] Could not persist desktop save {}: {}",
                            snapshot.path, err
                        );
                        next_fingerprints.remove(&summary.path);
                        if self.persisted_save_paths.contains(&summary.path) {
                            next_persisted_paths.insert(summary.path.clone());
                        }
                    }
                }
            } else {
                next_persisted_paths.insert(summary.path.clone());
            }
        }

        let stale_paths = self
            .persisted_save_paths
            .difference(&next_persisted_paths)
            .cloned()
            .collect::<Vec<_>>();
        for path in stale_paths {
            if let Err(err) = self.delete_save_file(&path) {
                eprintln!(
                    "[SYSTEMLESS] Could not remove desktop save {}: {}",
                    path, err
                );
                next_fingerprints.remove(&path);
                next_persisted_paths.insert(path.clone());
            }
        }

        self.persisted_save_paths = next_persisted_paths;
        self.last_vfs_fingerprints = next_fingerprints;
    }

    fn persist_save_file(&self, file: &VfsFileSnapshot) -> io::Result<()> {
        let dir = self.save_dir_for_vfs_path(&file.path);
        fs::create_dir_all(&dir)?;
        fs::write(dir.join(DATA_FORK_FILE), &file.data_fork)?;
        fs::write(dir.join(RESOURCE_FORK_FILE), &file.resource_fork)?;

        let metadata = StoredSaveMetadata {
            version: 1,
            path: file.path.clone(),
            file_type: file.file_type,
            creator: file.creator,
            finder_flags: file.finder_flags,
            created_date: file.created_date,
            modified_date: file.modified_date,
        };
        let metadata = serde_json::to_vec_pretty(&metadata)
            .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
        fs::write(dir.join(METADATA_FILE), metadata)?;
        Ok(())
    }

    fn delete_save_file(&self, path: &str) -> io::Result<()> {
        let dir = self.save_dir_for_vfs_path(path);
        match fs::remove_dir_all(&dir) {
            Ok(()) => {
                if let Some(parent) = dir.parent() {
                    self.prune_empty_parent_dirs(parent.to_path_buf());
                }
                Ok(())
            }
            Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(()),
            Err(err) => Err(err),
        }
    }

    fn save_dir_for_vfs_path(&self, path: &str) -> PathBuf {
        let mut dir = self.root.clone();
        let mut added = false;
        for component in path.trim_matches('/').split('/') {
            if component.is_empty() {
                continue;
            }
            dir.push(encode_host_component(component));
            added = true;
        }
        if !added {
            dir.push("%00");
        }
        dir
    }

    fn prune_empty_parent_dirs(&self, mut dir: PathBuf) {
        while dir != self.root {
            match fs::remove_dir(&dir) {
                Ok(()) => {
                    let Some(parent) = dir.parent() else {
                        break;
                    };
                    dir = parent.to_path_buf();
                }
                Err(_) => break,
            }
        }
    }
}

fn vfs_stats(runner: &mut FixtureRunner) -> HashMap<String, VfsFileStat> {
    runner
        .vfs_file_stats_where(|_| true)
        .into_iter()
        .map(|stat| (stat.path.clone(), stat))
        .collect()
}

fn vfs_stats_match(left: &VfsFileStat, right: &VfsFileStat) -> bool {
    left.data_len == right.data_len
        && left.resource_len == right.resource_len
        && left.file_type == right.file_type
        && left.creator == right.creator
        && left.finder_flags == right.finder_flags
        && left.modified_date == right.modified_date
}

fn save_fork_hash(bytes: &[u8]) -> u64 {
    let mut hash = 0xcbf2_9ce4_8422_2325u64;
    for &byte in bytes {
        hash ^= u64::from(byte);
        hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
    }
    hash
}

fn load_saved_files_from_root(root: &Path) -> io::Result<Vec<VfsFileSnapshot>> {
    if !root.exists() {
        return Ok(Vec::new());
    }

    let mut metadata_files = Vec::new();
    collect_metadata_files(root, &mut metadata_files)?;
    let mut files = Vec::new();
    for metadata_path in metadata_files {
        match read_saved_file(&metadata_path) {
            Ok(file) => files.push(file),
            Err(err) => eprintln!(
                "[SYSTEMLESS] Skipping unreadable desktop save {}: {}",
                metadata_path.display(),
                err
            ),
        }
    }
    files.sort_by(|left, right| {
        left.path
            .to_ascii_lowercase()
            .cmp(&right.path.to_ascii_lowercase())
    });
    Ok(files)
}

fn collect_metadata_files(dir: &Path, out: &mut Vec<PathBuf>) -> io::Result<()> {
    for entry in fs::read_dir(dir)? {
        let entry = entry?;
        let path = entry.path();
        let file_type = entry.file_type()?;
        if file_type.is_dir() {
            collect_metadata_files(&path, out)?;
        } else if file_type.is_file() && entry.file_name() == OsStr::new(METADATA_FILE) {
            out.push(path);
        }
    }
    Ok(())
}

fn read_saved_file(metadata_path: &Path) -> io::Result<VfsFileSnapshot> {
    let metadata = fs::read(metadata_path)?;
    let metadata: StoredSaveMetadata = serde_json::from_slice(&metadata)
        .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
    if metadata.version != 1 {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            format!("unsupported save metadata version {}", metadata.version),
        ));
    }
    if !is_user_save_path(&metadata.path) {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            "stored path is not a user save path",
        ));
    }

    let dir = metadata_path.parent().ok_or_else(|| {
        io::Error::new(
            io::ErrorKind::InvalidData,
            "save metadata path does not have a parent directory",
        )
    })?;

    Ok(VfsFileSnapshot {
        path: metadata.path,
        data_fork: read_fork_file(&dir.join(DATA_FORK_FILE))?,
        resource_fork: read_fork_file(&dir.join(RESOURCE_FORK_FILE))?,
        file_type: metadata.file_type,
        creator: metadata.creator,
        finder_flags: metadata.finder_flags,
        created_date: metadata.created_date,
        modified_date: metadata.modified_date,
    })
}

fn read_fork_file(path: &Path) -> io::Result<Vec<u8>> {
    match fs::read(path) {
        Ok(bytes) => Ok(bytes),
        Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(Vec::new()),
        Err(err) => Err(err),
    }
}

fn save_root_for_game_path(game_path: &Path) -> PathBuf {
    let archive_dir = game_path
        .parent()
        .filter(|path| !path.as_os_str().is_empty())
        .unwrap_or_else(|| Path::new("."));
    let archive_stem = game_path
        .file_stem()
        .and_then(OsStr::to_str)
        .filter(|stem| !stem.trim().is_empty())
        .map(encode_host_component)
        .unwrap_or_else(|| "game".to_string());
    archive_dir
        .join(".systemless")
        .join("saves")
        .join(archive_stem)
}

fn is_user_save_path(path: &str) -> bool {
    let normalized = path.trim_matches('/');
    if normalized.is_empty() {
        return false;
    }

    let lower = normalized.to_ascii_lowercase();
    if lower.starts_with("__rsrc__/")
        || lower.starts_with("system folder/preferences/")
        || lower.starts_with("system folder/temporary items/")
        || lower.starts_with("temporary items/")
        || lower.starts_with("trash/")
    {
        return false;
    }

    let name = lower.rsplit('/').next().unwrap_or(lower.as_str());
    !matches!(name, "desktop db" | "desktop df" | "thevolume")
}

fn encode_host_component(component: &str) -> String {
    if component == "." || component == ".." {
        return percent_encode_all(component.as_bytes());
    }

    let mut encoded = String::new();
    for &byte in component.as_bytes() {
        match byte {
            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b' ' | b'.' | b'-' | b'_' => {
                encoded.push(byte as char)
            }
            _ => push_percent_encoded(&mut encoded, byte),
        }
    }

    if encoded.is_empty() {
        "%00".to_string()
    } else {
        encoded
    }
}

fn percent_encode_all(bytes: &[u8]) -> String {
    let mut encoded = String::new();
    for &byte in bytes {
        push_percent_encoded(&mut encoded, byte);
    }
    encoded
}

fn push_percent_encoded(out: &mut String, byte: u8) {
    const HEX: &[u8; 16] = b"0123456789ABCDEF";
    out.push('%');
    out.push(HEX[(byte >> 4) as usize] as char);
    out.push(HEX[(byte & 0x0F) as usize] as char);
}

#[cfg(test)]
mod tests {
    use super::*;
    use systemless::runner::{FixtureRunner, FixtureRunnerConfig};

    fn snapshot(path: &str) -> VfsFileSnapshot {
        VfsFileSnapshot {
            path: path.to_string(),
            data_fork: vec![1, 2, 3],
            resource_fork: vec![4, 5, 6, 7],
            file_type: u32::from_be_bytes(*b"PIL "),
            creator: u32::from_be_bytes(*b"EVO!"),
            finder_flags: 0x4000,
            created_date: 123,
            modified_date: 456,
        }
    }

    #[test]
    fn save_root_sits_next_to_archive() {
        assert_eq!(
            save_root_for_game_path(Path::new("/Games/EV Override 1.0.1.sit")),
            Path::new("/Games/.systemless/saves/EV Override 1.0.1")
        );
        assert_eq!(
            save_root_for_game_path(Path::new("EVO.sit")),
            Path::new("./.systemless/saves/EVO")
        );
    }

    #[test]
    fn host_components_are_percent_encoded() {
        assert_eq!(encode_host_component("Pilots"), "Pilots");
        assert_eq!(encode_host_component("EV:Override%"), "EV%3AOverride%25");
        assert_eq!(encode_host_component("."), "%2E");
        assert_eq!(encode_host_component(".."), "%2E%2E");
    }

    #[test]
    fn native_snapshot_round_trips_both_forks_and_metadata() {
        let temp = tempfile::tempdir().unwrap();
        let root = temp.path().join(".systemless/saves/EVO");
        let store = DesktopSaveStore {
            root: root.clone(),
            archive_vfs_stats: HashMap::new(),
            last_vfs_fingerprints: HashMap::new(),
            persisted_save_paths: HashSet::new(),
            save_scan_frame: 0,
        };
        let original = snapshot("EV Override 1.0.1/Pilots/Rick Hardslab");

        store.persist_save_file(&original).unwrap();

        let loaded = load_saved_files_from_root(&root).unwrap();
        assert_eq!(loaded, vec![original]);
    }

    #[test]
    fn sync_skips_unchanged_archive_files_but_persists_modified_saves() {
        let temp = tempfile::tempdir().unwrap();
        let game_path = temp.path().join("EVO.sit");
        let mut runner = FixtureRunner::new(8 * 1024 * 1024, FixtureRunnerConfig::default());
        let mut packaged = snapshot("EV Override 1.0.1/Pilots/Packaged Pilot");
        runner.import_vfs_file(&packaged);

        let mut store = DesktopSaveStore::for_loaded_archive(&game_path, &mut runner);
        store.sync_save_files_now(&mut runner);
        assert!(!store.root().exists());

        packaged.resource_fork.push(8);
        packaged.modified_date += 1;
        runner.import_vfs_file(&packaged);
        store.sync_save_files_now(&mut runner);

        let loaded = load_saved_files_from_root(store.root()).unwrap();
        assert_eq!(loaded, vec![packaged]);
    }

    #[test]
    fn user_save_filter_skips_system_support_files() {
        assert!(is_user_save_path("Pilots/Rick Hardslab"));
        assert!(is_user_save_path("Games/My Saved Game"));

        assert!(!is_user_save_path(""));
        assert!(!is_user_save_path(
            "System Folder/Preferences/EV Override License"
        ));
        assert!(!is_user_save_path("Temporary Items/scratch"));
        assert!(!is_user_save_path("Trash/Old Pilot"));
        assert!(!is_user_save_path("Desktop DB"));
        assert!(!is_user_save_path("Desktop DF"));
        assert!(!is_user_save_path("TheVolume"));
        assert!(!is_user_save_path("__rsrc__/Pilots/Rick Hardslab"));
    }
}
