use std::io::{Read, Write};

use crate::{
    command::{EmbedSpec, InputSource},
    error::{Error, Result},
    esc::{
        MaterializedInput, materialize_path, materialize_reader, prepare_payload,
        write_payload_body,
    },
};

/// Writes a POSIX `sh` script that reproduces the requested input bytes.
///
/// The generated script writes the embedded bytes to stdout when executed.
/// Inputs are concatenated in order. If `spec.inputs` is empty, this function
/// reads `stdin` once and embeds those bytes.
pub fn write_embed<W: Write, R: Read>(
    spec: &EmbedSpec,
    stdin: &mut R,
    writer: &mut W,
) -> Result<()> {
    let mut inputs = materialize_inputs(spec, stdin)?;

    writer
        .write_all(embed_header().as_bytes())
        .map_err(|source| Error::io("failed to write embed script header", source))?;

    for (index, input) in inputs.iter_mut().enumerate() {
        let mut payload = prepare_payload(input)?;
        write_payload_function(writer, index + 1, &mut payload)?;
    }

    writer
        .write_all(b"main() {\n")
        .map_err(|source| Error::io("failed to write embed script", source))?;
    for index in 0..inputs.len() {
        writer
            .write_all(format!("  bdlx_emit_{}\n", index + 1).as_bytes())
            .map_err(|source| Error::io("failed to write embed script", source))?;
    }
    writer
        .write_all(b"}\n\nmain \"$@\"\n")
        .map_err(|source| Error::io("failed to finalize embed script", source))?;

    Ok(())
}

fn materialize_inputs<R: Read>(spec: &EmbedSpec, stdin: &mut R) -> Result<Vec<MaterializedInput>> {
    let mut saw_stdin = false;
    let mut out = Vec::with_capacity(spec.inputs.len().max(1));

    let inputs = if spec.inputs.is_empty() {
        vec![InputSource::Stdin]
    } else {
        spec.inputs.clone()
    };

    for input in inputs {
        match input {
            InputSource::Path(path) => out.push(materialize_path(&path)?),
            InputSource::Stdin => {
                if saw_stdin {
                    return Err(Error::DuplicateStdin);
                }
                saw_stdin = true;
                out.push(materialize_reader(stdin, "stdin")?);
            }
        }
    }

    Ok(out)
}

fn embed_header() -> String {
    format!(
        "#!/bin/sh\nset -eu\n\n# Generated by bdlx {}\n\n",
        crate::version()
    )
}

fn write_payload_function<W: Write>(
    writer: &mut W,
    index: usize,
    payload: &mut crate::esc::PreparedPayload,
) -> Result<()> {
    let mut shell = String::new();
    shell.push_str(&format!("bdlx_emit_{}() {{\n", index));
    shell.push_str("  ");
    payload
        .decoder
        .write_decoder_fragment(payload.original_len, &mut shell)
        .map_err(|_| Error::Internal("failed to render shell decoder fragment".to_owned()))?;
    shell.push_str(&format!(" <<'{}'\n", payload.delimiter));

    writer
        .write_all(shell.as_bytes())
        .map_err(|source| Error::io("failed to write embed function", source))?;
    write_payload_body(payload, writer)?;
    writer
        .write_all(format!("\n{}\n}}\n\n", payload.delimiter).as_bytes())
        .map_err(|source| Error::io("failed to finish embed function", source))?;
    Ok(())
}
