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

//! `SubtleCrypto.encrypt()` body in Rust.
//!
//! `WebIdlConverter` for the per-operation `AlgorithmIdentifier` (`RSA-OAEP`,
//! `AES-CBC`, `AES-CTR`, `AES-GCM`, `AES-OCB`, `ChaCha20-Poly1305`), the
//! per-algorithm spec validation (`OperationError` on bad iv length, bad
//! tag length, etc.), and the dispatch into the existing per-algorithm
//! `encrypt_*` helpers in [`crate::encrypt`].

use std::borrow::Cow;

use deno_core::v8;
use deno_core::webidl::ContextFn;
use deno_core::webidl::WebIdlConverter;
use deno_core::webidl::WebIdlError;
use deno_core::webidl::WebIdlErrorKind;
use deno_error::JsErrorBox;

use crate::CryptoError;
use crate::crypto_key::CryptoKeyType;
use crate::encrypt;
use crate::subtle_key::SubtleKey;

/// Normalized per-algorithm encrypt parameters. Each variant carries
/// exactly the dictionary members the matching `encrypt_*` helper needs.
///
/// `Unknown` is produced when the input has a string-coercible `.name`
/// that the encrypt registry doesn't know about; the impl method turns
/// it into a `NotSupportedError` `DOMException` (not the `TypeError` a
/// converter-level error would emit).
pub enum SubtleEncryptParams {
  RsaOaep {
    label: Option<Vec<u8>>,
  },
  AesCbc {
    iv: Vec<u8>,
  },
  AesCtr {
    counter: Vec<u8>,
    length: u32,
  },
  AesGcm {
    iv: Vec<u8>,
    additional_data: Option<Vec<u8>>,
    tag_length: Option<u32>,
  },
  AesOcb {
    iv: Vec<u8>,
    additional_data: Option<Vec<u8>>,
    tag_length: Option<u32>,
  },
  ChaCha20Poly1305 {
    iv: Option<Vec<u8>>,
    additional_data: Option<Vec<u8>>,
    tag_length: Option<u32>,
  },
  Unknown(String),
}

impl SubtleEncryptParams {
  pub fn canonical_name(&self) -> &str {
    match self {
      Self::RsaOaep { .. } => "RSA-OAEP",
      Self::AesCbc { .. } => "AES-CBC",
      Self::AesCtr { .. } => "AES-CTR",
      Self::AesGcm { .. } => "AES-GCM",
      Self::AesOcb { .. } => "AES-OCB",
      Self::ChaCha20Poly1305 { .. } => "ChaCha20-Poly1305",
      Self::Unknown(n) => n,
    }
  }
}

impl<'a> WebIdlConverter<'a> for SubtleEncryptParams {
  type Options = ();

