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

use deno_core::anyhow::Context;
use deno_core::anyhow::bail;
use deno_core::error::AnyError;
use deno_core::serde_json;
use deno_core::url::Url;
use deno_runtime::deno_fetch;
use deno_semver::jsr::JsrPackageReqReference;
use serde::de::DeserializeOwned;

use crate::http_util;
use crate::http_util::HttpClient;
use crate::util::console::escape_terminal_control_chars;

#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateAuthorizationResponse {
  pub verification_url: String,
  pub code: String,
  pub exchange_token: String,
  pub poll_interval: u64,
}

#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExchangeAuthorizationResponse {
  pub token: String,
  pub user: User,
}

#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct User {
  pub name: String,
}

#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OidcTokenResponse {
  pub value: String,
}

#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PublishingTaskError {
  #[allow(dead_code, reason = "currently unused")]
  pub code: String,
  pub message: String,
}

#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PublishingTask {
  pub id: String,
  pub status: String,
  pub error: Option<PublishingTaskError>,
}

#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApiError {
  pub code: String,
  pub message: String,
  #[serde(flatten)]
  pub data: serde_json::Value,
  #[serde(skip)]
  pub x_deno_ray: Option<String>,
}

impl std::fmt::Display for ApiError {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    write!(
      f,
      "{} ({})",
      escape_terminal_control_chars(&self.message),
      escape_terminal_control_chars(&self.code)
    )?;
    if let Some(x_deno_ray) = &self.x_deno_ray {
      write!(
        f,
        "[x-deno-ray: {}]",
        escape_terminal_control_chars(x_deno_ray)
      )?;
    }
    Ok(())
  }
}

impl std::fmt::Debug for ApiError {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    std::fmt::Display::fmt(self, f)
  }
}

impl std::error::Error for ApiError {}

pub async fn parse_response<T: DeserializeOwned>(
  response: http::Response<deno_fetch::ResBody>,
) -> Result<T, ApiError> {
  let status = response.status();
  let x_deno_ray = response
    .headers()
    .get("x-deno-ray")
    .and_then(|value| value.to_str().ok())
    .map(|s| s.to_string());
  let text = http_util::body_to_string(response).await.unwrap();

  if !status.is_success() {
    match serde_json::from_str::<ApiError>(&text) {
      Ok(mut err) => {
        err.x_deno_ray = x_deno_ray;
        return Err(err);
      }
      Err(_) => {
        let err = ApiError {
          code: "unknown".to_string(),
          message: format!("{}: {}", status, text),
          x_deno_ray,
          data: serde_json::json!({}),
        };
        return Err(err);
      }
    }
  }

  serde_json::from_str(&text).map_err(|err| ApiError {
    code: "unknown".to_string(),
    message: format!("Failed to parse response: {}, response: '{}'", err, text),
    x_deno_ray,
    data: serde_json::json!({}),
  })
}

pub fn get_package_api_url(
  registry_api_url: &Url,
  scope: &str,
  package: &str,
) -> Result<Url, AnyError> {
  append_path_segments(
    registry_api_url,
    &["scopes", scope, "packages", package],
  )
}

pub fn get_package_version_api_url(
  registry_api_url: &Url,
  scope: &str,
  package: &str,
  version: &str,
  config_path: Option<&str>,
) -> Result<Url, AnyError> {
  let mut url = append_path_segments(
    registry_api_url,
    &["scopes", scope, "packages", package, "versions", version],
  )?;
  if let Some(config_path) = config_path {
    url.query_pairs_mut().append_pair("config", config_path);
  }
  Ok(url)
}

pub fn get_package_version_provenance_api_url(
  registry_api_url: &Url,
  scope: &str,
  package: &str,
  version: &str,
) -> Result<Url, AnyError> {
  append_path_segments(
    registry_api_url,
    &[
      "scopes",
      scope,
      "packages",
      package,
      "versions",
      version,
      "provenance",
    ],
  )
}

