///|
enum GitHubImportProvider {
  GhCli
  JsonFile
}

///|
fn parse_github_import_provider(value : String) -> GitHubImportProvider? {
  match value.to_lower() {
    "gh" | "gh-cli" => Some(GitHubImportProvider::GhCli)
    "json-file" | "file" => Some(GitHubImportProvider::JsonFile)
    _ => None
  }
}

///|
fn parse_github_import_provider_option(
  value : String,
) -> GitHubImportProvider raise @bitcore.GitError {
  match parse_github_import_provider(value) {
    Some(provider) => provider
    None =>
      raise @bitcore.GitError::InvalidObject(
        "Unknown GitHub import provider: \{value}",
      )
  }
}

///|
fn read_import_json_from_file(
  fs : OsFs,
  context : String,
  input_path : String?,
) -> (String, String) raise @bitcore.GitError {
  guard input_path is Some(path) else {
    raise @bitcore.GitError::InvalidObject(
      "\{context}: --input requires a file path when --provider=json-file",
    )
  }
  let bytes = fs.read_file(path) catch {
    err =>
      raise @bitcore.GitError::IoError(
        "\{context}: failed to read --input file '\{path}': \{err}",
      )
  }
  (decode_bytes(bytes), "")
}

///|
fn resolve_github_repo(
  fs : OsFs,
  git_dir : String,
  repo : String?,
) -> String raise @bitcore.GitError {
  match repo {
    Some(name) => name
    None => {
      let remote_url = get_effective_remote_url(fs, git_dir)
      guard remote_url is Some(url) else {
        raise @bitcore.GitError::InvalidObject(
          "No remote configured; use --repo",
        )
      }
      match @hq.parse_repo_url(url) {
        Some(parsed) => {
          let host = parsed.host_name()
          if host != "github.com" {
            raise @bitcore.GitError::InvalidObject(
              "Only github.com is supported for gh import",
            )
          }
          parsed.full_name()
        }
        None =>
          raise @bitcore.GitError::InvalidObject(
            "Cannot parse remote URL: \{url}",
          )
      }
    }
  }
}

///|
async fn gh_pr_list(
  repo : String,
  state : String,
  limit : Int,
) -> (String, String) raise @bitcore.GitError {
  let fields = "number,title,body,headRefName,headRefOid,headRepository,baseRefName,baseRefOid,author,createdAt,updatedAt,state,labels"
  let jq = "[.[] | {id:(.number|tostring), title:.title, body:(.body // \"\"), source_branch:.headRefName, source_ref:(if (.headRefName // \"\") == \"\" then \"\" else (\"refs/heads/\" + .headRefName) end), source_repo:((.headRepository // {}) | .nameWithOwner // \"\"), source_commit:.headRefOid, target_branch:.baseRefName, target_commit:.baseRefOid, author:(.author.login // \"unknown\"), created_at:(.createdAt|fromdateiso8601), updated_at:(.updatedAt|fromdateiso8601), state:(.state|ascii_downcase), labels:(.labels|map(.name))}]"
  let args = [
    "pr",
    "list",
    "--state",
    state,
    "--limit",
    limit.to_string(),
    "--json",
    fields,
    "--jq",
    jq,
    "--repo",
    repo,
  ]
  gh_collect_output(args)
}

///|
async fn gh_issue_list(
  repo : String,
  state : String,
  limit : Int,
) -> (String, String) raise @bitcore.GitError {
  let fields = "number,title,body,author,createdAt,updatedAt,state,labels,assignees"
  let jq = "[.[] | {id:(.number|tostring), title:.title, body:(.body // \"\"), author:(.author.login // \"unknown\"), created_at:(.createdAt|fromdateiso8601), updated_at:(.updatedAt|fromdateiso8601), state:(.state|ascii_downcase), labels:(.labels|map(.name)), assignees:(.assignees|map(.login))}]"
  let args = [
    "issue",
    "list",
    "--state",
    state,
    "--limit",
    limit.to_string(),
    "--json",
    fields,
    "--jq",
    jq,
    "--repo",
    repo,
  ]
  gh_collect_output(args)
}

///|
async fn gh_collect_output(
  args : Array[String],
) -> (String, String) raise @bitcore.GitError {
  let (code, stdout, stderr) = @process.collect_output(
    "gh",
    args,
    inherit_env=true,
  ) catch {
    err => raise @bitcore.GitError::IoError("gh failed: \{err}")
  }
  let out = stdout.text() catch {
    _ => ""
  }
  let err_text = stderr.text() catch {
    _ => ""
  }
  if code != 0 {
    let message = trim_string(err_text)
    raise @bitcore.GitError::InvalidObject("gh failed (\{code}): \{message}")
  }
  (out, err_text)
}

///|
fn parse_json_array(text : String) -> Array[Json] raise @bitcore.GitError {
  let parsed = @json.parse(text) catch {
    err =>
      raise @bitcore.GitError::InvalidObject(
        "Invalid JSON from gh: \{err.to_string()}",
      )
  }
  match parsed {
    Json::Array(items) => items
    _ => raise @bitcore.GitError::InvalidObject("Expected JSON array from gh")
  }
}

///|
fn json_get_string(
  obj : Map[String, Json],
  key : String,
  fallback : String,
) -> String {
  match obj.get(key) {
    Some(Json::String(value)) => value
    Some(Json::Number(value, ..)) => value.to_string()
    Some(Json::True) => "true"
    Some(Json::False) => "false"
    _ => fallback
  }
}