  fn convert<'b>(
    scope: &mut v8::PinScope<'a, '_>,
    value: v8::Local<'a, v8::Value>,
    prefix: Cow<'static, str>,
    context: ContextFn<'b>,
    _options: &Self::Options,
  ) -> Result<Self, WebIdlError> {
    let (name_str, maybe_obj) =
      extract_name_and_obj(scope, value, prefix.clone(), context.borrowed())?;
    let Some(canonical) = canonical_encrypt_name(&name_str) else {
      return Ok(Self::Unknown(name_str));
    };
    match canonical {
      "RSA-OAEP" => {
        let label = match maybe_obj {
          Some(o) => read_optional_buffer_source(
            scope,
            o,
            "label",
            prefix.clone(),
            &context,
          )?,
          None => None,
        };
        Ok(Self::RsaOaep { label })
      }
      "AES-CBC" => {
        let obj =
          maybe_obj.ok_or_else(|| missing_dict(prefix.clone(), &context))?;
        let iv = read_required_buffer_source(
          scope,
          obj,
          "iv",
          prefix.clone(),
          &context,
        )?;
        Ok(Self::AesCbc { iv })
      }
      "AES-CTR" => {
        let obj =
          maybe_obj.ok_or_else(|| missing_dict(prefix.clone(), &context))?;
        let counter = read_required_buffer_source(
          scope,
          obj,
          "counter",
          prefix.clone(),
          &context,
        )?;
        let length =
          read_required_u32(scope, obj, "length", prefix.clone(), &context)?;
        Ok(Self::AesCtr { counter, length })
      }
      "AES-GCM" => {
        let obj =
          maybe_obj.ok_or_else(|| missing_dict(prefix.clone(), &context))?;
        let iv = read_required_buffer_source(
          scope,
          obj,
          "iv",
          prefix.clone(),
          &context,
        )?;
        let additional_data = read_optional_buffer_source(
          scope,
          obj,
          "additionalData",
          prefix.clone(),
          &context,
        )?;
        let tag_length =
          read_optional_u32(scope, obj, "tagLength", prefix.clone(), &context)?;
        Ok(Self::AesGcm {
          iv,
          additional_data,
          tag_length,
        })
      }
      "AES-OCB" => {
        let obj =
          maybe_obj.ok_or_else(|| missing_dict(prefix.clone(), &context))?;
        let iv = read_required_buffer_source(
          scope,
          obj,
          "iv",
          prefix.clone(),
          &context,
        )?;
        let additional_data = read_optional_buffer_source(
          scope,
          obj,
          "additionalData",
          prefix.clone(),
          &context,
        )?;
        let tag_length =
          read_optional_u32(scope, obj, "tagLength", prefix.clone(), &context)?;
        Ok(Self::AesOcb {
          iv,
          additional_data,
          tag_length,
        })
      }
      "ChaCha20-Poly1305" => {
        let obj =
          maybe_obj.ok_or_else(|| missing_dict(prefix.clone(), &context))?;
        let iv = read_optional_buffer_source(
          scope,
          obj,
          "iv",
          prefix.clone(),
          &context,
        )?;
        let additional_data = read_optional_buffer_source(
          scope,
          obj,
          "additionalData",
          prefix.clone(),
          &context,
        )?;
        let tag_length =
          read_optional_u32(scope, obj, "tagLength", prefix.clone(), &context)?;
        Ok(Self::ChaCha20Poly1305 {
          iv,
          additional_data,
          tag_length,
        })
      }
      _ => unreachable!(),
    }
  }
}

fn canonical_encrypt_name(name: &str) -> Option<&'static str> {
  const NAMES: &[&str] = &[
    "RSA-OAEP",
    "AES-CBC",
    "AES-CTR",
    "AES-GCM",
    "AES-OCB",
    "ChaCha20-Poly1305",
  ];
  NAMES.iter().copied().find(|n| n.eq_ignore_ascii_case(name))
}

fn missing_dict(
  prefix: Cow<'static, str>,
  context: &ContextFn<'_>,
) -> WebIdlError {
  WebIdlError::other(
    prefix,
    context.borrowed(),
    JsErrorBox::type_error("Algorithm requires a parameter dictionary"),
  )
}

pub(crate) fn extract_name_and_obj<'a, 'b>(
  scope: &mut v8::PinScope<'a, '_>,
  value: v8::Local<'a, v8::Value>,
  prefix: Cow<'static, str>,
  context: ContextFn<'b>,
) -> Result<(String, Option<v8::Local<'a, v8::Object>>), WebIdlError> {
  if value.is_string() {
    let s = value.to_rust_string_lossy(scope);
    return Ok((s, None));
  }
  if let Ok(obj) = v8::Local::<v8::Object>::try_from(value) {
    let name_key = v8_str(scope, "name");
    let name_val = obj
      .get(scope, name_key.into())
      .unwrap_or_else(|| v8::undefined(scope).into());
    if name_val.is_undefined() {
      return Err(WebIdlError::other(
        prefix,
        context,
        JsErrorBox::type_error("required member 'name' is undefined"),
      ));
    }
    let s = name_val
      .to_string(scope)
      .ok_or_else(|| {
        WebIdlError::other(
          prefix.clone(),
          context.borrowed(),
          JsErrorBox::type_error(
            "algorithm.name is not convertible to DOMString",
          ),
        )
      })?
      .to_rust_string_lossy(scope);
    return Ok((s, Some(obj)));
  }
  Err(WebIdlError::new(
    prefix,
    context,
    WebIdlErrorKind::ConvertToConverterType("AlgorithmIdentifier"),
  ))
}

pub(crate) fn v8_str<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  s: &str,
) -> v8::Local<'s, v8::String> {
  v8::String::new_from_one_byte(
    scope,
    s.as_bytes(),
    v8::NewStringType::Internalized,
  )
  .unwrap()
}

