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

use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;

use async_trait::async_trait;
use deno_core::FromV8;
use deno_core::OpState;
use deno_core::ToV8;
use deno_core::convert::Uint8Array;
use deno_core::error::AnyError;
use deno_core::op2;
use deno_error::JsErrorBox;
use deno_permissions::PermissionsContainer;

deno_core::extension!(
  deno_bundle_runtime,
  deps = [
    deno_web
  ],
  ops = [
    op_bundle,
  ],
  esm = [
    "bundle.ts"
  ],
  options = {
    bundle_provider: Option<Arc<dyn BundleProvider>>,
  },
  state = |state, options| {
    if let Some(bundle_provider) = options.bundle_provider {
      state.put(bundle_provider);
    } else {
      state.put::<Arc<dyn BundleProvider>>(Arc::new(()));
    }
  },
);

#[async_trait]
impl BundleProvider for () {
  async fn bundle(
    &self,
    _options: BundleOptions,
    _permissions: PermissionsContainer,
  ) -> Result<BuildResponse, AnyError> {
    // Embedders that don't wire up a real provider (notably `denort`, used
    // by `deno compile` outputs) fall through to this no-op implementation.
    // Surface that limitation directly to the user instead of leaking the
    // historical "default BundleProvider does not do anything" string.
    // See denoland/deno#31597.
    Err(deno_core::anyhow::anyhow!(
      "Deno.bundle() is not available in compiled binaries (`deno compile`). \
       Run with `deno run` instead, or pre-bundle the entrypoints at build time."
    ))
  }
}

#[async_trait]
pub trait BundleProvider: Send + Sync {
  async fn bundle(
    &self,
    options: BundleOptions,
    permissions: PermissionsContainer,
  ) -> Result<BuildResponse, AnyError>;
}

#[derive(Clone, Debug, Eq, PartialEq, Default, FromV8)]
pub struct BundleOptions {
  pub entrypoints: Vec<String>,
  pub output_path: Option<String>,
  pub output_dir: Option<String>,
  #[from_v8(default)]
  pub external: Vec<String>,
  #[from_v8(serde, default)]
  pub format: BundleFormat,
  #[from_v8(default)]
  pub minify: bool,
  #[from_v8(default)]
  pub keep_names: bool,
  #[from_v8(default)]
  pub code_splitting: bool,
  #[from_v8(default = true)]
  pub inline_imports: bool,
  #[from_v8(serde, default)]
  pub packages: PackageHandling,
  #[from_v8(serde)]
  pub sourcemap: Option<SourceMapType>,
  #[from_v8(serde, default)]
  pub platform: BundlePlatform,
  #[from_v8(default = true)]
  pub write: bool,
}

#[derive(Clone, Debug, Eq, PartialEq, Copy, Default, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum BundlePlatform {
  Browser,
  #[default]
  Deno,
}

#[derive(Clone, Debug, Eq, PartialEq, Copy, Default, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum BundleFormat {
  #[default]
  Esm,
  Cjs,
  Iife,
}

#[derive(Clone, Debug, Eq, PartialEq, Copy, Default, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum SourceMapType {
  #[default]
  Linked,
  Inline,
  External,
}

impl std::fmt::Display for BundleFormat {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    match self {
      BundleFormat::Esm => write!(f, "esm"),
      BundleFormat::Cjs => write!(f, "cjs"),
      BundleFormat::Iife => write!(f, "iife"),
    }
  }
}

impl std::fmt::Display for SourceMapType {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    match self {
      SourceMapType::Linked => write!(f, "linked"),
      SourceMapType::Inline => write!(f, "inline"),
      SourceMapType::External => write!(f, "external"),
    }
  }
}

#[derive(Clone, Debug, Eq, PartialEq, Copy, Default, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum PackageHandling {
  #[default]
  Bundle,
  External,
}

impl std::fmt::Display for PackageHandling {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    match self {
      PackageHandling::Bundle => write!(f, "bundle"),
      PackageHandling::External => write!(f, "external"),
    }
  }
}
#[derive(Debug, Clone, FromV8, ToV8)]
pub struct Message {
  pub text: String,
  pub location: Option<Location>,
  pub notes: Vec<Note>,
}

#[derive(Debug, Clone, FromV8, ToV8)]
pub struct PartialMessage {
  pub id: Option<String>,
  pub plugin_name: Option<String>,
  pub text: Option<String>,
  pub location: Option<Location>,
  pub notes: Option<Vec<Note>>,
  pub detail: Option<u32>,
}

#[derive(Debug, Clone, ToV8)]
pub struct BuildOutputFile {
  pub path: String,
  pub contents: Option<Uint8Array>,
  pub hash: String,
}
#[derive(Debug, Clone, ToV8)]
pub struct BuildResponse {
  pub errors: Vec<Message>,
  pub warnings: Vec<Message>,
  pub output_files: Option<Vec<BuildOutputFile>>,
}
#[derive(Debug, Clone, FromV8, ToV8)]
pub struct Note {
  pub text: String,
  pub location: Option<Location>,
}
#[derive(Debug, Clone, FromV8, ToV8)]
pub struct Location {
  pub file: String,
  pub namespace: Option<String>,
  pub line: u32,
  pub column: u32,
  pub length: Option<u32>,
  pub suggestion: Option<String>,
}

fn deserialize_regex<'de, D>(deserializer: D) -> Result<regex::Regex, D::Error>
where
  D: serde::Deserializer<'de>,
{
  use serde::Deserialize;
  let s = String::deserialize(deserializer)?;
  regex::Regex::new(&s).map_err(serde::de::Error::custom)
}

#[derive(Debug, Clone, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OnResolveOptions {
  #[serde(deserialize_with = "deserialize_regex")]
  pub filter: regex::Regex,
  pub namespace: Option<String>,
}

#[derive(Debug, Clone, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OnLoadOptions {
  #[serde(deserialize_with = "deserialize_regex")]
  pub filter: regex::Regex,
  pub namespace: Option<String>,
}

#[op2]
pub async fn op_bundle(
  state: Rc<RefCell<OpState>>,
  #[scoped] options: BundleOptions,
) -> Result<BuildResponse, JsErrorBox> {
  // eprintln!("op_bundle: {:?}", options);
  let (provider, permissions) = {
    let state = state.borrow();
    let permissions = state
      .try_borrow::<PermissionsContainer>()
      .cloned()
      .ok_or_else(|| {
        JsErrorBox::generic(
          "Deno.bundle() requires a runtime permissions context",
        )
      })?;
    (
      state.borrow::<Arc<dyn BundleProvider>>().clone(),
      permissions,
    )
  };

  provider
    .bundle(options, permissions)
    .await
    .map_err(|e| JsErrorBox::generic(e.to_string()))
}