fn append_path_segments(
  base_url: &Url,
  segments: &[&str],
) -> Result<Url, AnyError> {
  let mut url = base_url.clone();
  url.set_query(None);
  url.set_fragment(None);
  url
    .path_segments_mut()
    .map_err(|_| {
      deno_core::anyhow::anyhow!(
        "Registry API URL cannot be used as a base URL"
      )
    })?
    .pop_if_empty()
    .extend(segments);
  Ok(url)
}

pub async fn get_package(
  client: &HttpClient,
  registry_api_url: &Url,
  scope: &str,
  package: &str,
  authorization: Option<&str>,
) -> Result<http::Response<deno_fetch::ResBody>, AnyError> {
  let package_url = get_package_api_url(registry_api_url, scope, package)?;
  let mut request = client.get(package_url)?;
  // The registry responds with a 404 for private packages unless the request
  // is authenticated as someone with access, so authenticate when we can.
  if let Some(authorization) = authorization {
    request = request.header(
      http::header::AUTHORIZATION,
      authorization
        .parse()
        .context("Failed to parse authorization header")?,
    );
  }
  let response = request.send().await?;
  Ok(response)
}

/// Splits a fully qualified JSR package name (e.g. `@scope/package`) into its
/// `(scope, package)` parts.
pub fn parse_package_name(name: &str) -> Result<(&str, &str), AnyError> {
  // Keep the explicit path-safety checks below even if the JSR grammar changes.
  let reference = JsrPackageReqReference::from_str(&format!("jsr:{name}@*"))
    .map_err(|_| {
      deno_core::anyhow::anyhow!(
        "package name must use the '@<scope>/<package>' format"
      )
    })?;
  if reference.sub_path().is_some() {
    bail!("package name must not contain additional path segments");
  }

  let Some((scope, package)) =
    name.strip_prefix('@').and_then(|name| name.split_once('/'))
  else {
    bail!("package name must use the '@<scope>/<package>' format");
  };
  for component in [scope, package] {
    if component == "." || component == ".." {
      bail!("package name must not contain dot path segments");
    }
    if component
      .chars()
      .any(|c| matches!(c, '/' | '\\' | '?' | '#' | '%'))
    {
      bail!("package name contains a URL path or delimiter character");
    }
  }
  Ok((scope, package))
}

/// Returns `true` if the given package version is already published to the
/// registry.
///
/// Only a `200 OK` response is treated as "already published". A `404` (and any
/// other non-success status) is treated as "not published" so that this
/// up-front optimization never blocks a legitimate publish because of a
/// transient registry error.
pub async fn check_version_exists(
  client: &HttpClient,
  registry_api_url: &Url,
  scope: &str,
  package: &str,
  version: &str,
) -> Result<bool, AnyError> {
  let url = get_package_version_api_url(
    registry_api_url,
    scope,
    package,
    version,
    None,
  )?;
  let response = client.get(url)?.send().await?;
  Ok(response.status() == 200)
}

pub fn get_jsr_alternative(imported: &Url) -> Option<String> {
  if matches!(imported.host_str(), Some("esm.sh")) {
    let mut segments = imported.path_segments()?;
    match segments.next()? {
      "gh" => None,
      module => Some(format!("\"npm:{module}\"")),
    }
  } else if imported.as_str().starts_with("https://deno.land/") {
    let mut segments = imported.path_segments()?;
    let maybe_std = segments.next()?;
    if maybe_std != "std" && !maybe_std.starts_with("std@") {
      return None;
    }
    let module = segments.next()?;
    let export = segments
      .next()
      .filter(|s| *s != "mod.ts")
      .map(|s| s.strip_suffix(".ts").unwrap_or(s).replace("_", "-"));
    Some(format!(
      "\"jsr:@std/{}@1{}\"",
      module,
      export.map(|s| format!("/{}", s)).unwrap_or_default()
    ))
  } else {
    None
  }
}

#[cfg(test)]
mod test {
  use super::*;

