//! Abstraction système de fichiers.
//!
//! Permet de brancher un accès natif (`NativeFs`) ou distant (`BridgeFs` via WebSocket)
//! sans changer le code appelant.

/// Entrée d'un répertoire.
#[derive(Debug, Clone)]
pub struct FsDirEntry {
    pub name: String,
    pub is_dir: bool,
    pub size: u64,
}

/// Trait d'accès au système de fichiers — implémenté par `NativeFs` (desktop)
/// et `BridgeFs` (WASM via WebSocket).
pub trait FileSystem: Send + Sync {
    fn read_file(&self, path: &str) -> Result<Vec<u8>, String>;
    fn read_file_string(&self, path: &str) -> Result<String, String>;
    fn write_file(&self, path: &str, data: &[u8]) -> Result<(), String>;
    fn write_file_string(&self, path: &str, content: &str) -> Result<(), String> {
        self.write_file(path, content.as_bytes())
    }
    fn list_dir(&self, path: &str) -> Result<Vec<FsDirEntry>, String>;
    fn create_dir_all(&self, path: &str) -> Result<(), String>;
    fn copy_file(&self, src: &str, dst: &str) -> Result<(), String>;
    fn exists(&self, path: &str) -> bool;

    /// Supprime récursivement un dossier et son contenu.
    /// Implémentation WASM : envoie `BridgeRequest::RemoveDir` au bridge qui
    /// vérifie qu'un `project.ron` est présent à la racine (garde-fou destructif).
    /// Implémentation desktop : appelle `std::fs::remove_dir_all`.
    fn remove_dir_all(&self, path: &str) -> Result<(), String>;

    /// Supprime un chemin (fichier ou dossier) **sous un projet SanBevyEditor**
    /// (item feedback #31). Utilisé pour la suppression sélective qui garde
    /// certains sous-dossiers (`assets/` typiquement).
    ///
    /// Implémentation WASM : envoie `BridgeRequest::RemoveInProject` ; le
    /// bridge vérifie `project_root/project.ron` + absence de traversal.
    /// Implémentation desktop : effectue les mêmes vérifications puis
    /// `remove_dir_all` ou `remove_file` via `std::fs`.
    fn remove_in_project(&self, project_root: &str, path: &str) -> Result<(), String> {
        // Impl par défaut utilisant `remove_dir_all` pour compat arrière —
        // override dans BridgeFs/BridgeFsWeb/NativeFs pour la bonne sémantique.
        let _ = project_root;
        self.remove_dir_all(path)
    }

    /// Demande un listing de répertoire de manière asynchrone (web).
    /// En desktop, ne fait rien (`list_dir` est synchrone).
    /// En WASM, envoie une requête WebSocket — le résultat sera
    /// disponible via `list_dir` au prochain frame.
    fn request_list_dir(&self, _path: &str) {}

    /// Demande le chargement d'un fichier de manière asynchrone (web).
    fn request_read_file(&self, _path: &str) {}

    /// Demande au bridge la résolution canonique d'un chemin (absolu, nettoyé).
    /// En WASM, envoie une requête WebSocket — le résultat est récupéré via
    /// [`Self::canonical_path`]. En desktop, peut résoudre direct.
    fn request_canonicalize(&self, _path: &str) {}

    /// Retourne le chemin absolu canonique d'un chemin donné, si disponible
    /// en cache (web). En desktop, peut résoudre via `std::fs::canonicalize`.
    fn canonical_path(&self, _path: &str) -> Option<String> { None }

    /// Récupère les messages de build en attente (web).
    /// Retourne (stderr, finished, success, pid) pour chaque message.
    fn drain_build_messages(&self) -> Vec<(String, bool, Option<bool>, Option<u32>)> { vec![] }

    /// Envoie une requête de build au bridge (web).
    fn request_cargo_build(&self, _manifest_dir: &str, _args: &[String]) {}

