//! Minimal WebSocket client helpers for requesting metrics from the agent.

use flate2::bufread::GzDecoder;
use futures_util::{SinkExt, StreamExt};
use prost::Message as _;
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
use rustls::{ClientConfig, RootCertStore};
use rustls::{DigitallySignedStruct, SignatureScheme};
use rustls_pemfile::Item;
use std::io::Read;
use std::{fs::File, io::BufReader, sync::Arc};
use tokio::net::TcpStream;
use tokio_tungstenite::{
    connect_async, connect_async_tls_with_config, tungstenite::client::IntoClientRequest,
    tungstenite::Message, Connector, MaybeTlsStream, WebSocketStream,
};
use url::Url;

use crate::types::{DiskInfo, Metrics, ProcessInfo, ProcessesPayload};

mod pb {
    // generated by build.rs
    include!(concat!(env!("OUT_DIR"), "/socktop.rs"));
}

pub type WsStream = WebSocketStream<MaybeTlsStream<TcpStream>>;

// Connect to the agent and return the WS stream
pub async fn connect(
    url: &str,
    tls_ca: Option<&str>,
) -> Result<WsStream, Box<dyn std::error::Error>> {
    let mut u = Url::parse(url)?;
    if let Some(ca_path) = tls_ca {
        if u.scheme() == "ws" {
            let _ = u.set_scheme("wss");
        }
        return connect_with_ca(u.as_str(), ca_path).await;
    }
    let (ws, _) = connect_async(u.as_str()).await?;
    Ok(ws)
}

async fn connect_with_ca(url: &str, ca_path: &str) -> Result<WsStream, Box<dyn std::error::Error>> {
    let mut root = RootCertStore::empty();
    let mut reader = BufReader::new(File::open(ca_path)?);
    let mut der_certs = Vec::new();
    while let Ok(Some(item)) = rustls_pemfile::read_one(&mut reader) {
        if let Item::X509Certificate(der) = item {
            der_certs.push(der);
        }
    }
    root.add_parsable_certificates(der_certs);

    let mut cfg = ClientConfig::builder()
        .with_root_certificates(root)
        .with_no_client_auth();

    let req = url.into_client_request()?;
    let verify_domain = std::env::var("SOCKTOP_VERIFY_NAME").ok().as_deref() == Some("1");
    if !verify_domain {
        #[derive(Debug)]
        struct NoVerify;
        impl ServerCertVerifier for NoVerify {
            fn verify_server_cert(
                &self,
                _end_entity: &CertificateDer<'_>,
                _intermediates: &[CertificateDer<'_>],
                _server_name: &ServerName,
                _ocsp_response: &[u8],
                _now: UnixTime,
            ) -> Result<ServerCertVerified, rustls::Error> {
                Ok(ServerCertVerified::assertion())
            }
            fn verify_tls12_signature(
                &self,
                _message: &[u8],
                _cert: &CertificateDer<'_>,
                _dss: &DigitallySignedStruct,
            ) -> Result<HandshakeSignatureValid, rustls::Error> {
                Ok(HandshakeSignatureValid::assertion())
            }
            fn verify_tls13_signature(
                &self,
                _message: &[u8],
                _cert: &CertificateDer<'_>,
                _dss: &DigitallySignedStruct,
            ) -> Result<HandshakeSignatureValid, rustls::Error> {
                Ok(HandshakeSignatureValid::assertion())
            }
            fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
                // Provide common schemes; not strictly needed for skipping but keeps API happy
                vec![
                    SignatureScheme::ECDSA_NISTP256_SHA256,
                    SignatureScheme::ED25519,
                    SignatureScheme::RSA_PSS_SHA256,
                ]
            }
        }
        cfg.dangerous().set_certificate_verifier(Arc::new(NoVerify));
        eprintln!("socktop: hostname verification disabled (default). Use --verify-hostname to enable strict SAN checking.");
    }
    let cfg = Arc::new(cfg);
    let (ws, _) =
        connect_async_tls_with_config(req, None, verify_domain, Some(Connector::Rustls(cfg)))
            .await?;
    Ok(ws)
}

