//! Public API exposed to Dart via `flutter_rust_bridge`.
//!
//! Naming follows the FRB v2 convention: free functions at the crate
//! API root, with `#[frb(sync)]` for synchronous calls and async fn
//! signatures for async ones. Every input and output is an owned
//! type whose layout is schema-controlled (no `tsclientlib`, no
//! `cpal`, no `Connection` handles).

use std::sync::OnceLock;
use std::time::Duration;

use flutter_rust_bridge::frb;
use tokio::runtime::Runtime;
use tracing::{info, warn};

use crate::frb_generated::StreamSink;
use crate::BridgeError;

/// Process-wide tokio runtime used to drive the async core. Created
/// lazily on first use and never torn down — the application's
/// process lifetime is the runtime's lifetime.
fn runtime() -> &'static Runtime {
    static RT: OnceLock<Runtime> = OnceLock::new();
    RT.get_or_init(|| {
        tokio::runtime::Builder::new_multi_thread()
            .worker_threads(2)
            .enable_all()
            .thread_name("chanora-rt")
            .build()
            .expect("tokio runtime")
    })
}

/// Process-wide session handle. One instance per process is enough
/// for Alpha (DEC-006 single-connection invariant); the Mutex inside
/// `ChanoraSession` enforces the connection-count invariant.
fn session() -> &'static chanora_core::ChanoraSession {
    static S: OnceLock<chanora_core::ChanoraSession> = OnceLock::new();
    S.get_or_init(chanora_core::ChanoraSession::new)
}

/// Process-wide redacted-log sink. Captures the last N tracing
/// records (already redacted) so the user-initiated diagnostic
/// export has something to ship. Lazily created on first access.
fn log_sink() -> &'static chanora_core::InMemoryLogSink {
    static SINK: OnceLock<chanora_core::InMemoryLogSink> = OnceLock::new();
    SINK.get_or_init(|| {
        chanora_core::InMemoryLogSink::new(500, chanora_core::Redactor::with_default_policy())
    })
}

// ---------- Bridge lifecycle ----------

/// Default tracing filter. Suppresses the chatty
/// `tsproto::resend` and `tsproto::packet_codec` paths that
/// flood the diagnostic export during transient packet loss;
/// users can still raise verbosity via `RUST_LOG=info`.
const DEFAULT_LOG_FILTER: &str = "info,tsproto::resend=error,tsproto::packet_codec=error";

/// Initialise the bridge. Must be called once on Dart side before
/// any other API call. Sets up panic logging.
#[frb(init)]
pub fn bridge_init() {
    flutter_rust_bridge::setup_default_user_utils();

    // Always install the redacted in-memory log sink — diagnostic
    // export depends on it (DEC-016: user-initiated only, never
    // auto-upload). It runs alongside whatever platform sink
    // exists below; both consume the same `tracing` events.
    let redact_layer = chanora_core::RedactingLogLayer::new(log_sink().clone());

    // On Android, also fan tracing output out to logcat so a user can
    // see protocol/audio diagnostics via `adb logcat -s chanora`.
    #[cfg(target_os = "android")]
    {
        use tracing_subscriber::layer::SubscriberExt;
        use tracing_subscriber::util::SubscriberInitExt;
        let android_layer = tracing_android::layer("chanora").ok();
        let filter = tracing_subscriber::EnvFilter::try_from_default_env()
            .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(DEFAULT_LOG_FILTER));
        let _ = tracing_subscriber::registry()
            .with(filter)
            .with(android_layer)
            .with(redact_layer)
            .try_init();
    }

    #[cfg(not(target_os = "android"))]
    {
        use tracing_subscriber::layer::SubscriberExt;
        use tracing_subscriber::util::SubscriberInitExt;
        let fmt_layer = tracing_subscriber::fmt::layer().with_target(true);
        let filter = tracing_subscriber::EnvFilter::try_from_default_env()
            .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(DEFAULT_LOG_FILTER));
        let _ = tracing_subscriber::registry()
            .with(filter)
            .with(fmt_layer)
            .with(redact_layer)
            .try_init();
    }

    info!(target: "chanora_bridge", "bridge initialised");
}

// ---------- DTOs ----------

/// Channel as seen by Dart. Matches `chanora_protocol::ChannelInfo`
/// but with primitive `u64` ids so the Dart side gets `BigInt`s
/// without any wrapper-type ceremony.
#[derive(Debug, Clone)]
pub struct BridgeChannel {
    /// Stable channel id.
    pub id: u64,
    /// Parent channel id; 0 means top-level.
    pub parent: u64,
    /// Display name.
    pub name: String,
    /// Server-side ordering hint.
    pub order: i64,
}