    /// Envoie une requête d'exécution au bridge (web).
    fn request_cargo_run(&self, _manifest_dir: &str, _args: &[String]) {}

    /// Envoie une requête kill au bridge (web).
    fn request_kill_process(&self, _pid: u32) {}

    /// Envoie une requête `cargo apk build` au bridge (web). `signing` permet de patcher
    /// la section de signature du Cargo.toml avant le build (release uniquement).
    fn request_cargo_apk_build(
        &self,
        _project_root: &str,
        _release: bool,
        _signing: Option<crate::bridge_protocol::ApkSigning>,
    ) {}

    /// Envoie une commande `adb` au bridge (web).
    fn request_adb_command(&self, _adb_exe: &str, _args: &[String]) {}

    /// Demande au bridge le chemin du dernier APK produit. Le résultat arrive via
    /// `drain_apk_paths()` au prochain frame (web).
    fn request_find_last_apk(&self, _project_root: &str, _release: bool) {}

    /// Récupère les chemins APK reçus du bridge depuis le dernier drain (web).
    fn drain_apk_paths(&self) -> Vec<String> { vec![] }

    /// Écrit `data` uniquement dans le cache en mémoire (web) — **ne propage PAS**
    /// au bridge ni au disque. Utile pour les ressources virtuelles (langues custom
    /// chargées via file input) qu'on veut exposer à l'éditeur sans polluer le
    /// filesystem utilisateur. En desktop, fallback sur `write_file` normal.
    fn write_file_cache_only(&self, path: &str, data: &[u8]) -> Result<(), String> {
        self.write_file(path, data)
    }

    /// Retire une entrée du cache en mémoire (web) — pendant du
    /// `write_file_cache_only`. Supprime `files[path]`, `exists[path]` et
    /// l'entrée correspondante dans `dirs[parent]`. No-op en desktop (le
    /// fichier est persistant — utiliser un delete disque explicite si besoin).
    fn remove_cache_file(&self, _path: &str) -> Result<(), String> {
        Ok(())
    }

    /// Demande au bridge de détecter `adb` + lister les devices. Le résultat
    /// arrive via `drain_adb_info()` au prochain frame (web).
    fn request_detect_adb(&self) {}

    /// Récupère les réponses `DetectAdb` reçues depuis le dernier drain (web).
    /// Chaque tuple : `(adb_path, devices)`.
    fn drain_adb_info(&self) -> Vec<(String, Vec<crate::bridge_protocol::AdbDevice>)> { vec![] }

    /// Ouvre un dialogue d'import de fichier (web : HTML file input).
    /// Le fichier sera disponible via `drain_imported_files()` au prochain frame.
    fn request_file_import(&self, _dest_dir: &str) {}

    /// Signale que le bouton Import est survolé (web : contournement Firefox).
    /// Doit être appelé chaque frame avec l'état de survol du bouton.
    /// En desktop, no-op.
    fn on_import_hover(&self, _dest_dir: &str, _hovered: bool) {}

    /// Récupère les fichiers importés en attente : (nom_fichier, dest_dir, contenu).
    fn drain_imported_files(&self) -> Vec<(String, String, Vec<u8>)> { vec![] }

    /// Demande au bridge de générer le code du projet jeu (codegen).
    fn request_codegen_export(&self, _scene_path: &str, _output_dir: &str, _project_name: &str,
        _assets_source: Option<&str>, _san_debug_path: &str, _icon_path: Option<&str>, _lang: &str) {}

    /// Récupère les `ApiCall` en attente reçus du bridge (web).
    /// Chaque tuple : `(request_id, method, path, body)`.
    fn drain_pending_api_calls(&self) -> Vec<(u64, String, String, String)> { vec![] }

    /// Envoie une `BridgeResponse` sérialisée JSON vers le bridge (web).
    /// Utilisé pour renvoyer les `ApiResult` après traitement local.
    fn send_bridge_response_json(&self, _json: &str) {}

