//! Protocole de communication entre l'éditeur (web/WASM) et le bridge local.
//!
//! Tous les messages transitent en JSON via WebSocket.
//!
//! ## Versionnage
//!
//! `BRIDGE_PROTOCOL_VERSION` est incrémenté à chaque **ajout de variant** dans
//! `BridgeRequest`/`BridgeResponse` ou changement cassant. L'éditeur requiert
//! au minimum `BRIDGE_MIN_PROTOCOL_VERSION` ; un bridge plus ancien est refusé.

use serde::{Deserialize, Serialize};

/// Version courante du protocole, retournée par le bridge via `Version`.
/// À bumper à chaque changement du protocole.
pub const BRIDGE_PROTOCOL_VERSION: u32 = 8;

fn default_lang() -> String { "en".to_string() }

/// Version minimale du protocole acceptée par l'éditeur.
/// Un bridge qui répond une version < à celle-ci doit être mis à jour.
///
/// v5 : `WriteFile.data` et `FileContent.data` encodés en **base64** au lieu
/// de JSON-array de u8. Contourne le bug `serde` `TaggedContentVisitor` qui
/// explose en mémoire sur gros payloads (découverte : .ply 50 Mo → panic
/// `capacity_overflow` dans `raw_vec::grow_one` côté éditeur WASM).
pub const BRIDGE_MIN_PROTOCOL_VERSION: u32 = 8;

/// Helpers serde pour (dé)sérialiser `Vec<u8>` en **base64** au lieu de
/// JSON-array. Indispensable quand les payloads peuvent être gros (assets
/// binaires), car `#[serde(tag = "type")]` utilise `TaggedContentVisitor`
/// qui bufferise la séquence entière en mémoire (~40 octets par byte d'origine).
mod base64_bytes {
    use base64::{engine::general_purpose::STANDARD, Engine};
    use serde::{Deserialize, Deserializer, Serializer};

    pub fn serialize<S: Serializer>(bytes: &Vec<u8>, s: S) -> Result<S::Ok, S::Error> {
        s.serialize_str(&STANDARD.encode(bytes))
    }

    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
        let s = String::deserialize(d)?;
        STANDARD.decode(s.as_bytes()).map_err(serde::de::Error::custom)
    }
}

/// Configuration de signature pour un build APK release.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApkSigning {
    pub keystore_path: String,
    pub keystore_password: String,
    pub key_alias: String,
    pub key_password: String,
}

/// Un appareil détecté par `adb devices -l`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdbDevice {
    /// Identifiant du device : soit un serial USB (ex. `"R3CT904..."`),
    /// soit `"IP:PORT"` pour un device en WiFi debugging (ex. `"192.168.1.42:5555"`).
    pub serial: String,
    /// État : `"device"` = prêt, `"offline"`, `"unauthorized"`, etc.
    pub state: String,
    /// Modèle si fourni par `-l` (ex. `"SM_G998B"`).
    pub model: String,
    /// `true` si le serial ressemble à `IP:PORT` (WiFi debugging).
    pub is_wifi: bool,
}

