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

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 serde::de::DeserializeOwned;

use crate::http_util;
use crate::http_util::HttpClient;

#[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, "{} ({})", self.message, self.code)?;
    if let Some(x_deno_ray) = &self.x_deno_ray {
      write!(f, "[x-deno-ray: {}]", 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,
) -> String {
  format!("{}scopes/{}/packages/{}", registry_api_url, scope, package)
}

pub fn get_package_version_api_url(
  registry_api_url: &Url,
  scope: &str,
  package: &str,
  version: &str,
  params: Option<&str>,
) -> String {
  if let Some(params) = params {
    format!(
      "{}scopes/{}/packages/{}/versions/{}?{}",
      registry_api_url, scope, package, version, params
    )
  } else {
    format!(
      "{}scopes/{}/packages/{}/versions/{}",
      registry_api_url, scope, package, version
    )
  }
}

pub async fn get_package(
  client: &HttpClient,
  registry_api_url: &Url,
  scope: &str,
  package: &str,
) -> Result<http::Response<deno_fetch::ResBody>, AnyError> {
  let package_url = get_package_api_url(registry_api_url, scope, package);
  let response = client.get(package_url.parse()?)?.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> {
  let Some((scope, package)) = name
    .strip_prefix('@')
    .and_then(|no_at| no_at.split_once('/'))
  else {
    bail!("Invalid package name, use '@<scope_name>/<package_name>' format");
  };
  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.parse()?)?.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!(parse_package_name("deno/doc").is_err());
    assert!(parse_package_name("@deno").is_err());
  }
}