    /// Indique si le transport distant est disponible. Par défaut `true` (desktop natif).
    /// En web, retourne `false` quand la WebSocket vers san-bridge est fermée ou en erreur —
    /// permet aux panneaux d'arrêter leur boucle de retry et d'afficher une erreur claire.
    fn is_connected(&self) -> bool { true }
}

/// Implémentation native via `std::fs` (desktop).
pub struct NativeFs;

impl FileSystem for NativeFs {
    fn read_file(&self, path: &str) -> Result<Vec<u8>, String> {
        std::fs::read(path).map_err(|e| format!("{path}: {e}"))
    }

    fn read_file_string(&self, path: &str) -> Result<String, String> {
        std::fs::read_to_string(path).map_err(|e| format!("{path}: {e}"))
    }

    fn write_file(&self, path: &str, data: &[u8]) -> Result<(), String> {
        std::fs::write(path, data).map_err(|e| format!("{path}: {e}"))
    }

    fn list_dir(&self, path: &str) -> Result<Vec<FsDirEntry>, String> {
        let rd = std::fs::read_dir(path).map_err(|e| format!("{path}: {e}"))?;
        let mut entries = Vec::new();
        for entry in rd.flatten() {
            let meta = entry.metadata();
            entries.push(FsDirEntry {
                name: entry.file_name().to_string_lossy().into_owned(),
                is_dir: meta.as_ref().map(|m| m.is_dir()).unwrap_or(false),
                size: meta.as_ref().map(|m| m.len()).unwrap_or(0),
            });
        }
        Ok(entries)
    }

    fn create_dir_all(&self, path: &str) -> Result<(), String> {
        std::fs::create_dir_all(path).map_err(|e| format!("{path}: {e}"))
    }

    fn copy_file(&self, src: &str, dst: &str) -> Result<(), String> {
        // Créer les répertoires parents de la destination
        if let Some(parent) = std::path::Path::new(dst).parent() {
            if !parent.exists() {
                std::fs::create_dir_all(parent)
                    .map_err(|e| format!("mkdir {}: {e}", parent.display()))?;
            }
        }
        std::fs::copy(src, dst)
            .map(|_| ())
            .map_err(|e| format!("{src} → {dst}: {e}"))
    }

    fn exists(&self, path: &str) -> bool {
        std::path::Path::new(path).exists()
    }

    fn remove_dir_all(&self, path: &str) -> Result<(), String> {
        std::fs::remove_dir_all(path).map_err(|e| format!("{path}: {e}"))
    }

    fn remove_in_project(&self, project_root: &str, path: &str) -> Result<(), String> {
        use std::path::Path;
        let root = Path::new(project_root);
        let target = Path::new(path);
        if !root.join("project.ron").exists() {
            return Err(format!("RemoveInProject: '{project_root}/project.ron' missing"));
        }
        let can_root = root.canonicalize().map_err(|e| format!("canonicalize root: {e}"))?;
        if !target.exists() {
            return Err(format!("RemoveInProject: '{path}' does not exist"));
        }
        let can_target = target.canonicalize().map_err(|e| format!("canonicalize target: {e}"))?;
        if can_target == can_root {
            return Err("RemoveInProject: use remove_dir_all to delete the project".into());
        }
        if !can_target.starts_with(&can_root) {
            return Err(format!("RemoveInProject: '{path}' outside project '{project_root}'"));
        }
        if can_target.is_dir() {
            std::fs::remove_dir_all(&can_target).map_err(|e| format!("{path}: {e}"))
        } else {
            std::fs::remove_file(&can_target).map_err(|e| format!("{path}: {e}"))
        }
    }

    fn canonical_path(&self, path: &str) -> Option<String> {
        std::fs::canonicalize(path).ok().map(|p| {
            let s = p.to_string_lossy().replace('\\', "/");
            s.strip_prefix("//?/").map(String::from).unwrap_or(s)
        })
    }
}