  #[test]
  fn test_jsr_alternative() {
    #[track_caller]
    fn run_test(imported: &str, output: Option<&str>) {
      let imported = Url::parse(imported).unwrap();
      let output = output.map(|s| s.to_string());
      assert_eq!(get_jsr_alternative(&imported), output);
    }

    run_test("https://esm.sh/ts-morph", Some("\"npm:ts-morph\""));
    run_test(
      "https://deno.land/std/path/mod.ts",
      Some("\"jsr:@std/path@1\""),
    );
    run_test(
      "https://deno.land/std/path/join.ts",
      Some("\"jsr:@std/path@1/join\""),
    );
    run_test(
      "https://deno.land/std@0.229.0/path/join.ts",
      Some("\"jsr:@std/path@1/join\""),
    );
    run_test(
      "https://deno.land/std@0.229.0/path/something_underscore.ts",
      Some("\"jsr:@std/path@1/something-underscore\""),
    );
  }

  #[test]
  fn test_parse_package_name() {
    assert_eq!(parse_package_name("@deno/doc").unwrap(), ("deno", "doc"));
    assert_eq!(
      parse_package_name("@scope-1/package-2").unwrap(),
      ("scope-1", "package-2")
    );
    assert_eq!(parse_package_name("@a/b").unwrap(), ("a", "b"));

    for invalid_name in [
      "deno/doc",
      "@deno",
      "@deno/../doc",
      "@deno/doc/other",
      "@deno/doc?other",
      "@deno/doc#other",
      "@deno/doc\\other",
      "@deno/doc%2Fother",
    ] {
      assert!(
        parse_package_name(invalid_name).is_err(),
        "{invalid_name} should be invalid"
      );
    }
  }

  #[test]
  fn package_api_urls_use_path_segments() {
    let base = Url::parse("https://registry.example/custom/api/").unwrap();
    let package_url = get_package_api_url(&base, "scope", "package").unwrap();
    assert_eq!(
      package_url.as_str(),
      "https://registry.example/custom/api/scopes/scope/packages/package"
    );

    let version_url = get_package_version_api_url(
      &base,
      "scope",
      "package",
      "1.2.3+build.1",
      Some("/deno.json"),
    )
    .unwrap();
    assert_eq!(
      version_url.path(),
      "/custom/api/scopes/scope/packages/package/versions/1.2.3+build.1"
    );
    assert_eq!(
      version_url.query_pairs().collect::<Vec<_>>(),
      vec![("config".into(), "/deno.json".into())]
    );

    let provenance_url = get_package_version_provenance_api_url(
      &base, "scope", "package", "1.2.3",
    )
    .unwrap();
    assert_eq!(
      provenance_url.as_str(),
      "https://registry.example/custom/api/scopes/scope/packages/package/versions/1.2.3/provenance"
    );
  }

  #[test]
  fn package_api_url_components_cannot_change_the_endpoint() {
    let base = Url::parse("https://registry.example/custom/api/").unwrap();
    let url = get_package_version_api_url(
      &base,
      "scope/../../other",
      "package?mode=other#fragment",
      "1.0.0/../../other",
      None,
    )
    .unwrap();
    assert_eq!(
      url.origin().ascii_serialization(),
      "https://registry.example"
    );
    assert_eq!(
      url.as_str(),
      "https://registry.example/custom/api/scopes/scope%2F..%2F..%2Fother/packages/package%3Fmode=other%23fragment/versions/1.0.0%2F..%2F..%2Fother"
    );
  }

  #[test]
  fn api_error_display_escapes_terminal_controls() {
    let err = ApiError {
      code: "bad\u{202e}code".to_string(),
      message: "failed\x1b[2J\nagain".to_string(),
      data: serde_json::json!({}),
      x_deno_ray: Some("ray\u{009b}31m".to_string()),
    };

    let display = err.to_string();
    assert_eq!(
      display,
      r"failed\u{1b}[2J\nagain (bad\u{202e}code)[x-deno-ray: ray\u{9b}31m]"
    );
    assert_eq!(format!("{err:?}"), display);
    assert_eq!(err.code, "bad\u{202e}code");
  }
}
