use std::{cell::RefCell, ptr::NonNull};

use napi_derive::napi;
use rspack_core::{
  AsyncDependenciesBlock, AsyncDependenciesBlockIdentifier, Compilation, CompilationId,
  DependenciesBlock,
};
use rspack_napi::{napi::bindgen_prelude::*, OneShotRef};
use rustc_hash::FxHashMap as HashMap;

use crate::JsDependencyWrapper;

#[napi]
pub struct JsDependenciesBlock {
  block_id: AsyncDependenciesBlockIdentifier,
  compilation: NonNull<Compilation>,
}

#[napi]
impl JsDependenciesBlock {
  #[napi(getter, ts_return_type = "JsDependency[]")]
  pub fn dependencies(&mut self) -> Vec<JsDependencyWrapper> {
    let compilation = unsafe { self.compilation.as_ref() };
    let module_graph = compilation.get_module_graph();
    if let Some(block) = module_graph.block_by_id(&self.block_id) {
      block
        .get_dependencies()
        .iter()
        .filter_map(|dependency_id| {
          module_graph
            .dependency_by_id(dependency_id)
            .map(|dep| JsDependencyWrapper::new(dep.as_ref(), compilation.id(), Some(compilation)))
        })
        .collect::<Vec<_>>()
    } else {
      vec![]
    }
  }

  #[napi(getter, ts_return_type = "JsDependenciesBlock[]")]
  pub fn blocks(&mut self) -> Vec<JsDependenciesBlockWrapper> {
    let compilation = unsafe { self.compilation.as_ref() };
    let module_graph = compilation.get_module_graph();
    if let Some(block) = module_graph.block_by_id(&self.block_id) {
      block
        .get_blocks()
        .iter()
        .filter_map(|block_id| {
          module_graph
            .block_by_id(block_id)
            .map(|block| JsDependenciesBlockWrapper::new(block, compilation))
        })
        .collect::<Vec<_>>()
    } else {
      vec![]
    }
  }
}

type BlockInstanceRefs = HashMap<AsyncDependenciesBlockIdentifier, OneShotRef<JsDependenciesBlock>>;

type BlockInstanceRefsByCompilationId = RefCell<HashMap<CompilationId, BlockInstanceRefs>>;

thread_local! {
  static BLOCK_INSTANCE_REFS: BlockInstanceRefsByCompilationId = Default::default();
}

pub struct JsDependenciesBlockWrapper {
  block_id: AsyncDependenciesBlockIdentifier,
  compilation: NonNull<Compilation>,
}

impl JsDependenciesBlockWrapper {
  pub fn new(block: &AsyncDependenciesBlock, compilation: &Compilation) -> Self {
    let block_id = block.identifier();

    #[allow(clippy::unwrap_used)]
    Self {
      block_id,
      compilation: NonNull::new(compilation as *const Compilation as *mut Compilation).unwrap(),
    }
  }

  pub fn cleanup_last_compilation(compilation_id: CompilationId) {
    BLOCK_INSTANCE_REFS.with(|refs| {
      let mut refs_by_compilation_id = refs.borrow_mut();
      refs_by_compilation_id.remove(&compilation_id)
    });
  }
}

impl ToNapiValue for JsDependenciesBlockWrapper {
  unsafe fn to_napi_value(
    env: napi::sys::napi_env,
    val: Self,
  ) -> napi::Result<napi::sys::napi_value> {
    BLOCK_INSTANCE_REFS.with(|refs| {
      let compilation = unsafe { val.compilation.as_ref() };
      let mut refs_by_compilation_id = refs.borrow_mut();
      let entry = refs_by_compilation_id.entry(compilation.id());
      let refs = match entry {
        std::collections::hash_map::Entry::Occupied(entry) => entry.into_mut(),
        std::collections::hash_map::Entry::Vacant(entry) => {
          let refs = HashMap::default();
          entry.insert(refs)
        }
      };

      match refs.entry(val.block_id) {
        std::collections::hash_map::Entry::Occupied(occupied_entry) => {
          let r = occupied_entry.get();
          ToNapiValue::to_napi_value(env, r)
        }
        std::collections::hash_map::Entry::Vacant(vacant_entry) => {
          let js_block = JsDependenciesBlock {
            block_id: val.block_id,
            compilation: val.compilation,
          };
          let r = vacant_entry.insert(OneShotRef::new(env, js_block)?);
          ToNapiValue::to_napi_value(env, r)
        }
      }
    })
  }
}
