use crate::api::HttpClient;
use crate::error::BioLibError;

pub struct ModuleOutputV2 {
    data: OutputData,
    metadata: Option<Metadata>,
    stdout: Option<Vec<u8>>,
    stderr: Option<Vec<u8>>,
    files: Option<Vec<OutputFile>>,
}

enum OutputData {
    InMemory(Vec<u8>),
    Remote { http: HttpClient, url: String },
}

struct Metadata {
    stdout_length: u64,
    stderr_length: u64,
    files_info_length: u64,
    #[allow(dead_code)]
    files_data_length: u64,
    exit_code: u16,
}

const METADATA_LENGTH: usize = 1 + 1 + 8 + 8 + 8 + 8 + 2; // 36 bytes

#[derive(Debug, Clone)]
pub struct OutputFile {
    pub path: String,
    pub data_start: u64,
    pub data_length: u64,
}

impl ModuleOutputV2 {
    pub fn from_bytes(data: Vec<u8>) -> Self {
        Self {
            data: OutputData::InMemory(data),
            metadata: None,
            stdout: None,
            stderr: None,
            files: None,
        }
    }

    pub fn from_remote(url: String) -> Self {
        Self {
            data: OutputData::Remote {
                http: HttpClient::new(),
                url,
            },
            metadata: None,
            stdout: None,
            stderr: None,
            files: None,
        }
    }

    fn get_data(&self, start: u64, length: u64) -> crate::Result<Vec<u8>> {
        if length == 0 {
            return Ok(Vec::new());
        }
        match &self.data {
            OutputData::InMemory(data) => {
                let s = start as usize;
                let e = s + length as usize;
                if e > data.len() {
                    return Err(BioLibError::BinaryFormat(
                        "Read past end of buffer".to_string(),
                    ));
                }
                Ok(data[s..e].to_vec())
            }
            OutputData::Remote { http, url } => {
                let end = start + length - 1;
                let response = http.get_with_range(url, start, end)?;
                if response.body.len() != length as usize {
                    return Err(BioLibError::BinaryFormat(format!(
                        "Expected {} bytes, got {}",
                        length,
                        response.body.len()
                    )));
                }
                Ok(response.body)
            }
        }
    }

    fn get_metadata(&mut self) -> crate::Result<&Metadata> {
        if self.metadata.is_none() {
            let meta_bytes = self.get_data(0, METADATA_LENGTH as u64)?;
            let mut pointer = 0usize;

            let version = meta_bytes[pointer];
            pointer += 1;
            let pkg_type = meta_bytes[pointer];
            pointer += 1;

            if version != 1 {
                return Err(BioLibError::BinaryFormat(format!(
                    "Version mismatch: expected 1, got {version}"
                )));
            }
            if pkg_type != 11 {
                return Err(BioLibError::BinaryFormat(format!(
                    "Type mismatch: expected 11, got {pkg_type}"
                )));
            }

            let stdout_length =
                u64::from_be_bytes(meta_bytes[pointer..pointer + 8].try_into().unwrap());
            pointer += 8;
            let stderr_length =
                u64::from_be_bytes(meta_bytes[pointer..pointer + 8].try_into().unwrap());
            pointer += 8;
            let files_info_length =
                u64::from_be_bytes(meta_bytes[pointer..pointer + 8].try_into().unwrap());
            pointer += 8;
            let files_data_length =
                u64::from_be_bytes(meta_bytes[pointer..pointer + 8].try_into().unwrap());
            pointer += 8;
            let exit_code =
                u16::from_be_bytes(meta_bytes[pointer..pointer + 2].try_into().unwrap());

            self.metadata = Some(Metadata {
                stdout_length,
                stderr_length,
                files_info_length,
                files_data_length,
                exit_code,
            });
        }
        Ok(self.metadata.as_ref().unwrap())
    }

    pub fn get_exit_code(&mut self) -> crate::Result<u16> {
        let metadata = self.get_metadata()?;
        Ok(metadata.exit_code)
    }

    pub fn get_stdout(&mut self) -> crate::Result<Vec<u8>> {
        if self.stdout.is_none() {
            let metadata = self.get_metadata()?;
            let start = METADATA_LENGTH as u64;
            let length = metadata.stdout_length;
            self.stdout = Some(self.get_data(start, length)?);
        }
        Ok(self.stdout.clone().unwrap())
    }

    pub fn get_stderr(&mut self) -> crate::Result<Vec<u8>> {
        if self.stderr.is_none() {
            let metadata = self.get_metadata()?;
            let start = METADATA_LENGTH as u64 + metadata.stdout_length;
            let length = metadata.stderr_length;
            self.stderr = Some(self.get_data(start, length)?);
        }
        Ok(self.stderr.clone().unwrap())
    }

    pub fn get_files(&mut self) -> crate::Result<Vec<OutputFile>> {
        if self.files.is_none() {
            let metadata = self.get_metadata()?;
            let files_info_length = metadata.files_info_length;
            let stdout_length = metadata.stdout_length;
            let stderr_length = metadata.stderr_length;

            if files_info_length == 0 {
                self.files = Some(Vec::new());
                return Ok(self.files.clone().unwrap());
            }

            let files_info_start = METADATA_LENGTH as u64 + stdout_length + stderr_length;
            let files_info_data = self.get_data(files_info_start, files_info_length)?;

            let files_data_start = files_info_start + files_info_length;
            let mut files = Vec::new();
            let mut pointer = 0usize;
            let mut data_pointer = files_data_start;

            while pointer < files_info_data.len() {
                let path_length =
                    u32::from_be_bytes(files_info_data[pointer..pointer + 4].try_into().unwrap())
                        as usize;
                pointer += 4;
                let path =
                    String::from_utf8_lossy(&files_info_data[pointer..pointer + path_length])
                        .to_string();
                pointer += path_length;
                let data_length =
                    u64::from_be_bytes(files_info_data[pointer..pointer + 8].try_into().unwrap());
                pointer += 8;

                files.push(OutputFile {
                    path,
                    data_start: data_pointer,
                    data_length,
                });
                data_pointer += data_length;
            }

            self.files = Some(files);
        }
        Ok(self.files.clone().unwrap())
    }

    pub fn get_file_data(&self, file: &OutputFile) -> crate::Result<Vec<u8>> {
        self.get_data(file.data_start, file.data_length)
    }
}