/// Client as seen by Dart.
#[derive(Debug, Clone)]
pub struct BridgeClient {
    /// Stable client id.
    pub id: u64,
    /// Channel id the client is currently in.
    pub channel: u64,
    /// Nickname.
    pub name: String,
}

/// Server snapshot as seen by Dart.
#[derive(Debug, Clone)]
pub struct BridgeSnapshot {
    /// Server name.
    pub server_name: String,
    /// Welcome banner text.
    pub welcome_message: String,
    /// Server platform (e.g. "Linux").
    pub platform: String,
    /// Server version string.
    pub version: String,
    /// Channels currently known.
    pub channels: Vec<BridgeChannel>,
    /// Clients currently known.
    pub clients: Vec<BridgeClient>,
}

impl From<chanora_protocol::ServerSnapshot> for BridgeSnapshot {
    fn from(s: chanora_protocol::ServerSnapshot) -> Self {
        Self {
            server_name: s.server_name,
            welcome_message: s.welcome_message,
            platform: s.platform,
            version: s.version,
            channels: s
                .channels
                .into_iter()
                .map(|c| BridgeChannel {
                    id: c.id.0,
                    parent: c.parent.0,
                    name: c.name,
                    order: c.order,
                })
                .collect(),
            clients: s
                .clients
                .into_iter()
                .map(|c| BridgeClient {
                    id: c.id.0,
                    channel: c.channel.0,
                    name: c.name,
                })
                .collect(),
        }
    }
}

// ---------- Commands ----------

/// Connect to a TeamSpeak-compatible server and return the initial
/// state snapshot. Honours the DEC-006 single-connection invariant
/// via [`BridgeError::AlreadyConnected`].
///
/// `password` is optional — pass an empty string for servers that
/// don't require one.
pub async fn connect(
    host: String,
    nickname: String,
    password: String,
) -> Result<BridgeSnapshot, BridgeError> {
    let cfg = chanora_core::ConnectConfig {
        address: host,
        nickname,
        password: if password.is_empty() { None } else { Some(password) },
        identity: None,
        ready_timeout: Duration::from_secs(15),
    };
    let snap = runtime()
        .spawn(async move { session().connect(cfg).await })
        .await
        .map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??;
    Ok(snap.into())
}

/// Re-fetch a fresh snapshot from the active connection.
pub async fn snapshot() -> Result<BridgeSnapshot, BridgeError> {
    let snap = runtime()
        .spawn(async { session().snapshot().await })
        .await
        .map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??;
    Ok(snap.into())
}

/// Disconnect from the server. No-op if not connected.
pub async fn disconnect() -> Result<(), BridgeError> {
    runtime()
        .spawn(async { session().disconnect().await })
        .await
        .map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??;
    Ok(())
}

/// True if a connection is currently active.
pub async fn is_connected() -> bool {
    runtime()
        .spawn(async { session().is_connected().await })
        .await
        .unwrap_or(false)
}

// ---------- Audio commands (Beta) ----------

/// Start the audio engine on the active connection. Requires a
/// connection; idempotent (will replace any previous engine).
pub async fn start_audio() -> Result<(), BridgeError> {
    runtime()
        .spawn(async {
            session()
                .start_audio(chanora_core::AudioEngineConfig::default())
                .await
        })
        .await
        .map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??;
    Ok(())
}

/// Set the push-to-talk state.
pub async fn set_ptt(active: bool) -> Result<(), BridgeError> {
    runtime()
        .spawn(async move { session().set_ptt(active).await })
        .await
        .map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??;
    Ok(())
}

/// Coarse PTT input class (gen2 v0.9.3 / DEC-026). Stable strings;
/// the bridge never carries raw key codes.
#[derive(Debug, Clone, Copy)]
pub enum BridgePttInputClass {
    /// No binding is active.
    None,
    /// A keyboard key.
    Keyboard,
    /// A mouse side button (Mouse4 / Mouse5).
    MouseSideButton,
}

impl From<BridgePttInputClass> for chanora_core::PttInputClass {
    fn from(c: BridgePttInputClass) -> Self {
        match c {
            BridgePttInputClass::None => chanora_core::PttInputClass::None,
            BridgePttInputClass::Keyboard => chanora_core::PttInputClass::Keyboard,
            BridgePttInputClass::MouseSideButton => chanora_core::PttInputClass::MouseSideButton,
        }
    }
}

