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

use std::io::BufReader;
use std::io::Cursor;
use std::path::Path;
use std::path::PathBuf;

use base64::prelude::BASE64_STANDARD;
use base64::prelude::Engine;
use deno_npm::resolution::PackageIdNotFoundError;
use deno_npm::resolution::ValidSerializedNpmResolutionSnapshot;
use deno_npm_installer::process_state::NpmProcessState;
use deno_npm_installer::process_state::NpmProcessStateFromEnvVarSys;
use deno_npm_installer::process_state::NpmProcessStateKind;
use deno_runtime::UNSTABLE_ENV_VAR_NAMES;
use deno_runtime::colors;
use deno_runtime::deno_node::ops::ipc::ChildIpcSerialization;
use deno_runtime::deno_tls::deno_native_certs::load_native_certs;
use deno_runtime::deno_tls::rustls;
use deno_runtime::deno_tls::rustls::RootCertStore;
use deno_runtime::deno_tls::rustls_pemfile;
use deno_runtime::deno_tls::webpki_roots;
use deno_semver::npm::NpmPackageReqReference;
use serde::Deserialize;
use serde::Serialize;
use sys_traits::EnvVar;
use thiserror::Error;

pub fn npm_pkg_req_ref_to_binary_command(
  req_ref: &NpmPackageReqReference,
) -> &str {
  req_ref.sub_path().unwrap_or_else(|| &req_ref.req().name)
}

pub fn has_trace_permissions_enabled(sys: &impl EnvVar) -> bool {
  has_flag_env_var(sys, "DENO_TRACE_PERMISSIONS")
}

pub fn has_flag_env_var(sys: &impl EnvVar, name: &str) -> bool {
  match sys.env_var_os(name) {
    Some(value) => value == "1",
    None => false,
  }
}

/// Reads the node IPC channel configuration that `node:child_process.fork()`
/// passes to a child process through `NODE_CHANNEL_FD` (and the optional
/// `NODE_CHANNEL_SERIALIZATION_MODE`), returning the channel fd and
/// serialization mode so the child can wire up `process.send()` /
/// `process.on("message")`. The env vars are removed once read so they don't
/// leak into grandchild processes. Returns `Ok(None)` when no channel was set.
///
/// Shared by the `deno` CLI and the standalone (`denort`) runtime so the two
/// entry points can't drift; the latter relies on this for IPC to work in a
/// compiled binary's forked child (issue #26304).
pub fn node_ipc_init(
  sys: &impl EnvVar,
) -> Result<
  Option<(i64, ChildIpcSerialization)>,
  deno_runtime::deno_core::anyhow::Error,
> {
  let maybe_node_channel_fd = sys.env_var("NODE_CHANNEL_FD").ok();
  let maybe_node_channel_serialization =
    if let Ok(serialization) = sys.env_var("NODE_CHANNEL_SERIALIZATION_MODE") {
      Some(serialization.parse::<ChildIpcSerialization>()?)
    } else {
      None
    };
  if let Some(node_channel_fd) = maybe_node_channel_fd {
    // Remove so that child processes don't inherit these environment variables.
    // SAFETY: single-threaded at this point in startup
    unsafe {
      std::env::remove_var("NODE_CHANNEL_FD");
      std::env::remove_var("NODE_CHANNEL_SERIALIZATION_MODE");
    }
    let node_channel_fd = node_channel_fd.parse::<i64>()?;
    Ok(Some((
      node_channel_fd,
      maybe_node_channel_serialization.unwrap_or(ChildIpcSerialization::Json),
    )))
  } else {
    Ok(None)
  }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CaData {
  /// The string is a file path
  File(String),
  /// The string holds the actual certificate
  Bytes(Vec<u8>),
}

impl CaData {
  pub fn parse(input: String) -> Option<Self> {
    if let Some(x) = input.strip_prefix("base64:") {
      Some(CaData::Bytes(BASE64_STANDARD.decode(x).ok()?))
    } else {
      Some(CaData::File(input))
    }
  }
}

#[derive(Error, Debug, Clone, deno_error::JsError)]
#[class(generic)]
pub enum RootCertStoreLoadError {
  #[error(
    "Unknown certificate store \"{0}\" specified (allowed: \"system,mozilla\")"
  )]
  UnknownStore(String),
  #[error("Unable to add pem file to certificate store: {0}")]
  FailedAddPemFile(String),
  #[error("Failed opening CA file: {0}")]
  CaFileOpenError(String),
  #[error("Failed to load platform certificates: {0}")]
  FailedNativeCerts(String),
}