///|
fn json_get_int64(
  obj : Map[String, Json],
  key : String,
  fallback : Int64,
) -> Int64 {
  match obj.get(key) {
    Some(Json::Number(value, ..)) => value.to_int64()
    Some(Json::String(value)) =>
      @string.parse_int64(value) catch {
        _ => fallback
      }
    _ => fallback
  }
}

///|
fn json_get_string_array(
  obj : Map[String, Json],
  key : String,
) -> Array[String] {
  match obj.get(key) {
    Some(Json::Array(items)) => {
      let out : Array[String] = []
      for item in items {
        match item {
          Json::String(value) => out.push(value)
          _ => ()
        }
      }
      out
    }
    _ => []
  }
}

///|
fn parse_gh_prs(
  text : String,
) -> Array[@hub.PullRequest] raise @bitcore.GitError {
  let items = parse_json_array(text)
  let out : Array[@hub.PullRequest] = []
  for item in items {
    match item {
      Json::Object(obj) => {
        let id = json_get_string(obj, "id", "")
        if id.length() == 0 {
          continue
        }
        let title = json_get_string(obj, "title", "")
        let body = json_get_string(obj, "body", "")
        let source_branch = json_get_string(obj, "source_branch", "")
        let source_ref_raw = json_get_string(obj, "source_ref", "")
        let source_repo_raw = json_get_string(obj, "source_repo", "")
        let target_branch = json_get_string(obj, "target_branch", "")
        let source_hex = json_get_string(obj, "source_commit", "")
        let target_hex = json_get_string(obj, "target_commit", "")
        if source_branch.length() == 0 || target_branch.length() == 0 {
          raise @bitcore.GitError::InvalidObject(
            "Missing branch name for PR \{id}",
          )
        }
        let source_commit = @bitcore.ObjectId::from_hex(source_hex) catch {
          _ =>
            raise @bitcore.GitError::InvalidObject(
              "Invalid source commit for PR \{id}",
            )
        }
        let target_commit = @bitcore.ObjectId::from_hex(target_hex) catch {
          _ =>
            raise @bitcore.GitError::InvalidObject(
              "Invalid target commit for PR \{id}",
            )
        }
        let author = json_get_string(obj, "author", "unknown")
        let created_at = json_get_int64(obj, "created_at", 0L)
        let updated_at = json_get_int64(obj, "updated_at", created_at)
        let labels = json_get_string_array(obj, "labels")
        let state_raw = json_get_string(obj, "state", "open").to_lower()
        let state = match parse_pr_state(state_raw) {
          Some(s) => s
          None => @hub.PrState::Open
        }
        let source_repo = if source_repo_raw.length() == 0 {
          None
        } else {
          Some(source_repo_raw)
        }
        let source_ref = if source_ref_raw.length() == 0 {
          None
        } else {
          Some(source_ref_raw)
        }
        let pr = @hub.PullRequest::new(
          id,
          title,
          body,
          source_branch,
          source_commit,
          target_branch,
          target_commit,
          author,
          created_at,
          updated_at,
          state,
          labels,
          source_repo~,
          source_ref~,
        )
        out.push(pr)
      }
      _ => ()
    }
  }
  out
}

///|
fn parse_gh_issues(text : String) -> Array[@hub.Issue] raise @bitcore.GitError {
  let items = parse_json_array(text)
  let out : Array[@hub.Issue] = []
  for item in items {
    match item {
      Json::Object(obj) => {
        let id = json_get_string(obj, "id", "")
        if id.length() == 0 {
          continue
        }
        let title = json_get_string(obj, "title", "")
        let body = json_get_string(obj, "body", "")
        let author = json_get_string(obj, "author", "unknown")
        let created_at = json_get_int64(obj, "created_at", 0L)
        let updated_at = json_get_int64(obj, "updated_at", created_at)
        let labels = json_get_string_array(obj, "labels")
        let assignees = json_get_string_array(obj, "assignees")
        let state_raw = json_get_string(obj, "state", "open").to_lower()
        let state = match parse_issue_state(state_raw) {
          Some(s) => s
          None => @hub.IssueState::Open
        }
        let issue = @hub.Issue::new(
          id,
          title,
          body,
          author,
          created_at,
          updated_at,
          state,
          labels~,
          assignees~,
        )
        out.push(issue)
      }
      _ => ()
    }
  }
  out
}

///|
fn parse_branch_ref_name(ref_name : String) -> String? {
  let text = trim_string(ref_name)
  if text.has_prefix("refs/heads/") {
    Some(String::unsafe_substring(text, start=11, end=text.length()))
  } else {
    None
  }
}

///|
fn parse_head_branch_name(head_content : String) -> String? {
  let text = trim_string(head_content)
  if text.has_prefix("ref: ") {
    parse_branch_ref_name(
      String::unsafe_substring(text, start=5, end=text.length()),
    )
  } else {
    None
  }
}

///|
fn detect_current_branch_name(git_dir : String) -> String? {
  let fs = OsFs::new()
  let head_bytes = fs.read_file(git_dir + "/HEAD") catch { _ => return None }
  parse_head_branch_name(decode_bytes(head_bytes))
}

///|
fn pr_matches_branch(pr : @hub.PullRequest, branch : String) -> Bool {
  let source_branch = pr.source_branch()
  let branch_ref = "refs/heads/" + branch
  if source_branch == branch || source_branch == branch_ref {
    return true
  }
  match pr.source_ref() {
    Some(source_ref) => source_ref == branch || source_ref == branch_ref
    None => false
  }
}