/// Update the active PTT binding (gen2 v0.9.3 / DEC-026). The
/// platform_key string is opaque to the bridge — it identifies the
/// bound key inside the platform backend and never appears in any
/// log record or diagnostic export (DEC-027 enforced at the
/// diagnostics-sanitizer layer).
pub async fn set_ptt_binding(
    input_class: BridgePttInputClass,
    platform_key: String,
) -> Result<(), BridgeError> {
    let binding = chanora_core::PttBinding {
        input_class: input_class.into(),
        platform_key,
    };
    runtime()
        .spawn(async move { session().set_ptt_binding(binding).await })
        .await
        .map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??;
    Ok(())
}

/// Read the current PTT capability descriptor. Returns a
/// `(level, backend_id, bound_input_class)` triple matching the
/// privacy-safe `BridgeEvent::PttCapability` event shape; useful
/// for the initial UI render before the first event arrives.
pub async fn ptt_descriptor() -> (String, String, String) {
    runtime()
        .spawn(async { session().ptt_descriptor().await })
        .await
        .unwrap_or_else(|_| (String::new(), String::new(), String::new()))
}

/// Move our own client to `channel_id`. Optional channel password
/// for password-protected channels — pass an empty string when not
/// required.
pub async fn move_to_channel(channel_id: u64, password: String) -> Result<(), BridgeError> {
    let pw = if password.is_empty() { None } else { Some(password) };
    runtime()
        .spawn(async move { session().move_to_channel(channel_id, pw).await })
        .await
        .map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??;
    Ok(())
}

/// Toggle self input-mute (microphone) on the server. Independent
/// of push-to-talk: a muted client never transmits regardless of
/// PTT state.
pub async fn set_input_muted(muted: bool) -> Result<(), BridgeError> {
    runtime()
        .spawn(async move { session().set_self_muted(Some(muted), None).await })
        .await
        .map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??;
    Ok(())
}

/// Toggle self output-mute (speaker). Mutes locally *and* informs
/// the server. The server uses this for the channel icon next to
/// the client name; the local mute kicks in immediately even
/// before the server acknowledges.
pub async fn set_output_muted(muted: bool) -> Result<(), BridgeError> {
    runtime()
        .spawn(async move { session().set_self_muted(None, Some(muted)).await })
        .await
        .map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??;
    Ok(())
}

/// Set master output gain. `1.0` is unity, `0.0` is silent. Values
/// above `1.0` amplify and can clip downstream. Errors when audio
/// is not started.
pub async fn set_output_gain(gain: f32) -> Result<(), BridgeError> {
    runtime()
        .spawn(async move { session().set_output_gain(gain).await })
        .await
        .map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??;
    Ok(())
}

/// Statistics from the audio engine.
#[derive(Debug, Clone)]
pub struct BridgeAudioStats {
    /// Number of Opus frames sent since audio started.
    pub frames_sent: u32,
    /// Number of inbound voice packets decoded.
    pub frames_received: u32,
    /// Current push-to-talk state.
    pub ptt_active: bool,
}

// ---------- Diagnostics (A.3) ----------

/// User-initiated diagnostic export. Returns a multi-line text
/// blob, redacted per the production policy, that the user can
/// share or copy. DEC-016 forbids automatic uploads — this is the
/// only path that surfaces logs.
#[frb(sync)]
pub fn export_diagnostics() -> String {
    let metadata = vec![
        ("crate_version".to_string(), env!("CARGO_PKG_VERSION").to_string()),
        ("target_os".to_string(), std::env::consts::OS.to_string()),
        ("target_arch".to_string(), std::env::consts::ARCH.to_string()),
    ];
    match chanora_core::DiagnosticExport::from_sink(log_sink(), metadata) {
        Ok(exp) => exp.to_text(),
        Err(e) => format!("(diagnostic export failed: {e})"),
    }
}

// ---------- Storage (A.2) ----------