fn load_pem_certs(
  path: &Path,
) -> Result<Vec<rustls::pki_types::CertificateDer<'static>>, std::io::Error> {
  let file: std::fs::File = std::fs::File::open(path)?;
  let mut reader: BufReader<std::fs::File> = BufReader::new(file);
  rustls_pemfile::certs(&mut reader).collect()
}

/// Create and populate a root cert store based on the passed options and
/// environment.
pub fn get_root_cert_store(
  sys: &impl EnvVar,
  maybe_root_path: Option<PathBuf>,
  maybe_ca_stores: Option<Vec<String>>,
  maybe_ca_data: Option<CaData>,
) -> Result<RootCertStore, RootCertStoreLoadError> {
  let mut root_cert_store = RootCertStore::empty();
  let ca_stores: Vec<String> = maybe_ca_stores
    .or_else(|| {
      let env_ca_store = sys.env_var("DENO_TLS_CA_STORE").ok()?;
      Some(
        env_ca_store
          .split(',')
          .map(|s| s.trim().to_string())
          .filter(|s| !s.is_empty())
          .collect(),
      )
    })
    .unwrap_or_else(|| vec!["mozilla".to_string()]);

  for store in ca_stores.iter() {
    match store.as_str() {
      "mozilla" => {
        root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.to_vec());
      }
      "system" => {
        let roots = load_native_certs().map_err(|err| {
          RootCertStoreLoadError::FailedNativeCerts(err.to_string())
        })?;
        for root in roots {
          if let Err(err) = root_cert_store
            .add(rustls::pki_types::CertificateDer::from(root.0.clone()))
          {
            log::error!(
              "{}",
              colors::yellow(&format!(
                "Unable to add system certificate to certificate store: {:?}",
                err
              ))
            );
            let hex_encoded_root = faster_hex::hex_string(&root.0);
            log::error!("{}", colors::gray(&hex_encoded_root));
          }
        }
      }
      _ => {
        return Err(RootCertStoreLoadError::UnknownStore(store.clone()));
      }
    }
  }

  let ca_data = maybe_ca_data
    .or_else(|| sys.env_var("DENO_CERT").ok().and_then(CaData::parse));
  if let Some(ca_data) = ca_data {
    let result = match ca_data {
      CaData::File(ca_file) => {
        let ca_file = if let Some(root) = &maybe_root_path {
          root.join(&ca_file)
        } else {
          PathBuf::from(ca_file)
        };
        let certfile = std::fs::File::open(ca_file).map_err(|err| {
          RootCertStoreLoadError::CaFileOpenError(err.to_string())
        })?;
        let mut reader = BufReader::new(certfile);
        rustls_pemfile::certs(&mut reader).collect::<Result<Vec<_>, _>>()
      }
      CaData::Bytes(data) => {
        let mut reader = BufReader::new(Cursor::new(data));
        rustls_pemfile::certs(&mut reader).collect::<Result<Vec<_>, _>>()
      }
    };

    match result {
      Ok(certs) => {
        root_cert_store.add_parsable_certificates(certs);
      }
      Err(e) => {
        return Err(RootCertStoreLoadError::FailedAddPemFile(e.to_string()));
      }
    }
  }

  if let Ok(extra_ca_certs_path) = sys.env_var("NODE_EXTRA_CA_CERTS")
    && !extra_ca_certs_path.is_empty()
  {
    let path: PathBuf = maybe_root_path.as_ref().map_or_else(
      || PathBuf::from(&extra_ca_certs_path),
      |root| root.join(&extra_ca_certs_path),
    );
    match load_pem_certs(&path) {
      Ok(certs) => {
        root_cert_store.add_parsable_certificates(certs);
      }
      Err(e) => log::warn!(
        "{}",
        colors::yellow(&format!(
          "Warning: Ignoring extra certs from \"{extra_ca_certs_path}\", load failed: {e}"
        ))
      ),
    }
  }

  Ok(root_cert_store)
}

