use std::{
    fs::File,
    io::{Read, Seek, SeekFrom, Write},
    path::Path,
};

use escnul::{ShellDecodeMode, StreamEncodeError, escape::TWO_PASS_SED};
use sha2::{Digest, Sha256};
use tempfile::NamedTempFile;

use crate::error::{Error, Result};

const CHUNK_SIZE: usize = 16 * 1024;

#[derive(Debug)]
pub struct MaterializedInput {
    pub temp: NamedTempFile,
    pub label: String,
    pub byte_len: u64,
    pub sha256: String,
}

pub type PayloadDecoder = ShellDecodeMode;

#[derive(Debug)]
pub struct PreparedPayload {
    pub encoded: NamedTempFile,
    pub decoder: PayloadDecoder,
    pub original_len: u64,
    pub delimiter: String,
}

pub fn materialize_reader<R: Read + ?Sized>(
    reader: &mut R,
    label: impl Into<String>,
) -> Result<MaterializedInput> {
    let label = label.into();
    let mut temp =
        NamedTempFile::new().map_err(|source| Error::io("failed to create temp file", source))?;
    let mut hasher = Sha256::new();
    let mut byte_len = 0u64;
    let mut buffer = [0u8; CHUNK_SIZE];

    loop {
        let read = reader
            .read(&mut buffer)
            .map_err(|source| Error::io(format!("failed to read {}", label), source))?;
        if read == 0 {
            break;
        }
        temp.write_all(&buffer[..read])
            .map_err(|source| Error::io(format!("failed to spool {}", label), source))?;
        hasher.update(&buffer[..read]);
        byte_len += read as u64;
    }

    temp.flush()
        .map_err(|source| Error::io(format!("failed to flush {}", label), source))?;
    rewind(temp.as_file_mut(), &label)?;

    Ok(MaterializedInput {
        temp,
        label,
        byte_len,
        sha256: hex::encode(hasher.finalize()),
    })
}

pub fn materialize_path(path: &Path) -> Result<MaterializedInput> {
    let mut file =
        File::open(path).map_err(|source| Error::io_path("failed to open input", path, source))?;
    materialize_reader(&mut file, path.display().to_string())
}

pub fn prepare_payload(input: &mut MaterializedInput) -> Result<PreparedPayload> {
    let decoder = detect_decoder(input.temp.path(), &input.label)?;
    rewind(input.temp.as_file_mut(), &input.label)?;

    let mut encoded = NamedTempFile::new()
        .map_err(|source| Error::io("failed to create payload temp file", source))?;
    encode_stream(
        input.temp.as_file_mut(),
        encoded.as_file_mut(),
        decoder,
        &input.label,
    )?;
    encoded.flush().map_err(|source| {
        Error::io(
            format!("failed to flush encoded payload for {}", input.label),
            source,
        )
    })?;
    rewind(encoded.as_file_mut(), &input.label)?;

    let delimiter = choose_delimiter(encoded.path(), &input.sha256)?;
    rewind(encoded.as_file_mut(), &input.label)?;

    Ok(PreparedPayload {
        encoded,
        decoder,
        original_len: input.byte_len,
        delimiter,
    })
}

pub fn write_payload_body<W: Write>(payload: &mut PreparedPayload, writer: &mut W) -> Result<()> {
    rewind(payload.encoded.as_file_mut(), "encoded payload")?;
    std::io::copy(payload.encoded.as_file_mut(), writer)
        .map_err(|source| Error::io("failed to write payload body", source))?;
    Ok(())
}

fn detect_decoder(path: &Path, label: &str) -> Result<PayloadDecoder> {
    let mut file = File::open(path)
        .map_err(|source| Error::io_path("failed to open materialized payload", path, source))?;
    TWO_PASS_SED
        .choose_shell_decode_mode_reader(&mut file)
        .map_err(|source| Error::io(format!("failed to scan {}", label), source))
}

fn encode_stream<R: Read, W: Write>(
    reader: &mut R,
    writer: &mut W,
    decoder: PayloadDecoder,
    label: &str,
) -> Result<()> {
    decoder
        .encode_reader(reader, writer)
        .map_err(|error| match error {
            StreamEncodeError::Io(source) => Error::io(
                format!("failed to write encoded payload for {}", label),
                source,
            ),
            StreamEncodeError::Escnul(_) => Error::EncodeFailure {
                label: label.to_owned(),
            },
        })
}

fn choose_delimiter(path: &Path, sha256: &str) -> Result<String> {
    let mut index = 0usize;
    loop {
        let candidate = if index == 0 {
            format!("__BDLX_{}__", sha256)
        } else {
            format!("__BDLX_{}_{}__", sha256, index)
        };

        if !file_contains_subsequence(path, candidate.as_bytes())? {
            return Ok(candidate);
        }

        index += 1;
    }
}

fn file_contains_subsequence(path: &Path, needle: &[u8]) -> Result<bool> {
    if needle.is_empty() {
        return Ok(true);
    }

    let mut file = File::open(path)
        .map_err(|source| Error::io_path("failed to open encoded payload", path, source))?;
    let mut overlap = Vec::new();
    let mut chunk = [0u8; CHUNK_SIZE];

    loop {
        let read = file
            .read(&mut chunk)
            .map_err(|source| Error::io_path("failed to scan encoded payload", path, source))?;
        if read == 0 {
            return Ok(false);
        }

        let mut haystack = Vec::with_capacity(overlap.len() + read);
        haystack.extend_from_slice(&overlap);
        haystack.extend_from_slice(&chunk[..read]);

        if haystack
            .windows(needle.len())
            .any(|window| window == needle)
        {
            return Ok(true);
        }

        let keep = needle.len().saturating_sub(1);
        overlap = haystack[haystack.len().saturating_sub(keep)..].to_vec();
    }
}

fn rewind(file: &mut File, label: &str) -> Result<()> {
    file.seek(SeekFrom::Start(0))
        .map(|_| ())
        .map_err(|source| Error::io(format!("failed to rewind {}", label), source))
}
