use std::{
  collections::HashMap,
  io::{BufRead, BufReader},
  sync::Arc,
  thread,
};

use napi::{
  threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode},
  Result,
};
use serde_json::json;
use uuid::Uuid;

use crate::version::{types::VersionBackend, utils::DropletHandler};

const CHUNK_SIZE: usize = 1024 * 1024 * 64;

#[derive(serde::Serialize)]
struct ChunkData {
  permissions: u32,
  ids: Vec<String>,
  checksums: Vec<String>,
  lengths: Vec<usize>,
}

#[napi]
pub fn call_alt_thread_func(tsfn: Arc<ThreadsafeFunction<()>>) -> Result<(), String> {
  let tsfn_cloned = tsfn.clone();
  thread::spawn(move || {
    tsfn_cloned.call(Ok(()), ThreadsafeFunctionCallMode::Blocking);
  });
  Ok(())
}

#[napi]
pub fn generate_manifest<'a>(
  droplet_handler: &mut DropletHandler,
  dir: String,
  progress_sfn: ThreadsafeFunction<i32>,
  log_sfn: ThreadsafeFunction<String>,
  callback_sfn: ThreadsafeFunction<String>,
) -> Result<()> {
  let backend: &mut Box<dyn VersionBackend + Send> = droplet_handler
    .create_backend_for_path(dir)
    .ok_or(napi::Error::from_reason(
    "Could not create backend for path.",
  ))?;
    let files = backend.list_files();

    // Filepath to chunk data
    let mut chunks: HashMap<String, ChunkData> = HashMap::new();

    let total: i32 = files.len() as i32;
    let mut i: i32 = 0;

    for version_file in files {
      let reader = backend.reader(&version_file).unwrap();
      let mut reader = BufReader::with_capacity(8128, reader);

      let mut chunk_data = ChunkData {
        permissions: version_file.permission,
        ids: Vec::new(),
        checksums: Vec::new(),
        lengths: Vec::new(),
      };

      let mut chunk_index = 0;
      loop {
        let mut length = 0;
        let mut buffer: Vec<u8> = Vec::new();
        let mut file_empty = false;

        loop {
          let read_buf = reader.fill_buf().unwrap();
          let buf_length = read_buf.len();

          length += buf_length;

          if length >= CHUNK_SIZE {
            break;
          }

          // If we're out of data, add this chunk and then move onto the next file
          if buf_length == 0 {
            file_empty = true;
            break;
          }

          buffer.extend_from_slice(read_buf);
          reader.consume(length);
        }

        let chunk_id = Uuid::new_v4();
        let checksum = md5::compute(buffer).0;
        let checksum_string = hex::encode(checksum);

        chunk_data.ids.push(chunk_id.to_string());
        chunk_data.checksums.push(checksum_string);
        chunk_data.lengths.push(length);

        let log_str = format!(
          "Processed chunk {} for {}",
          chunk_index, &version_file.relative_filename
        );

        log_sfn.call(Ok(log_str), ThreadsafeFunctionCallMode::Blocking);

        chunk_index += 1;

        if file_empty {
          break;
        }
      }

      chunks.insert(version_file.relative_filename, chunk_data);

      i += 1;
      let progress = i * 100 / total;
      progress_sfn.call(Ok(progress), ThreadsafeFunctionCallMode::Blocking);
    }

    callback_sfn.call(
      Ok(json!(chunks).to_string()),
      ThreadsafeFunctionCallMode::Blocking,
    );

  Ok(())
}