/// Wire the identity persistence store to a platform-private
/// directory. Should be called once on app start after Flutter has
/// resolved `getApplicationSupportDirectory()` (or equivalent).
///
/// Subsequent [`connect`] calls will reuse the persisted identity,
/// or generate-and-persist a fresh one on first use. This keeps the
/// server-visible UID stable across app restarts.
///
/// Beta caveat: the identity is stored as a plain file (mode 0600
/// on Unix). It is *not* encrypted at rest. RISK-PoC-002 documents
/// this gap; the v0.4 storage rework lands the proper Secret
/// Service + Android Keystore + iOS Keychain backends.
pub async fn init_storage(dir: String) -> Result<(), BridgeError> {
    runtime()
        .spawn(async move { session().init_storage(&dir).await })
        .await
        .map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??;
    Ok(())
}

/// Bookmark DTO mirroring [`chanora_core::Bookmark`].
#[derive(Debug, Clone)]
pub struct BridgeBookmark {
    /// Row id assigned by SQLite. Use `0` when adding new rows;
    /// the returned id is then meaningful.
    pub id: i64,
    /// User-facing label.
    pub display_name: String,
    /// `hostname[:port]` or TSDNS name.
    pub host: String,
    /// Nickname to use for this bookmark.
    pub nickname: String,
    /// Optional remembered password. Empty string = none.
    pub password: String,
}

impl From<chanora_core::Bookmark> for BridgeBookmark {
    fn from(b: chanora_core::Bookmark) -> Self {
        Self {
            id: b.id,
            display_name: b.display_name,
            host: b.host,
            nickname: b.nickname,
            password: b.password.unwrap_or_default(),
        }
    }
}

impl From<BridgeBookmark> for chanora_core::Bookmark {
    fn from(b: BridgeBookmark) -> Self {
        chanora_core::Bookmark {
            id: b.id,
            display_name: b.display_name,
            host: b.host,
            nickname: b.nickname,
            password: if b.password.is_empty() {
                None
            } else {
                Some(b.password)
            },
        }
    }
}

/// List persisted bookmarks.
pub async fn list_bookmarks() -> Result<Vec<BridgeBookmark>, BridgeError> {
    let v = runtime()
        .spawn(async { session().list_bookmarks().await })
        .await
        .map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??;
    Ok(v.into_iter().map(Into::into).collect())
}

/// Insert a bookmark and return its assigned id. The `id` field on
/// the input is ignored.
pub async fn add_bookmark(b: BridgeBookmark) -> Result<i64, BridgeError> {
    let core_b: chanora_core::Bookmark = b.into();
    let id = runtime()
        .spawn(async move { session().add_bookmark(core_b).await })
        .await
        .map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??;
    Ok(id)
}

/// Update an existing bookmark.
pub async fn update_bookmark(b: BridgeBookmark) -> Result<(), BridgeError> {
    let core_b: chanora_core::Bookmark = b.into();
    runtime()
        .spawn(async move { session().update_bookmark(core_b).await })
        .await
        .map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??;
    Ok(())
}

/// Delete a bookmark by id.
pub async fn delete_bookmark(id: i64) -> Result<(), BridgeError> {
    runtime()
        .spawn(async move { session().delete_bookmark(id).await })
        .await
        .map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??;
    Ok(())
}

// ---------- Connectivity (A.6.1) ----------

/// Coarse OS-reported network state. Mirrors
/// [`chanora_core::NetworkState`] across the bridge.
#[derive(Debug, Clone, Copy)]
pub enum BridgeNetworkState {
    /// No signal seen yet.
    Unknown,
    /// OS reports a usable network.
    Online,
    /// OS reports no network.
    Offline,
}

impl From<BridgeNetworkState> for chanora_core::NetworkState {
    fn from(s: BridgeNetworkState) -> Self {
        match s {
            BridgeNetworkState::Unknown => chanora_core::NetworkState::Unknown,
            BridgeNetworkState::Online => chanora_core::NetworkState::Online,
            BridgeNetworkState::Offline => chanora_core::NetworkState::Offline,
        }
    }
}

/// Notify the core of the latest OS-reported connectivity state.
/// Called by the Flutter side from `connectivity_plus` callbacks.
/// The core's supervisor uses this to (a) pre-charge the watchdog
/// on Offline and (b) short-circuit reconnect backoff on Online.
#[frb(sync)]
pub fn set_network_state(state: BridgeNetworkState) {
    session().set_network_state(state.into());
}

// ---------- Events (A.6) ----------

