// Copyright 2018-2026 the Deno authors. MIT license.

use std::sync::Arc;

use deno_ast::ModuleSpecifier;
use deno_config::glob::FilePatterns;
use deno_config::glob::PathOrPatternSet;
use deno_core::error::AnyError;
use deno_core::parking_lot::RwLock;
use deno_graph::ModuleGraph;
use deno_runtime::deno_permissions::PermissionsContainer;

use crate::args::CliOptions;
use crate::module_loader::ModuleLoadPreparer;
use crate::module_loader::PrepareModuleLoadOptions;
use crate::util::fs::collect_specifiers;
use crate::util::path::is_script_ext;

pub trait ModuleGraphContainer: Clone + 'static {
  /// Acquires a permit to modify the module graph without other code
  /// having the chance to modify it. In the meantime, other code may
  /// still read from the existing module graph.
  async fn acquire_update_permit(&self) -> impl ModuleGraphUpdatePermit;
  /// Gets a copy of the graph.
  fn graph(&self) -> Arc<ModuleGraph>;
}

/// A permit for updating the module graph. When complete and
/// everything looks fine, calling `.commit()` will store the
/// new graph in the ModuleGraphContainer.
pub trait ModuleGraphUpdatePermit {
  /// Gets the module graph for mutation.
  fn graph_mut(&mut self) -> &mut ModuleGraph;
  /// Saves the mutated module graph in the container.
  fn commit(self);
}

/// Holds the `ModuleGraph` for the main worker.
#[derive(Clone)]
pub struct MainModuleGraphContainer {
  // Allow only one request to update the graph data at a time,
  // but allow other requests to read from it at any time even
  // while another request is updating the data.
  update_queue: Arc<deno_core::unsync::sync::TaskQueue>,
  inner: Arc<RwLock<Arc<ModuleGraph>>>,
  cli_options: Arc<CliOptions>,
  module_load_preparer: Arc<ModuleLoadPreparer>,
  root_permissions: PermissionsContainer,
}

#[derive(Default, Debug)]
pub struct CheckSpecifiersOptions<'a> {
  pub ext_overwrite: Option<&'a String>,
  pub allow_unknown_media_types: bool,
}

pub struct CollectSpecifiersOptions {
  /// Whether to include paths that are specified even if they're ignored.
  pub include_ignored_specified: bool,
}

impl MainModuleGraphContainer {
  pub fn new(
    cli_options: Arc<CliOptions>,
    module_load_preparer: Arc<ModuleLoadPreparer>,
    root_permissions: PermissionsContainer,
  ) -> Self {
    Self {
      update_queue: Default::default(),
      inner: Arc::new(RwLock::new(Arc::new(ModuleGraph::new(
        cli_options.graph_kind(),
      )))),
      cli_options,
      module_load_preparer,
      root_permissions,
    }
  }

  pub async fn check_specifiers(
    &self,
    specifiers: &[ModuleSpecifier],
    options: CheckSpecifiersOptions<'_>,
  ) -> Result<(), AnyError> {
    let mut graph_permit = self.acquire_update_permit().await;
    let graph = graph_permit.graph_mut();
    self
      .module_load_preparer
      .prepare_module_load(
        graph,
        specifiers,
        PrepareModuleLoadOptions {
          is_dynamic: false,
          lib: self.cli_options.ts_type_lib_window(),
          permissions: self.root_permissions.clone(),
          file_permission_api_name: None,
          ext_overwrite: options.ext_overwrite,
          allow_unknown_media_types: options.allow_unknown_media_types,
          skip_graph_roots_validation: false,
          file_content_overrides: Default::default(),
          file_header_overrides: Default::default(),
        },
      )
      .await?;
    graph_permit.commit();
    Ok(())
  }

  pub fn collect_specifiers(
    &self,
    files: &[String],
    options: CollectSpecifiersOptions,
  ) -> Result<Vec<ModuleSpecifier>, AnyError> {
    let excludes = self.cli_options.workspace().resolve_config_excludes()?;
    let include_patterns =
      PathOrPatternSet::from_include_relative_path_or_patterns(
        self.cli_options.initial_cwd(),
        files,
      )?;
    let file_patterns = FilePatterns {
      base: self.cli_options.initial_cwd().to_path_buf(),
      include: Some(include_patterns),
      exclude: excludes,
    };
    collect_specifiers(
      crate::util::fs::CollectSpecifiersOptions {
        file_patterns,
        vendor_folder: self
          .cli_options
          .vendor_dir_path()
          .map(ToOwned::to_owned),
        include_ignored_specified: options.include_ignored_specified,
      },
      |e| is_script_ext(e.path),
    )
  }
}

impl ModuleGraphContainer for MainModuleGraphContainer {
  async fn acquire_update_permit(&self) -> impl ModuleGraphUpdatePermit {
    let permit = self.update_queue.acquire().await;
    MainModuleGraphUpdatePermit {
      permit,
      inner: self.inner.clone(),
      graph: (**self.inner.read()).clone(),
    }
  }

  fn graph(&self) -> Arc<ModuleGraph> {
    self.inner.read().clone()
  }
}

/// A permit for updating the module graph. When complete and
/// everything looks fine, calling `.commit()` will store the
/// new graph in the ModuleGraphContainer.
pub struct MainModuleGraphUpdatePermit<'a> {
  permit: deno_core::unsync::sync::TaskQueuePermit<'a>,
  inner: Arc<RwLock<Arc<ModuleGraph>>>,
  graph: ModuleGraph,
}

impl ModuleGraphUpdatePermit for MainModuleGraphUpdatePermit<'_> {
  fn graph_mut(&mut self) -> &mut ModuleGraph {
    &mut self.graph
  }

  fn commit(self) {
    *self.inner.write() = Arc::new(self.graph);
    drop(self.permit); // explicit drop for clarity
  }
}