fn read_required_buffer_source<'a, 'b>(
  scope: &mut v8::PinScope<'a, '_>,
  obj: v8::Local<'a, v8::Object>,
  field: &'static str,
  prefix: Cow<'static, str>,
  context: &ContextFn<'b>,
) -> Result<Vec<u8>, WebIdlError> {
  let key = v8_str(scope, field);
  let val = obj
    .get(scope, key.into())
    .unwrap_or_else(|| v8::undefined(scope).into());
  if val.is_undefined() {
    return Err(WebIdlError::other(
      prefix,
      context.borrowed(),
      JsErrorBox::type_error(format!("required dictionary member '{field}'")),
    ));
  }
  value_to_buffer_source(scope, val, field, prefix, context)
}

fn read_optional_buffer_source<'a, 'b>(
  scope: &mut v8::PinScope<'a, '_>,
  obj: v8::Local<'a, v8::Object>,
  field: &'static str,
  prefix: Cow<'static, str>,
  context: &ContextFn<'b>,
) -> Result<Option<Vec<u8>>, WebIdlError> {
  let key = v8_str(scope, field);
  let val = obj
    .get(scope, key.into())
    .unwrap_or_else(|| v8::undefined(scope).into());
  if val.is_undefined() || val.is_null() {
    return Ok(None);
  }
  // Route through the strict `BufferSource` guard so a `SharedArrayBuffer`
  // (or a view backed by one) and any non-BufferSource value rejects with
  // `TypeError`, matching the JS `webidl.converters.BufferSource` contract.
  // The previous silent-`None` path let AES-GCM `additionalData`, RSA-OAEP
  // `label`, cSHAKE `functionName`/`customization`, and ChaCha20-Poly1305
  // `iv` through with garbage shapes.
  value_to_buffer_source(scope, val, field, prefix, context).map(Some)
}

fn value_to_buffer_source<'a, 'b>(
  scope: &mut v8::PinScope<'a, '_>,
  value: v8::Local<'a, v8::Value>,
  field: &'static str,
  prefix: Cow<'static, str>,
  context: &ContextFn<'b>,
) -> Result<Vec<u8>, WebIdlError> {
  if let Ok(view) = v8::Local::<v8::ArrayBufferView>::try_from(value) {
    if let Some(ab) = view.buffer(scope) {
      let ab_val: v8::Local<v8::Value> = ab.into();
      if ab_val.is_shared_array_buffer() {
        return Err(WebIdlError::other(
          prefix,
          context.borrowed(),
          JsErrorBox::type_error(format!(
            "'{field}' is a view on a SharedArrayBuffer, which is not allowed"
          )),
        ));
      }
    }
    return Ok(view_to_bytes(scope, view));
  }
  if value.is_shared_array_buffer() {
    return Err(WebIdlError::other(
      prefix,
      context.borrowed(),
      JsErrorBox::type_error(format!(
        "'{field}' is a SharedArrayBuffer, which is not allowed"
      )),
    ));
  }
  if let Ok(ab) = v8::Local::<v8::ArrayBuffer>::try_from(value) {
    return Ok(arraybuffer_to_bytes(ab));
  }
  Err(WebIdlError::other(
    prefix,
    context.borrowed(),
    JsErrorBox::type_error(format!("'{field}' is not a BufferSource")),
  ))
}

fn view_to_bytes<'a>(
  scope: &mut v8::PinScope<'a, '_>,
  view: v8::Local<'a, v8::ArrayBufferView>,
) -> Vec<u8> {
  let byte_length = view.byte_length();
  if byte_length == 0 {
    return Vec::new();
  }
  let byte_offset = view.byte_offset();
  let ab = view.buffer(scope).unwrap();
  // SAFETY: V8 guarantees byte_offset + byte_length stay within the
  // backing store and a non-detached buffer has a non-null data pointer.
  unsafe {
    let base = ab.data().unwrap().as_ptr() as *const u8;
    std::slice::from_raw_parts(base.add(byte_offset), byte_length).to_vec()
  }
}

fn arraybuffer_to_bytes(ab: v8::Local<v8::ArrayBuffer>) -> Vec<u8> {
  let byte_length = ab.byte_length();
  if byte_length == 0 {
    return Vec::new();
  }
  // SAFETY: as above.
  unsafe {
    let base = ab.data().unwrap().as_ptr() as *const u8;
    std::slice::from_raw_parts(base, byte_length).to_vec()
  }
}

/// `[EnforceRange] unsigned long` conversion. WebCrypto uses this for
/// AES-CTR `length` and AES-GCM `tagLength`; `uint32_value` (ToUint32
/// truncation) would let `2**32 + 5` wrap to `5` past the JS converter
/// that asserted `TypeError`.
fn to_enforce_range_u32<'a>(
  scope: &mut v8::PinScope<'a, '_>,
  val: v8::Local<'a, v8::Value>,
) -> Option<u32> {
  let n = val.number_value(scope)?;
  if !n.is_finite() {
    return None;
  }
  let trunc = n.trunc();
  if trunc < 0.0 || trunc > u32::MAX as f64 {
    return None;
  }
  Some(trunc as u32)
}