/// Lifecycle event surfaced to Dart. Schema-controlled mirror of
/// [`chanora_core::SessionEvent`] — no core types cross the bridge.
#[derive(Debug, Clone)]
pub enum BridgeEvent {
    /// Initial connect succeeded, or a reconnect attempt succeeded.
    Connected {
        /// Server name reported by the server snapshot.
        server_name: String,
    },
    /// Connection lost; supervisor will retry.
    Lost {
        /// Reason classification from the protocol layer.
        reason: String,
    },
    /// Supervisor is sleeping before its next reconnect attempt.
    Reconnecting {
        /// 1-based attempt counter for the current outage.
        attempt: u32,
        /// Seconds the supervisor will sleep before this attempt.
        delay_secs: u32,
    },
    /// Session ended (user-requested disconnect or unrecoverable).
    Disconnected {
        /// Reason classification.
        reason: String,
    },
    /// Audio engine started.
    AudioStarted,
    /// Audio engine stopped.
    AudioStopped,
    /// Snapshot probe observed a change in channel/client counts.
    /// UI uses this to drive an auto-refresh without active
    /// polling.
    SnapshotChanged {
        /// Latest channel count.
        channels: u32,
        /// Latest client count.
        clients: u32,
    },
    /// Detected desktop Push-to-Talk capability (gen2 v0.9.3,
    /// DEC-023..028). The fields carry only privacy-safe values per
    /// DEC-027: the capability level, a stable backend identifier,
    /// and the bound input class. No key codes, scan codes, or
    /// virtual-key values cross this boundary.
    PttCapability {
        /// Stable capability identifier (`"L0Focused"`,
        /// `"L1GlobalShortcut"`, `"L2GlobalHoldToTalk"`,
        /// `"L3GlobalWithMouseButtons"`, or `"L4DeviceAware"`).
        level: String,
        /// Stable backend identifier (e.g. `"focused"`).
        backend_id: String,
        /// Coarse bound input class (e.g. `"keyboard"`,
        /// `"mouse-side-button"`); empty when no binding is active.
        bound_input_class: String,
    },
}

impl From<chanora_core::SessionEvent> for BridgeEvent {
    fn from(e: chanora_core::SessionEvent) -> Self {
        match e {
            chanora_core::SessionEvent::Connected { server_name } => {
                BridgeEvent::Connected { server_name }
            }
            chanora_core::SessionEvent::Lost { reason } => BridgeEvent::Lost { reason },
            chanora_core::SessionEvent::Reconnecting {
                attempt,
                delay_secs,
            } => BridgeEvent::Reconnecting {
                attempt,
                delay_secs,
            },
            chanora_core::SessionEvent::Disconnected { reason } => {
                BridgeEvent::Disconnected { reason }
            }
            chanora_core::SessionEvent::AudioStarted => BridgeEvent::AudioStarted,
            chanora_core::SessionEvent::AudioStopped => BridgeEvent::AudioStopped,
            chanora_core::SessionEvent::SnapshotChanged { channels, clients } => {
                BridgeEvent::SnapshotChanged { channels, clients }
            }
            chanora_core::SessionEvent::PttCapability {
                level,
                backend_id,
                bound_input_class,
            } => BridgeEvent::PttCapability {
                level,
                backend_id,
                bound_input_class,
            },
        }
    }
}

/// Subscribe to lifecycle events. Each call yields a fresh
/// subscription; multiple subscribers are supported. On slow
/// consumers, events are dropped rather than blocking the supervisor
/// (consistent with `tokio::sync::broadcast::Receiver` semantics).
pub fn events_stream(sink: StreamSink<BridgeEvent>) -> Result<(), BridgeError> {
    let mut rx = session().subscribe_events();
    runtime().spawn(async move {
        loop {
            match rx.recv().await {
                Ok(evt) => {
                    if sink.add(BridgeEvent::from(evt)).is_err() {
                        // Dart side closed the sink — stop the bridge task.
                        info!(target: "chanora_bridge", "events_stream: dart sink closed");
                        return;
                    }
                }
                Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => {
                    warn!(target: "chanora_bridge", "events_stream: lagged, dropped {n} events");
                    continue;
                }
                Err(tokio::sync::broadcast::error::RecvError::Closed) => {
                    info!(target: "chanora_bridge", "events_stream: source closed");
                    return;
                }
            }
        }
    });
    Ok(())
}

/// Read audio statistics. Errors if no connection or audio not started.
pub async fn audio_stats() -> Result<BridgeAudioStats, BridgeError> {
    let (s, r, p) = runtime()
        .spawn(async { session().audio_stats().await })
        .await
        .map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??;
    Ok(BridgeAudioStats {
        frames_sent: s,
        frames_received: r,
        ptt_active: p,
    })
}
