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

use std::sync::Arc;

use boxed_error::Boxed;
use deno_core::OpState;
use deno_core::ToV8;
use deno_core::futures::StreamExt;
use deno_core::op2;
use deno_core::url::Url;
use deno_fetch::FetchError;
use deno_fetch::data_url::DataUrl;
use deno_web::BlobStoreTrait;
use http_body_util::BodyExt;
use hyper::body::Bytes;

use crate::web_worker::WebWorkerInternalHandle;
use crate::web_worker::WorkerThreadType;

// TODO(andreubotella) Properly parse the MIME type
fn mime_type_essence(mime_type: &str) -> String {
  let essence = match mime_type.split_once(';') {
    Some((essence, _)) => essence,
    None => mime_type,
  };
  essence.trim().to_ascii_lowercase()
}

#[derive(Debug, Boxed, deno_error::JsError)]
pub struct SyncFetchError(pub Box<SyncFetchErrorKind>);

#[derive(Debug, thiserror::Error, deno_error::JsError)]
pub enum SyncFetchErrorKind {
  #[class(type)]
  #[error("Blob URLs are not supported in this context.")]
  BlobUrlsNotSupportedInContext,
  #[class(inherit)]
  #[error("{0}")]
  Io(
    #[from]
    #[inherit]
    std::io::Error,
  ),
  #[class(type)]
  #[error("Invalid script URL")]
  InvalidScriptUrl,
  #[class(type)]
  #[error("http status error: {0}")]
  InvalidStatusCode(http::StatusCode),
  #[class(type)]
  #[error("Classic scripts with scheme {0}: are not supported in workers")]
  ClassicScriptSchemeUnsupportedInWorkers(String),
  #[class(generic)]
  #[error("{0}")]
  InvalidUri(#[from] http::uri::InvalidUri),
  #[class("DOMExceptionNetworkError")]
  #[error("Invalid MIME type {0:?}.")]
  InvalidMimeType(String),
  #[class("DOMExceptionNetworkError")]
  #[error("Missing MIME type.")]
  MissingMimeType,
  #[class(inherit)]
  #[error(transparent)]
  Fetch(
    #[from]
    #[inherit]
    FetchError,
  ),
  #[class(inherit)]
  #[error(transparent)]
  Join(
    #[from]
    #[inherit]
    tokio::task::JoinError,
  ),
  #[class(inherit)]
  #[error(transparent)]
  Other(#[inherit] deno_error::JsErrorBox),
}

#[derive(ToV8)]
pub struct SyncFetchScript {
  url: String,
  script: String,
}

#[op2]
pub fn op_worker_sync_fetch(
  state: &mut OpState,
  #[scoped] scripts: Vec<String>,
  loose_mime_checks: bool,
) -> Result<Vec<SyncFetchScript>, SyncFetchError> {
  let handle = state.borrow::<WebWorkerInternalHandle>().clone();
  assert_eq!(handle.worker_type, WorkerThreadType::Classic);

  let client = deno_fetch::get_or_create_client_from_state(state)
    .map_err(FetchError::ClientCreate)?;

  // TODO(andreubotella) It's not good to throw an exception related to blob
  // URLs when none of the script URLs use the blob scheme.
  // Also, in which contexts are blob URLs not supported?
  let blob_store = state
    .try_borrow::<Arc<dyn BlobStoreTrait>>()
    .ok_or(SyncFetchErrorKind::BlobUrlsNotSupportedInContext)?
    .clone();

  // TODO(andreubotella): make the below thread into a resource that can be
  // re-used. This would allow parallel fetching of multiple scripts.

  let thread = std::thread::spawn(move || {
    let runtime = tokio::runtime::Builder::new_current_thread()
      .enable_io()
      .enable_time()
      .build()?;

    runtime.block_on(async move {
      let mut futures = scripts
        .into_iter()
        .map(|script| {
          let client = client.clone();
          let blob_store = blob_store.clone();
          let maybe_main_module_blob = handle.maybe_main_module_blob.clone();
          deno_core::unsync::spawn(async move {
            let script_url = Url::parse(&script)
              .map_err(|_| SyncFetchErrorKind::InvalidScriptUrl)?;
            let mut loose_mime_checks = loose_mime_checks;

            let (body, mime_type, res_url) = match script_url.scheme() {
              "http" | "https" => {
                let mut req = http::Request::new(deno_fetch::ReqBody::empty());
                *req.uri_mut() = script_url.as_str().parse()?;

                let resp =
                  client.send(req).await.map_err(FetchError::ClientSend)?;

                if resp.status().is_client_error()
                  || resp.status().is_server_error()
                {
                  return Err(SyncFetchErrorKind::InvalidStatusCode(
                    resp.status(),
                  ));
                }

                // TODO(andreubotella) Properly run fetch's "extract a MIME type".
                let mime_type = resp
                  .headers()
                  .get("Content-Type")
                  .and_then(|v| v.to_str().ok())
                  .map(mime_type_essence);

                // Always check the MIME type with HTTP(S).
                loose_mime_checks = false;

                let body = resp
                  .collect()
                  .await
                  .map_err(SyncFetchErrorKind::Other)?
                  .to_bytes();

                (body, mime_type, script)
              }
              "data" => {
                let data_url =
                  DataUrl::process(&script).map_err(FetchError::DataUrl)?;

                let mime_type = {
                  let mime = data_url.mime_type();
                  format!("{}/{}", mime.type_, mime.subtype)
                };

                let (body, _) =
                  data_url.decode_to_vec().map_err(FetchError::Base64)?;

                (Bytes::from(body), Some(mime_type), script)
              }
              "blob" => {
                let blob = maybe_main_module_blob
                  .as_ref()
                  .filter(|(blob_specifier, _)| *blob_specifier == script_url)
                  .map(|(_, blob)| blob.clone())
                  .or_else(|| blob_store.get_object_url(script_url))
                  .ok_or(FetchError::BlobNotFound)?;

                let mime_type = mime_type_essence(&blob.media_type);

                let body = blob.read_all().await;

                (Bytes::from(body), Some(mime_type), script)
              }
              _ => {
                return Err(
                  SyncFetchErrorKind::ClassicScriptSchemeUnsupportedInWorkers(
                    script_url.scheme().to_string(),
                  ),
                );
              }
            };

            if !loose_mime_checks {
              // TODO(andreubotella) Check properly for a Javascript MIME type.
              match mime_type.as_deref() {
                Some("application/javascript" | "text/javascript") => {}
                Some(mime_type) => {
                  return Err(SyncFetchErrorKind::InvalidMimeType(
                    mime_type.to_string(),
                  ));
                }
                None => return Err(SyncFetchErrorKind::MissingMimeType),
              }
            }

            let (text, _) = encoding_rs::UTF_8.decode_with_bom_removal(&body);

            Ok(SyncFetchScript {
              url: res_url,
              script: text.into_owned(),
            })
          })
        })
        .collect::<deno_core::futures::stream::FuturesUnordered<_>>();
      let mut ret = Vec::with_capacity(futures.len());
      while let Some(result) = futures.next().await {
        let script = result??;
        ret.push(script);
      }
      Ok(ret)
    })
  });

  thread.join().unwrap()
}