fn read_required_u32<'a, 'b>(
  scope: &mut v8::PinScope<'a, '_>,
  obj: v8::Local<'a, v8::Object>,
  field: &'static str,
  prefix: Cow<'static, str>,
  context: &ContextFn<'b>,
) -> Result<u32, WebIdlError> {
  let key = v8_str(scope, field);
  let val = obj
    .get(scope, key.into())
    .unwrap_or_else(|| v8::undefined(scope).into());
  if val.is_undefined() {
    return Err(WebIdlError::other(
      prefix,
      context.borrowed(),
      JsErrorBox::type_error(format!("required dictionary member '{field}'")),
    ));
  }
  to_enforce_range_u32(scope, val).ok_or_else(|| {
    WebIdlError::other(
      prefix,
      context.borrowed(),
      JsErrorBox::type_error(format!(
        "'{field}' is outside the [0, 2**32-1] range"
      )),
    )
  })
}

fn read_optional_u32<'a, 'b>(
  scope: &mut v8::PinScope<'a, '_>,
  obj: v8::Local<'a, v8::Object>,
  field: &'static str,
  prefix: Cow<'static, str>,
  context: &ContextFn<'b>,
) -> Result<Option<u32>, WebIdlError> {
  let key = v8_str(scope, field);
  let val = obj
    .get(scope, key.into())
    .unwrap_or_else(|| v8::undefined(scope).into());
  if val.is_undefined() || val.is_null() {
    return Ok(None);
  }
  match to_enforce_range_u32(scope, val) {
    Some(v) => Ok(Some(v)),
    None => Err(WebIdlError::other(
      prefix,
      context.borrowed(),
      JsErrorBox::type_error(format!(
        "'{field}' is outside the [0, 2**32-1] range"
      )),
    )),
  }
}