// Send a "get_metrics" request and await a single JSON reply
pub async fn request_metrics(ws: &mut WsStream) -> Option<Metrics> {
    if ws.send(Message::Text("get_metrics".into())).await.is_err() {
        return None;
    }
    match ws.next().await {
        Some(Ok(Message::Binary(b))) => {
            gunzip_to_string(&b).and_then(|s| serde_json::from_str::<Metrics>(&s).ok())
        }
        Some(Ok(Message::Text(json))) => serde_json::from_str::<Metrics>(&json).ok(),
        _ => None,
    }
}

// Decompress a gzip-compressed binary frame into a String.
fn gunzip_to_string(bytes: &[u8]) -> Option<String> {
    let mut dec = GzDecoder::new(bytes);
    let mut out = String::new();
    dec.read_to_string(&mut out).ok()?;
    Some(out)
}

fn gunzip_to_vec(bytes: &[u8]) -> Option<Vec<u8>> {
    let mut dec = GzDecoder::new(bytes);
    let mut out = Vec::new();
    dec.read_to_end(&mut out).ok()?;
    Some(out)
}

fn is_gzip(bytes: &[u8]) -> bool {
    bytes.len() >= 2 && bytes[0] == 0x1f && bytes[1] == 0x8b
}
// Suppress dead_code until these are wired into the app
#[allow(dead_code)]
pub enum Payload {
    Metrics(Metrics),
    Disks(Vec<DiskInfo>),
    Processes(ProcessesPayload),
}

// Send a "get_disks" request and await a JSON Vec<DiskInfo>
pub async fn request_disks(ws: &mut WsStream) -> Option<Vec<DiskInfo>> {
    if ws.send(Message::Text("get_disks".into())).await.is_err() {
        return None;
    }
    match ws.next().await {
        Some(Ok(Message::Binary(b))) => {
            gunzip_to_string(&b).and_then(|s| serde_json::from_str::<Vec<DiskInfo>>(&s).ok())
        }
        Some(Ok(Message::Text(json))) => serde_json::from_str::<Vec<DiskInfo>>(&json).ok(),
        _ => None,
    }
}

// Send a "get_processes" request and await a ProcessesPayload decoded from protobuf (binary, may be gzipped)
pub async fn request_processes(ws: &mut WsStream) -> Option<ProcessesPayload> {
    if ws
        .send(Message::Text("get_processes".into()))
        .await
        .is_err()
    {
        return None;
    }
    match ws.next().await {
        Some(Ok(Message::Binary(b))) => {
            let gz = is_gzip(&b);
            let data = if gz { gunzip_to_vec(&b)? } else { b };
            match pb::Processes::decode(data.as_slice()) {
                Ok(pb) => {
                    let rows: Vec<ProcessInfo> = pb
                        .rows
                        .into_iter()
                        .map(|p: pb::Process| ProcessInfo {
                            pid: p.pid,
                            name: p.name,
                            cpu_usage: p.cpu_usage,
                            mem_bytes: p.mem_bytes,
                        })
                        .collect();
                    Some(ProcessesPayload {
                        process_count: pb.process_count as usize,
                        top_processes: rows,
                    })
                }
                Err(e) => {
                    if std::env::var("SOCKTOP_DEBUG").ok().as_deref() == Some("1") {
                        eprintln!("protobuf decode failed: {e}");
                    }
                    // Fallback: maybe it's JSON (bytes already decompressed if gz)
                    match String::from_utf8(data) {
                        Ok(s) => serde_json::from_str::<ProcessesPayload>(&s).ok(),
                        Err(_) => None,
                    }
                }
            }
        }
        Some(Ok(Message::Text(json))) => serde_json::from_str::<ProcessesPayload>(&json).ok(),
        _ => None,
    }
}