pub fn npm_process_state(
  sys: &impl NpmProcessStateFromEnvVarSys,
) -> Option<&'static NpmProcessState> {
  static NPM_PROCESS_STATE: std::sync::OnceLock<Option<NpmProcessState>> =
    std::sync::OnceLock::new();

  NPM_PROCESS_STATE
    .get_or_init(|| {
      use deno_runtime::deno_process::NPM_RESOLUTION_STATE_FD_ENV_VAR_NAME;
      let fd_or_path = sys.env_var_os(NPM_RESOLUTION_STATE_FD_ENV_VAR_NAME)?;

      sys.env_remove_var(NPM_RESOLUTION_STATE_FD_ENV_VAR_NAME);
      if fd_or_path.is_empty() {
        return None;
      }
      NpmProcessState::from_env_var(sys, fd_or_path)
        .inspect_err(|e| {
          log::error!("failed to resolve npm process state: {}", e);
        })
        .ok()
    })
    .as_ref()
}

pub fn resolve_npm_resolution_snapshot(
  sys: &impl NpmProcessStateFromEnvVarSys,
) -> Result<Option<ValidSerializedNpmResolutionSnapshot>, PackageIdNotFoundError>
{
  if let Some(NpmProcessStateKind::Snapshot(snapshot)) =
    npm_process_state(sys).map(|s| &s.kind)
  {
    // TODO(bartlomieju): remove this clone
    Ok(Some(snapshot.clone().into_valid()?))
  } else {
    Ok(None)
  }
}

#[derive(Clone, Default, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct UnstableConfig {
  // TODO(bartlomieju): remove in Deno 2.5
  pub legacy_flag_enabled: bool, // --unstable
  pub detect_cjs: bool,
  pub lazy_dynamic_imports: bool,
  pub raw_imports: bool,
  pub sloppy_imports: bool,
  pub npm_lazy_caching: bool,
  pub tsgo: bool,
  pub features: Vec<String>, // --unstabe-kv --unstable-cron
}

impl UnstableConfig {
  pub fn fill_with_env(&mut self, sys: &impl EnvVar) {
    fn maybe_set(sys: &impl EnvVar, value: &mut bool, var_name: &str) {
      if !*value && has_flag_env_var(sys, var_name) {
        *value = true;
      }
    }

    maybe_set(
      sys,
      &mut self.lazy_dynamic_imports,
      UNSTABLE_ENV_VAR_NAMES.lazy_dynamic_imports,
    );
    maybe_set(
      sys,
      &mut self.npm_lazy_caching,
      UNSTABLE_ENV_VAR_NAMES.npm_lazy_caching,
    );
    maybe_set(sys, &mut self.tsgo, UNSTABLE_ENV_VAR_NAMES.tsgo);
    maybe_set(
      sys,
      &mut self.raw_imports,
      UNSTABLE_ENV_VAR_NAMES.raw_imports,
    );
    maybe_set(
      sys,
      &mut self.sloppy_imports,
      UNSTABLE_ENV_VAR_NAMES.sloppy_imports,
    );
  }

  pub fn enable_node_compat(&mut self) {
    self.sloppy_imports = true;
    self.detect_cjs = true;
    if !self.features.iter().any(|f| f == "node-globals") {
      self.features.push("node-globals".to_string());
    }
  }
}