/// Requête envoyée par l'éditeur au bridge.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum BridgeRequest {
    /// Test de connectivité.
    Ping,

    /// Demander la version du bridge (binaire + protocole).
    /// Un vieux bridge qui ne connaît pas ce variant renverra une `Error`
    /// de désérialisation — l'éditeur interprète ça comme "bridge obsolète".
    Version,

    /// Lire le contenu d'un fichier.
    ReadFile { path: String },

    /// Écrire du contenu dans un fichier (crée le fichier si absent).
    WriteFile {
        path: String,
        #[serde(with = "base64_bytes")]
        data: Vec<u8>,
    },

    /// Lister le contenu d'un répertoire.
    ListDir { path: String },

    /// Créer un répertoire (et ses parents).
    CreateDir { path: String },

    /// Supprime récursivement un répertoire (et son contenu).
    /// **Garde-fou côté bridge** : refuse si le dossier ne contient pas un
    /// fichier `project.ron` à sa racine — empêche une erreur utilisateur
    /// d'effacer un dossier système par inadvertance.
    RemoveDir { path: String },

    /// Supprime un chemin (fichier ou dossier) **à l'intérieur** d'un projet
    /// SanBevyEditor (item feedback #31). Utilisé pour la suppression
    /// sélective qui garde certains sous-dossiers.
    ///
    /// Garde-fous côté bridge :
    /// - `project_root/project.ron` doit exister.
    /// - `path` doit être un sous-chemin strict de `project_root` (pas de
    ///   traversal via `..`).
    /// - `project_root` != `path` (utiliser `RemoveDir` pour wiper complet).
    RemoveInProject { project_root: String, path: String },

    /// Copier un fichier.
    CopyFile { src: String, dst: String },

    /// Vérifier si un chemin existe.
    Exists { path: String },

    /// Résout un chemin en chemin absolu canonique (depuis le cwd du bridge).
    /// Utile pour afficher à l'utilisateur où il se situe quand il navigue
    /// avec "." ou des chemins relatifs.
    Canonicalize { path: String },

    /// Surveiller un répertoire pour les changements.
    WatchDir { path: String },

    /// Arrêter la surveillance d'un répertoire.
    UnwatchDir { path: String },

    /// Lancer `cargo build` dans un répertoire.
    CargoBuild {
        manifest_dir: String,
        args: Vec<String>,
    },

    /// Lancer `cargo run` ou un exécutable.
    CargoRun {
        manifest_dir: String,
        args: Vec<String>,
    },

    /// Tuer un processus lancé par le bridge.
    KillProcess { pid: u32 },

    /// Arrêter le bridge.
    Shutdown,

    /// Générer le code du projet jeu (codegen).
    CodegenExport {
        scene_path: String,
        output_dir: String,
        project_name: String,
        assets_source: Option<String>,
        san_debug_path: String,
        icon_path: Option<String>,
        /// Langue des en-têtes et README du projet généré (`fr`, `en`, `vi`).
        #[serde(default = "default_lang")]
        lang: String,
    },

    /// Vérifier/créer le keystore Android debug standard.
    /// Le bridge garantit que `~/.android/debug.keystore` existe (génère via
    /// `keytool` si absent) et répond avec le chemin absolu.
    EnsureAndroidDebugKeystore,

    /// Lancer `cargo apk build [--release]` dans `project_root`.
    /// Si `signing` est fourni, patche le Cargo.toml avec la config avant le build.
    /// Stream la sortie via `BuildOutput` comme `CargoBuild`.
    CargoApkBuild {
        project_root: String,
        release: bool,
        signing: Option<ApkSigning>,
    },

    /// Exécuter une commande `adb` (pair, connect, install, logcat, etc.).
    /// `adb_exe` vide = utilise `adb` dans le PATH. Stream la sortie via `BuildOutput`.
    AdbCommand {
        adb_exe: String,
        args: Vec<String>,
    },

    /// Vérifier si un fichier APK a été produit par le dernier build (parcourt
    /// `target/{debug,release}/apk/*.apk` dans `project_root`).
    FindLastApk {
        project_root: String,
        release: bool,
    },

    /// Chercher `adb.exe` aux emplacements standards (Android Studio SDK,
    /// `ANDROID_HOME`, `PATH`) et lister les devices actuellement connectés.
    /// Réponse : `AdbInfo { adb_path, devices }`.
    DetectAdb,

    /// Appel API REST à traiter par l'éditeur WASM (proxy HTTP→WebSocket).
    /// Le bridge reçoit la requête HTTP, la transmet via ce message, puis
    /// attend la réponse `ApiResult` avec le même `request_id`.
    ApiCall {
        request_id: u64,
        method: String,
        path: String,
        body: String,
    },
}

/// Réponse envoyée par le bridge à l'éditeur.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum BridgeResponse {
    /// Réponse au Ping.
    Pong,

    /// Version du bridge — réponse à `BridgeRequest::Version`.
    Version {
        /// Version du binaire san-bridge (`CARGO_PKG_VERSION`).
        bridge_version: String,
        /// Version du protocole (cf. `BRIDGE_PROTOCOL_VERSION`).
        protocol_version: u32,
    },

    /// Opération réussie sans données.
    Ok,

    /// Erreur avec message.
    Error { message: String },

    /// Contenu d'un fichier.
    FileContent {
        #[serde(with = "base64_bytes")]
        data: Vec<u8>,
    },

    /// Contenu d'un répertoire.
    DirListing { entries: Vec<DirEntry> },

    /// Résultat d'un test d'existence.
    ExistsResult { exists: bool },

    /// Chemin absolu canonique d'un chemin donné (réponse à `Canonicalize`).
    /// `input` est renvoyé tel que demandé pour que le client corrèle la
    /// réponse à sa requête (file asynchrone).
    CanonicalPath { input: String, resolved: String },

    /// Chemin absolu d'un keystore (réponse à `EnsureAndroidDebugKeystore`).
    KeystorePath { path: String },

    /// Chemin absolu d'un APK (réponse à `FindLastApk`).
    /// `path` vide = pas d'APK trouvé.
    ApkPath { path: String },

    /// Chemin d'`adb` détecté + devices connectés (réponse à `DetectAdb`).
    /// `adb_path` vide = adb introuvable.
    AdbInfo {
        adb_path: String,
        devices: Vec<AdbDevice>,
    },

    /// Notification de changement de fichier (push asynchrone).
    FileChanged {
        path: String,
        kind: FileChangeKind,
    },

    /// Sortie d'un processus de build (streaming).
    BuildOutput {
        stdout: String,
        stderr: String,
        finished: bool,
        success: Option<bool>,
    },

    /// Un processus a démarré.
    ProcessStarted { pid: u32 },

    /// Résultat d'un `ApiCall` traité par l'éditeur WASM.
    /// Le `request_id` permet au bridge de corréler avec la requête HTTP originelle.
    /// `body` est transmis en base64 pour supporter le binaire (screenshots PNG).
    ApiResult {
        request_id: u64,
        status: u16,
        content_type: String,
        body_base64: String,
    },
}

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

/// Type de changement de fichier.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FileChangeKind {
    Created,
    Modified,
    Removed,
}
