//! Generic **PTY ↔ Tauri Channel** bridge infrastructure.
//!
//! ARCHITECTURE §2 decides that high-frequency PTY byte streams travel over
//! per-session [`tauri::ipc::Channel`]s rather than global events, for
//! throughput and isolation. This module provides the transport-side plumbing
//! that L3 will plug a real `PtyPort` into; here there is **no real PTY** yet —
//! only the registry + the abstraction that routes byte chunks to the right
//! frontend channel.
//!
//! Design:
//! - The frontend opens a terminal and passes a [`tauri::ipc::Channel`] for that
//!   session. The backend registers it in [`PtyBridge`] keyed by `SessionId`.
//! - Whatever produces output (the PTY adapter in L3) calls
//!   [`PtyBridge::send_output`], which forwards the bytes on the matching
//!   channel. Bytes are sent as-is; the frontend xterm wrapper consumes them.
//! - [`PtyBridge::unregister`] tears the channel down on terminal close.

use std::collections::HashMap;
use std::sync::Mutex;

use tauri::ipc::Channel;

use domain::ids::SessionId;

/// A chunk of PTY output bytes destined for a specific session's channel.
///
/// Sent as a raw byte vector; serde encodes it for the IPC channel. Kept as a
/// distinct type so the wire shape can evolve (e.g. add a sequence number for
/// backpressure handling) without touching call sites.
pub type PtyChunk = Vec<u8>;

/// Registry mapping live terminal sessions to their output [`Channel`].
///
/// Thread-safe; cloned `Arc<PtyBridge>` is held in [`crate::state::AppState`].
#[derive(Default)]
pub struct PtyBridge {
    /// Per session: a monotonically-increasing **generation** plus the current
    /// output channel. The generation distinguishes successive (re-)attaches so a
    /// superseded pump thread can't tear down the channel of the attach that
    /// replaced it (see [`PtyBridge::unregister_if`]).
    channels: Mutex<HashMap<SessionId, (u64, Channel<PtyChunk>)>>,
}

impl PtyBridge {
    /// Creates an empty bridge.
    #[must_use]
    pub fn new() -> Self {
        Self {
            channels: Mutex::new(HashMap::new()),
        }
    }

    /// Registers (or replaces) the output channel for a session and returns the
    /// **generation** of this registration. Each call for a session bumps the
    /// generation, so the caller's pump thread can later tear down *only its own*
    /// registration via [`unregister_if`](Self::unregister_if).
    pub fn register(&self, session: SessionId, channel: Channel<PtyChunk>) -> u64 {
        if let Ok(mut map) = self.channels.lock() {
            let gen = map.get(&session).map_or(0, |(g, _)| g.wrapping_add(1));
            map.insert(session, (gen, channel));
            gen
        } else {
            0
        }
    }

    /// Removes a session's channel unconditionally (terminal explicitly closed).
    pub fn unregister(&self, session: &SessionId) {
        if let Ok(mut map) = self.channels.lock() {
            map.remove(session);
        }
    }

    /// Removes a session's channel **only if** `gen` is still the current
    /// generation. A pump thread calls this when its output stream ends: if the
    /// session has since been re-attached (newer generation), this is a no-op, so
    /// the dying thread never unregisters the live channel that superseded it.
    pub fn unregister_if(&self, session: &SessionId, gen: u64) {
        if let Ok(mut map) = self.channels.lock() {
            if matches!(map.get(session), Some((g, _)) if *g == gen) {
                map.remove(session);
            }
        }
    }

    /// Forwards a chunk of output bytes to a session's channel.
    ///
    /// Returns `true` if the chunk was delivered, `false` if no channel is
    /// registered for the session (e.g. already closed). In L3 the PTY adapter's
    /// output stream drives this.
    pub fn send_output(&self, session: &SessionId, chunk: PtyChunk) -> bool {
        let Ok(map) = self.channels.lock() else {
            return false;
        };
        match map.get(session) {
            Some((_, channel)) => channel.send(chunk).is_ok(),
            None => false,
        }
    }

    /// Number of currently-registered sessions (handy for tests/diagnostics).
    #[must_use]
    pub fn active_sessions(&self) -> usize {
        self.channels.lock().map(|m| m.len()).unwrap_or(0)
    }
}
