use crate::args;
use crate::errors::*;
use crate::plot::RouteAction;
use futures_util::StreamExt;
use http::header::{HeaderMap, HeaderName, HeaderValue};
use std::io::Write;
use std::process::Stdio;
use tokio::io::AsyncWriteExt;

pub struct Hexdumper {
    child: tokio::process::Child,
    stdin: tokio::process::ChildStdin,
}

impl Hexdumper {
    pub fn spawn() -> Result<Self> {
        let mut child = tokio::process::Command::new("hexdump")
            .arg("-C")
            .stdin(Stdio::piped())
            .spawn()
            .context("Failed to spawn hexdump process")?;
        let stdin = child.stdin.take().unwrap();
        Ok(Self { child, stdin })
    }
}

pub enum OutputWriter {
    Stdout(tokio::io::Stdout),
    Hexdump(Hexdumper),
}

impl OutputWriter {
    pub async fn write_all(&mut self, buf: &[u8]) -> Result<()> {
        match self {
            OutputWriter::Stdout(stdout) => stdout.write_all(buf).await?,
            OutputWriter::Hexdump(hexdump) => hexdump.stdin.write_all(buf).await?,
        }
        Ok(())
    }

    pub async fn wait(self) -> Result<()> {
        match self {
            OutputWriter::Stdout(_) => Ok(()),
            OutputWriter::Hexdump(mut hexdump) => {
                drop(hexdump.stdin);
                hexdump.child.wait().await?;
                Ok(())
            }
        }
    }
}

pub async fn run(req: &args::Req) -> Result<()> {
    let ctx = req.plot.load_into_context().await?;

    let mut headers = HeaderMap::new();
    for header in &req.headers {
        let (k, v) = header.split_once(':').with_context(|| {
            anyhow!("Configured header has no value (missing `:`): {:?}", header)
        })?;
        let v = v.strip_prefix(' ').unwrap_or(v);
        headers.insert(
            HeaderName::from_bytes(k.as_bytes())?,
            HeaderValue::try_from(v)?,
        );
    }

    let route_action = ctx
        .plot
        .select_route(
            &req.req_path,
            req.addr.as_ref(),
            req.authority.as_ref(),
            &headers,
        )
        .context("Failed to select route")?;
    info!("Selected route: {:?}", route_action);

    if req.show_response || req.show_status || req.show_headers || req.show_content {
        let response = match route_action {
            RouteAction::Static(route) => route.generate_response(&ctx, &req.req_path).await?,
            _ => bail!("Emulation of this route is currently not supported"),
        };

        let mut stdout = tokio::io::stdout();
        let mut preceding_data = false;
        if req.show_response || req.show_status {
            let status = format!("{}\n", response.status());
            stdout.write_all(status.as_bytes()).await?;
            preceding_data = true;
        }
        if req.show_response || req.show_headers {
            for (k, v) in response.headers() {
                let mut buf = Vec::new();
                write!(buf, "{k}: ")?;
                buf.extend(v.as_bytes());
                buf.push(b'\n');
                stdout.write_all(&buf).await?;
            }
            preceding_data = true;
        }
        if preceding_data {
            stdout.write_all(b"\n").await?;
        }
        if req.show_response || req.show_content {
            let mut body = response.into_body();
            let mut out = if req.hexdump {
                OutputWriter::Hexdump(Hexdumper::spawn()?)
            } else {
                OutputWriter::Stdout(stdout)
            };
            while let Some(chunk) = body.next().await {
                out.write_all(&chunk?).await?;
            }
        }
    }
    Ok(())
}
