///|
/// Pull request details read from provider event metadata.
priv struct GithubPrEvent {
  branch : String?
  pr : String?
}

///|
/// Detect CI information from the current process environment.
///
/// If no known provider is detected, this falls back to local git metadata and
/// sets `is_ci` from the generic `CI` environment variable.
pub async fn env_ci(cwd? : String = ".") -> Info {
  env_ci_from(@env.get_env_vars(), cwd~)
}

///|
/// Detect CI information from an explicit environment map.
///
/// This is the deterministic entry point for tests and callers that already
/// captured an environment. Provider-specific environment variables are checked
/// first. If none match, the function runs small `git` commands in `cwd` to
/// fill `commit` and `branch`, matching the fallback behavior of `deno-ci`.
pub async fn env_ci_from(
  env : Map[String, String],
  cwd? : String = ".",
) -> Info {
  if env_has(env, "GITHUB_ACTION") {
    let pr_event = if env_get(env, "GITHUB_EVENT_NAME") == Some("pull_request") {
      read_github_pr_event(env)
    } else {
      None
    }
    return github_info(env, pr_event~)
  }

  if env_has(env, "CODEBUILD_BUILD_ID") {
    let head = git_head(env, cwd)
    let branch = git_branch(env, cwd)
    return { ..codebuild_info(env), commit: head, branch }
  }

  if env_has(env, "JENKINS_URL") {
    return jenkins_info(env, git_head=git_head(env, cwd))
  }

  if env_has(env, "SEMAPHORE") {
    return semaphore_info(env, git_head=git_head(env, cwd))
  }

  match detect_from_env(env) {
    Some(info) => info
    None => git_fallback_info(env, cwd)
  }
}

///|
/// Return whether the current process appears to be running in CI.
pub async fn is_ci(cwd? : String = ".") -> Bool {
  env_ci(cwd~).is_ci
}

///|
/// Return the current CI service identifier, or `unknown` for the git fallback.
pub async fn service(cwd? : String = ".") -> String {
  env_ci(cwd~).service
}

///|
/// Return the current CI service display name, or `unknown` for the git fallback.
pub async fn name(cwd? : String = ".") -> String {
  env_ci(cwd~).name
}

///|
async fn git_fallback_info(env : Map[String, String], cwd : String) -> Info {
  {
    ..base_info("unknown", "unknown", is_ci=env_has(env, "CI")),
    commit: git_head(env, cwd),
    branch: git_branch(env, cwd),
  }
}

///|
async fn git_head(env : Map[String, String], cwd : String) -> String? {
  git_stdout(env, cwd, ["rev-parse", "HEAD"])
}

///|
async fn git_branch(env : Map[String, String], cwd : String) -> String? {
  match git_stdout(env, cwd, ["rev-parse", "--abbrev-ref", "HEAD"]) {
    Some("HEAD") => detached_head_branch(env, cwd)
    other => other
  }
}

///|
async fn detached_head_branch(
  env : Map[String, String],
  cwd : String,
) -> String? {
  match git_stdout(env, cwd, ["show", "-s", "--pretty=%d", "HEAD"]) {
    Some(output) => origin_branch_from_decoration(output)
    None => None
  }
}

///|
async fn git_stdout(
  env : Map[String, String],
  cwd : String,
  args : Array[String],
) -> String? {
  try {
    let (status, stdout, _) = @process.collect_output(
      "git",
      args,
      extra_env=env,
      inherit_env=true,
      cwd~,
    )
    if status == 0 {
      let text = stdout.text().trim().to_owned()
      if text == "" {
        None
      } else {
        Some(text)
      }
    } else {
      None
    }
  } catch {
    _ => None
  }
}

///|
fn origin_branch_from_decoration(output : String) -> String? {
  let normalized = output.trim().to_owned()
  let without_open = match normalized.strip_prefix("(") {
    Some(value) => value.to_owned()
    None => normalized
  }
  let without_close = match without_open.strip_suffix(")") {
    Some(value) => value.to_owned()
    None => without_open
  }
  for part in without_close.split(", ") {
    let text = part.to_owned()
    match text.strip_prefix("origin/") {
      Some(branch) => return Some(branch.to_owned())
      None => ()
    }
  }
  None
}

///|
async fn read_github_pr_event(env : Map[String, String]) -> GithubPrEvent? {
  match env_get(env, "GITHUB_EVENT_PATH") {
    None => None
    Some(path) =>
      parse_github_pr_event(@json.parse(@fs.read_file(path).text())) catch {
        _ => None
      }
  }
}

///|
fn parse_github_pr_event(json : Json) -> GithubPrEvent? {
  match json {
    Object(fields) =>
      match fields.get("pull_request") {
        Some(Object(pr_fields)) => {
          let pr = json_stringish(pr_fields.get("number"))
          let branch = match pr_fields.get("base") {
            Some(Object(base_fields)) =>
              branch_from_ref(json_stringish(base_fields.get("ref")))
            _ => None
          }
          Some({ branch, pr })
        }
        _ => None
      }
    _ => None
  }
}

///|
fn json_stringish(value : Json?) -> String? {
  match value {
    Some(String(text)) => Some(text)
    Some(Number(_, repr=Some(repr))) => Some(repr)
    Some(Number(number, repr=None)) => Some(number.to_string())
    _ => None
  }
}