/// Validate the per-algorithm prerequisites (key type, iv length, tag
/// length, counter length, etc.) and dispatch to the existing
/// [`crate::encrypt`] backend helpers.
pub fn run(
  params: SubtleEncryptParams,
  key: SubtleKey,
  data: Vec<u8>,
) -> Result<Vec<u8>, CryptoError> {
  if params.canonical_name() != key.algorithm_name {
    return Err(invalid_access(format!(
      "Encryption algorithm '{}' does not match key algorithm",
      params.canonical_name()
    )));
  }
  if !key.has_usage("encrypt") {
    return Err(invalid_access(
      "The requested operation is not valid for the provided key".to_string(),
    ));
  }

  match params {
    SubtleEncryptParams::RsaOaep { label } => {
      if key.key_type != CryptoKeyType::Public {
        return Err(invalid_access("Key type not supported".to_string()));
      }
      let hash = key.algorithm_hash.ok_or_else(|| {
        op_error("RSA-OAEP key is missing 'hash'".to_string())
      })?;
      encrypt::encrypt_rsa_oaep(
        &key.raw,
        hash,
        label.unwrap_or_default(),
        &data,
      )
      .map_err(encrypt_error_to_crypto)
    }
    SubtleEncryptParams::AesCbc { iv } => {
      if iv.len() != 16 {
        return Err(op_error(
          "Initialization vector must be 16 bytes".to_string(),
        ));
      }
      let length = key.algorithm_length.ok_or_else(|| {
        op_error("AES-CBC key is missing 'length'".to_string())
      })?;
      encrypt::encrypt_aes_cbc(&key.raw, length as usize, iv, &data)
        .map_err(encrypt_error_to_crypto)
    }
    SubtleEncryptParams::AesCtr { counter, length } => {
      if counter.len() != 16 {
        return Err(op_error("Counter vector must be 16 bytes".to_string()));
      }
      if length == 0 || length > 128 {
        return Err(op_error(
          "Counter length must not be 0 or greater than 128".to_string(),
        ));
      }
      let key_length = key.algorithm_length.ok_or_else(|| {
        op_error("AES-CTR key is missing 'length'".to_string())
      })?;
      encrypt::encrypt_aes_ctr(
        &key.raw,
        key_length as usize,
        &counter,
        length as usize,
        &data,
      )
      .map_err(encrypt_error_to_crypto)
    }
    SubtleEncryptParams::AesGcm {
      iv,
      additional_data,
      tag_length,
    } => {
      if data.len() > ((1u64 << 39) - 256) as usize {
        return Err(op_error("Plaintext too large".to_string()));
      }
      // Spec order: validate `tagLength` before `iv` length so a bad
      // tag length combined with an unsupported IV length rejects with
      // the `OperationError` WebCrypto requires (WPT
      // `aes_gcm_256_iv` "illegal tag length" subtests). Mirrors the
      // decrypt path.
      let tag_length = match tag_length {
        None => 128u32,
        Some(t) if [32, 64, 96, 104, 112, 120, 128].contains(&t) => t,
        Some(t) => {
          return Err(op_error(format!("Invalid tag length: {t}")));
        }
      };
      let iv_len = iv.len();
      if iv_len != 12 && iv_len != 16 {
        return Err(not_supported(
          "Initialization vector length not supported".to_string(),
        ));
      }
      let key_length = key.algorithm_length.ok_or_else(|| {
        op_error("AES-GCM key is missing 'length'".to_string())
      })?;
      encrypt::encrypt_aes_gcm(
        &key.raw,
        key_length as usize,
        tag_length as usize,
        iv,
        additional_data,
        &data,
      )
      .map_err(encrypt_error_to_crypto)
    }
    SubtleEncryptParams::AesOcb {
      iv,
      additional_data,
      tag_length,
    } => {
      if data.len() > ((1u64 << 39) - 256) as usize {
        return Err(op_error("Plaintext too large".to_string()));
      }
      let iv_len = iv.len();
      if !(6..=15).contains(&iv_len) {
        return Err(op_error(
          "Invalid nonce length for AES-OCB (must be 6-15 bytes)".to_string(),
        ));
      }
      let tag_length = match tag_length {
        None => 128u32,
        Some(t) if [64, 96, 128].contains(&t) => t,
        Some(t) => {
          return Err(op_error(format!("Invalid tag length: {t}")));
        }
      };
      let key_length = key.algorithm_length.ok_or_else(|| {
        op_error("AES-OCB key is missing 'length'".to_string())
      })?;
      encrypt::encrypt_aes_ocb(
        &key.raw,
        key_length as usize,
        tag_length as usize,
        iv,
        additional_data,
        &data,
      )
      .map_err(encrypt_error_to_crypto)
    }
    SubtleEncryptParams::ChaCha20Poly1305 {
      iv,
      additional_data,
      tag_length,
    } => {
      // Match the AES-GCM/OCB "Plaintext too large" cap for parity with
      // the legacy JS impl. RFC 8439 §2.8 caps ChaCha20-Poly1305 at
      // `(2^32 - 1) * 64` bytes (< 2^39); the AES cap is the tighter
      // value the spec uses for AES-GCM, and ArrayBuffer maxes well
      // below either limit, so this is defense in depth.
      if data.len() > ((1u64 << 39) - 256) as usize {
        return Err(op_error("Plaintext too large".to_string()));
      }
      let Some(iv) = iv else {
        return Err(CryptoError::Other(JsErrorBox::type_error(
          "iv is required",
        )));
      };
      if iv.len() != 12 {
        return Err(op_error(
          "ChaCha20-Poly1305 iv must be 12 bytes".to_string(),
        ));
      }
      if let Some(t) = tag_length
        && t != 128
      {
        return Err(op_error(
          "ChaCha20-Poly1305 tagLength must be 128".to_string(),
        ));
      }
      encrypt::encrypt_chacha20_poly1305(&key.raw, &iv, additional_data, &data)
        .map_err(encrypt_error_to_crypto)
    }
    SubtleEncryptParams::Unknown(name) => {
      Err(CryptoError::Other(JsErrorBox::new(
        "DOMExceptionNotSupportedError",
        format!("Algorithm '{name}' is not supported"),
      )))
    }
  }
}

fn invalid_access(msg: String) -> CryptoError {
  CryptoError::Other(JsErrorBox::new("DOMExceptionInvalidAccessError", msg))
}

fn op_error(msg: String) -> CryptoError {
  CryptoError::Other(JsErrorBox::new("DOMExceptionOperationError", msg))
}

fn not_supported(msg: String) -> CryptoError {
  CryptoError::Other(JsErrorBox::new("DOMExceptionNotSupportedError", msg))
}

fn encrypt_error_to_crypto(e: encrypt::EncryptError) -> CryptoError {
  CryptoError::Other(JsErrorBox::from_err(e))
}
