//! Chanora PoC: tsclientlib connect spike.
//!
//! Purpose (per `docs/architecture/proof-of-concept-plan.md` §2):
//!     Prove that a Rust process can connect to a TeamSpeak-3-compatible
//!     server through `tsclientlib` and observe the server state.
//!
//! Exit criterion: "Rust can connect to a compatible server/test double."
//!
//! This is PoC code. It deliberately:
//!   * does not capture or play audio;
//!   * does not expose any FFI surface to Flutter;
//!   * does not use the Chanora architecture layers (chanora_protocol etc.);
//!   * generates a fresh identity per run unless one is supplied.
//!
//! It is not promoted to product code unless explicitly migrated per
//! `docs/architecture/proof-of-concept-plan.md` §4.

use std::time::Duration;

use anyhow::{Context, Result};
use clap::Parser;
use futures::prelude::*;
use tokio::time;
use tracing::{info, warn};
use tracing_subscriber::EnvFilter;

use tsclientlib::data::{self, Channel, Client};
use tsclientlib::{ChannelId, Connection, DisconnectOptions, Identity, OutCommandExt, StreamItem};

#[derive(Parser, Debug)]
#[command(
    name = "tsclientlib-connect-spike",
    about = "Chanora PoC: connect to a TS3-compatible server and dump channel tree."
)]
struct Args {
    /// Server address (hostname[:port] or TSDNS name).
    #[arg(short, long, default_value = "cn.teamspeak.app")]
    address: String,

    /// Nickname used on the server.
    #[arg(short, long, default_value = "ChanoraPoC")]
    nickname: String,

    /// Seconds to remain connected before disconnecting cleanly.
    #[arg(long, default_value_t = 3)]
    hold_secs: u64,

    /// Optional base64 identity string. If omitted, a fresh identity is generated.
    #[arg(long)]
    identity: Option<String>,

    /// Server password, if the server requires one.
    #[arg(long)]
    password: Option<String>,

    /// Increase log verbosity (-v, -vv, -vvv).
    #[arg(short, long, action = clap::ArgAction::Count)]
    verbose: u8,
}

#[tokio::main]
async fn main() -> Result<()> {
    let args = Args::parse();

    // Default to info-level logs; respect RUST_LOG if set.
    let filter = EnvFilter::try_from_default_env()
        .unwrap_or_else(|_| EnvFilter::new("info,tsproto=warn,tsclientlib=warn"));
    tracing_subscriber::fmt().with_env_filter(filter).init();

    info!(target: "spike", address = %args.address, nickname = %args.nickname, "starting connect spike");

    // Build connection config.
    let mut builder = Connection::build(args.address.clone())
        .name(args.nickname.clone())
        .log_commands(args.verbose >= 1)
        .log_packets(args.verbose >= 2)
        .log_udp_packets(args.verbose >= 3);

    // Identity: provided or freshly generated.
    let identity = match &args.identity {
        Some(s) => Identity::new_from_str(s).context("parsing --identity")?,
        None => {
            // Generate a level-0 identity. For a real product we would persist this
            // via SecureStorage (see docs/security/secure-storage-audit-report.md).
            // PoC: ephemeral per run.
            Identity::create()
        }
    };
    builder = builder.identity(identity);

    if let Some(pw) = &args.password {
        builder = builder.password(pw.clone());
    }

    // Connect.
    let mut con = builder.connect().context("dialling server")?;

    // Wait for the initial book/state snapshot (BookEvents == state ready).
    let first = con
        .events()
        .try_filter(|e| future::ready(matches!(e, StreamItem::BookEvents(_))))
        .next()
        .await;
    match first {
        Some(Ok(_)) => info!(target: "spike", "received initial state snapshot"),
        Some(Err(e)) => {
            anyhow::bail!("connection failed before state ready: {e}");
        }
        None => {
            anyhow::bail!("event stream ended before state ready");
        }
    }

    // Subscribe to the server so we get the full channel tree, not just our own.
    if let Ok(state) = con.get_state() {
        if let Err(e) = state.server.set_subscribed(true).send(&mut con) {
            warn!(target: "spike", error = %e, "could not subscribe to server tree");
        }
    }

    // Give the server a moment to ship the tree, but bail out early if the
    // connection disconnects in the meantime.
    let mut bg = con.events().try_filter(|_| future::ready(false));
    tokio::select! {
        _ = time::sleep(Duration::from_secs(args.hold_secs.min(60))) => {}
        ev = bg.next() => {
            if let Some(Err(e)) = ev {
                anyhow::bail!("disconnected during hold window: {e}");
            }
        }
    }
    drop(bg);

    // Print snapshot.
    print_snapshot(&*con.get_state().context("reading server state")?);

    // Clean disconnect.
    con.disconnect(DisconnectOptions::new()).ok();
    con.events().for_each(|_| future::ready(())).await;

    info!(target: "spike", "spike completed successfully");
    Ok(())
}

fn print_snapshot(con: &data::Connection) {
    println!();
    println!("=== Chanora tsclientlib connect spike — server snapshot ===");
    println!("Server name    : {}", sanitize(&con.server.name));
    println!("Welcome msg    : {}", sanitize(&con.server.welcome_message));
    println!("Platform       : {}", sanitize(&con.server.platform));
    println!("Version        : {}", sanitize(&con.server.version));
    println!("Clients online : {}", con.clients.len());
    println!("Channels       : {}", con.channels.len());
    println!();

    let mut channels: Vec<&Channel> = con.channels.values().collect();
    channels.sort_by_key(|c| c.order.0);
    let clients: Vec<&Client> = con.clients.values().collect();

    println!("Channel tree:");
    print_children(&clients, &channels, ChannelId(0), 0);
    println!();
}

fn print_children(clients: &[&Client], channels: &[&Channel], parent: ChannelId, depth: usize) {
    let indent = "  ".repeat(depth);
    for ch in channels {
        if ch.parent == parent {
            println!("{}- [{}] {}", indent, ch.id.0, sanitize(&ch.name));
            for client in clients {
                if client.channel == ch.id {
                    println!("{}    * {}", indent, sanitize(&client.name));
                }
            }
            print_children(clients, channels, ch.id, depth + 1);
        }
    }
}

/// Restrict to a safe printable set; mirrors the upstream `simple.rs` example.
/// This is *not* the Chanora redaction policy — see
/// `docs/security/diagnostic-redaction-audit-report.md`.
fn sanitize(s: &str) -> String {
    s.chars()
        .filter(|c| {
            c.is_alphanumeric()
                || [
                    ' ', '\t', '.', ':', '-', '_', '"', '\'', '/', '(', ')', '[', ']', '{', '}',
                    '!', '?', ',', '#', '+', '*',
                ]
                .contains(c)
        })
        .collect()
}
