use std::io;
use std::net::{SocketAddr, ToSocketAddrs};
use std::pin::pin;

use dope::fiber::{Connector, Fiber, Holding};
use dope::runtime::profile::Production;
use dope::transport::Tcp;
use dope::transport::config::tcp::StreamOpts;
use dope::{DriverConfig, Executor};
use dope_tls::{Endpoint, Tls, WebpkiRoots};
use shin::client::{Config as TlsConfig, Verifier};

use crate::error::Error;

type Conn = Connector<0, Tcp, Tls>;

const READ_CHUNK: usize = 32 * 1024;
const MAX_RESPONSE: usize = 64 * 1024 * 1024;
const POOL: usize = 2;

#[pin_project::pin_project]
#[derive(dope_gen::Dispatcher)]
struct App {
    #[pin]
    #[manifold]
    connector: Conn,
}

pub(crate) fn fetch(
    host: &str,
    port: u16,
    unix_secs: u64,
    wire: Vec<u8>,
) -> Result<Vec<u8>, Error> {
    let addr = resolve(host, port)?;

    let tls_cfg = TlsConfig {
        verifier: Verifier::X509 {
            anchors: WebpkiRoots::anchors(),
            hostname: host.as_bytes().to_vec(),
            now_seconds: unix_secs,
        },
        transport_params: Vec::new(),
        alpn_protocols: Vec::new(),
        resumption: None,
        enable_early_data: false,
    };

    let cfg = <dope::DriverCfg as DriverConfig>::for_tcp_profile::<Production>(POOL);
    let mut exec = Executor::new(cfg)?;
    let mut connector = Conn::new(POOL, exec.driver_mut());
    connector.set_cfg(Endpoint::Client(tls_cfg));

    let mut app = pin!(App { connector });
    // borrow must end here: `block_on` re-borrows `app` to drive the same manifold (thread-per-core, no concurrent &mut).
    let hold = app.as_mut().connector_handle();
    let fiber = Fiber::new(round_trip(hold, addr, wire));
    crate::runtime::block_on(&mut exec, app.as_mut(), fiber).map_err(Error::from)
}

async fn round_trip(
    hold: Holding<'_, Conn>,
    addr: SocketAddr,
    wire: Vec<u8>,
) -> io::Result<Vec<u8>> {
    let mut io = Conn::connect_held(hold, addr, StreamOpts::default()).await?;
    io.write_all(&wire).await?;

    let mut out = Vec::new();
    let mut buf = vec![0u8; READ_CHUNK];
    loop {
        let n = io.read_into(&mut buf).await?;
        if n == 0 {
            break;
        }
        out.extend_from_slice(&buf[..n]);
        if out.len() > MAX_RESPONSE {
            return Err(io::Error::new(
                io::ErrorKind::InvalidData,
                "response exceeds 64 MiB cap",
            ));
        }
    }
    Ok(out)
}

fn resolve(host: &str, port: u16) -> Result<SocketAddr, Error> {
    (host, port)
        .to_socket_addrs()
        .map_err(|e| Error::transport(format!("dns resolve {host}:{port}: {e}")))?
        .next()
        .ok_or_else(|| Error::transport(format!("no address for {host}:{port}")))
}
