use crate::message::{SerialChunk, CHUNK_SIZE};
use crate::transport::{self, ChunkReader, ChunkWriter};
use anyhow::{Context, Result};
use futures::StreamExt;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use tracing::{debug, warn};

pub async fn pump_read_to_up(
    mut reader: impl AsyncRead + Unpin,
    up_writer: &ChunkWriter,
    label: &str,
) -> Result<()> {
    let mut buf = vec![0u8; CHUNK_SIZE];
    let mut seq = 0u64;

    loop {
        let n = reader
            .read(&mut buf)
            .await
            .with_context(|| format!("{label} read failed"))?;
        if n == 0 {
            continue;
        }

        let chunk = SerialChunk::new(seq, buf[..n].to_vec());
        seq = seq.wrapping_add(1);
        debug!(label, seq = chunk.seq, bytes = n, "forwarding chunk up");
        transport::dds::write_chunk_async(up_writer, chunk).await?;
    }
}

pub async fn pump_read_to_down(
    mut reader: impl AsyncRead + Unpin,
    down_writer: &ChunkWriter,
    label: &str,
) -> Result<()> {
    let mut buf = vec![0u8; CHUNK_SIZE];
    let mut seq = 0u64;

    loop {
        let n = reader
            .read(&mut buf)
            .await
            .with_context(|| format!("{label} read failed"))?;
        if n == 0 {
            continue;
        }

        let chunk = SerialChunk::new(seq, buf[..n].to_vec());
        seq = seq.wrapping_add(1);
        debug!(label, seq = chunk.seq, bytes = n, "forwarding chunk down");
        transport::dds::write_chunk_async(down_writer, chunk).await?;
    }
}

pub async fn pump_down_to_write(
    down_reader: ChunkReader,
    mut writer: impl AsyncWrite + Unpin,
    label: &str,
) -> Result<()> {
    let mut stream = down_reader.async_bare_sample_stream();
    while let Some(sample) = stream.next().await {
        match sample {
            Ok(chunk) => {
                debug!(
                    label,
                    seq = chunk.seq,
                    bytes = chunk.data.len(),
                    "writing chunk from down"
                );
                writer
                    .write_all(&chunk.data)
                    .await
                    .with_context(|| format!("{label} write failed"))?;
                writer
                    .flush()
                    .await
                    .with_context(|| format!("{label} flush failed"))?;
            }
            Err(e) => {
                warn!(label, error = ?e, "DDS read error on down topic");
                return Err(anyhow::anyhow!("DDS read failed on down topic: {e:?}"));
            }
        }
    }
    Ok(())
}

pub async fn pump_up_to_write(
    up_reader: ChunkReader,
    mut writer: impl AsyncWrite + Unpin,
    label: &str,
) -> Result<()> {
    let mut stream = up_reader.async_bare_sample_stream();
    while let Some(sample) = stream.next().await {
        match sample {
            Ok(chunk) => {
                debug!(
                    label,
                    seq = chunk.seq,
                    bytes = chunk.data.len(),
                    "writing chunk from up"
                );
                writer
                    .write_all(&chunk.data)
                    .await
                    .with_context(|| format!("{label} write failed"))?;
                writer
                    .flush()
                    .await
                    .with_context(|| format!("{label} flush failed"))?;
            }
            Err(e) => {
                warn!(label, error = ?e, "DDS read error on up topic");
                return Err(anyhow::anyhow!("DDS read failed on up topic: {e:?}"));
            }
        }
    }
    Ok(())
}

pub async fn run_serve_bridge(
    serial: impl AsyncRead + AsyncWrite + Unpin + Send + 'static,
    channel: transport::DdsChannel,
) -> Result<()> {
    let (mut serial_read, mut serial_write) = tokio::io::split(serial);
    let up_writer = channel.up_writer;
    let down_reader = channel.down_reader;

    let up_task = tokio::spawn(async move {
        pump_read_to_up(&mut serial_read, &up_writer, "serial").await
    });
    let down_task = tokio::spawn(async move {
        pump_down_to_write(down_reader, &mut serial_write, "serial").await
    });

    let result = tokio::select! {
        res = up_task => res,
        res = down_task => res,
    };
    result.context("serve bridge task join failed")??;
    Ok(())
}

pub async fn run_attach_bridge(
    pty: impl AsyncRead + AsyncWrite + Unpin + Send + 'static,
    channel: transport::DdsChannel,
) -> Result<()> {
    let (mut pty_read, mut pty_write) = tokio::io::split(pty);
    let down_writer = channel.down_writer;
    let up_reader = channel.up_reader;

    let down_task = tokio::spawn(async move {
        pump_read_to_down(&mut pty_read, &down_writer, "pty").await
    });
    let up_task = tokio::spawn(async move {
        pump_up_to_write(up_reader, &mut pty_write, "pty").await
    });

    let result = tokio::select! {
        res = down_task => res,
        res = up_task => res,
    };
    result.context("attach bridge task join failed")??;
    Ok(())
}
