#!/usr/bin/env bash

:<<'DOC'
API for label taxonomy: merging org-wide base labels with per-repo extensions.

Produces a combined label set from two sources:
  1. Base definitions (schema/labels.base.json) -- org-wide Forgejo label categories
  2. Repo extensions (manifest.json .labels)   -- per-repo items and net-new categories

Linear-only taxonomy (e.g. blizz/* triage) lives in schema/labels.linear-blizz.json and is
merged only by the Blizz triage action, not by merged_labels() / Forgejo sync.

Repo categories use one of two forms:
  Flat form:  {"cli": "CLI tools", "lib": "Libraries"}
  Full form:  {"color": "#4477aa", "items": {"cli": "CLI tools"}}

Flat form extends an existing base category. Full form is for net-new categories
that need a color. The forms are distinguished by checking whether .items is an object.

Merge rules:
  - Base color is authoritative (repo cannot override it)
  - Items are unioned; repo descriptions win on conflict
  - Net-new categories without a color are rejected

Accessed via manifest_get("labels"), which delegates to merged_labels().

Sync support:
  flatten_labels()  -- converts the merged tree into a flat [{name, color, description}] array
  diff_labels()     -- compares desired (flat) vs actual (Forgejo) and produces create/update/archive lists
DOC

import "$SHELLOPS_DIR/lib/areas.api.sh"

__labels_api__BASE_FILE="$SHELLOPS_DIR/schema/labels.base.json"

:<<'DOC'
Returns the fully merged label set as a JSON object.

When the manifest defines an "areas" config, computed area labels are injected
into the repo labels before merging. Computed areas are unioned with any
manually declared area items (computed wins on conflict).

Usage: merged_labels
DOC
function merged_labels() {
  local base repo_labels

  base="$(__labels_api__load_base)" || return 1
  repo_labels="$(__labels_api__load_repo_labels)"
  repo_labels="$(__labels_api__inject_computed_areas "$repo_labels")"

  __labels_api__validate "$base" "$repo_labels" || return 1
  __labels_api__merge "$base" "$repo_labels"
}

function __labels_api__inject_computed_areas() {
  local repo_labels="$1"

  if ! has_areas_config; then
    echo "$repo_labels"
    return
  fi

  local config canonical_areas label_items
  config="$(load_areas_config)"
  canonical_areas="$(compute_areas "$config")" || { echo "$repo_labels"; return; }
  label_items="$(areas_to_label_items "$canonical_areas")"

  local existing_area
  existing_area="$(jq_get "$repo_labels" '.area' '{}')"

  local merged_area
  merged_area="$(jq_merge "$existing_area" "$label_items")"

  jq_set "$repo_labels" '.area' "$merged_area"
}

# ── Data loading ─────────────────────────────────────────────────────

function __labels_api__load_base() {
  if [[ ! -f "$__labels_api__BASE_FILE" ]]; then
    error "Base label file not found: $__labels_api__BASE_FILE"
    return 1
  fi
  cat "$__labels_api__BASE_FILE"
}

function __labels_api__load_repo_labels() {
  local manifest
  manifest="$(cat "$PROJ/manifest.json")"
  jq_get "$manifest" '.labels' '{}'
}

# ── Validation ───────────────────────────────────────────────────────

function __labels_api__validate() {
  local base="$1" repo_labels="$2"
  local entries missing count

  entries="$(jq_entries "$repo_labels")"
  missing="$(jq_reduce __labels_api__find_missing_colors "$entries" '[]' "$base")"
  count="$(jq_len "$missing")"

  if ((count > 0)); then
    error "Label categories not in base and missing color: $(jq_join "$missing" ", ")"
    return 1
  fi
}

function __labels_api__find_missing_colors() {
  local acc="$1" entry="$2" base="$3"
  local key base_cat

  key="$(jq_get "$entry" '.key')"
  base_cat="$(jq_get "$base" ".\"$key\"")"

  if jq_defined "$base_cat"; then
    echo "$acc"
    return
  fi

  local value
  value="$(jq_get "$entry" '.value')"

  if [[
    "$(jq_typeof "$value" '.items')" != "object" ||
    "$(jq_typeof "$value" '.color')" != "string"
  ]]; then
    jq_push "$acc" "\"$key\""
    return
  fi

  echo "$acc"
}

# ── Merge ────────────────────────────────────────────────────────────

function __labels_api__merge() {
  local base="$1" repo_labels="$2"
  local entries

  entries="$(jq_entries "$repo_labels")"
  jq_reduce __labels_api__merge_category "$entries" "$base"
}

:<<'DOC'
jq_reduce callback. Normalizes each repo category (flat vs full form),
then either merges items into an existing base category or adds a net-new one.
DOC
function __labels_api__merge_category() {
  local acc="$1" entry="$2"
  local key value items color

  key="$(jq_get "$entry" '.key')"
  value="$(jq_get "$entry" '.value')"

  if [[ "$(jq_typeof "$value" '.items')" == "object" ]]; then
    items="$(jq_get "$value" '.items')"
    color="$(jq_get "$value" '.color')"
  else
    items="$value"
    color=""
  fi

  local existing
  existing="$(jq_get "$acc" ".\"$key\"")"

  if jq_defined "$existing"; then
    __labels_api__merge_existing "$acc" "$key" "$existing" "$items"
  else
    __labels_api__add_new "$acc" "$key" "$color" "$items"
  fi
}

function __labels_api__merge_existing() {
  local acc="$1" key="$2" existing="$3" items="$4"
  local existing_items merged_items

  existing_items="$(jq_get "$existing" '.items')"
  merged_items="$(jq_merge "$existing_items" "$items")"

  jq_set "$acc" ".\"$key\".items" "$merged_items"
}

function __labels_api__add_new() {
  local acc="$1" key="$2" color="$3" items="$4"
  local new_cat

  new_cat="$(jq_set '{}' '.color' "$color" '.items' "$items")"
  jq_set "$acc" ".\"$key\"" "$new_cat"
}

# ── Flattening ───────────────────────────────────────────────────────

:<<'DOC'
Converts a merged label tree into a flat array of label objects.

Each object has {name, color, description} where name is "category/item".

Usage: flatten_labels "$merged_labels_json"
DOC
function flatten_labels() {
  local merged="$1"
  local categories

  categories="$(jq_entries "$merged")"
  jq_reduce __labels_api__flatten_category "$categories" '[]'
}

function __labels_api__flatten_category() {
  local acc="$1" entry="$2"
  local category color items item_entries

  category="$(jq_get "$entry" '.key')"
  color="$(jq_get "$entry" '.value.color')"
  items="$(jq_get "$entry" '.value.items')"
  item_entries="$(jq_entries "$items")"

  jq_reduce __labels_api__flatten_item "$item_entries" "$acc" "$category" "$color"
}

function __labels_api__flatten_item() {
  local acc="$1" entry="$2" category="$3" color="$4"
  local item_name description label

  item_name="$(jq_get "$entry" '.key')"
  description="$(jq_get "$entry" '.value')"

  label="$(jq_set '{}' \
    '.name'        "${category}/${item_name}" \
    '.color'       "$color" \
    '.description' "$description")"

  jq_push "$acc" "$label"
}

# ── Diffing ──────────────────────────────────────────────────────────

:<<'DOC'
Compares desired labels against actual Forgejo labels.

Returns a JSON object with three arrays: {create, update, archive}.
  create:  labels in desired but not in actual
  update:  labels in both but with drifted color or description
  archive: labels in actual but not in desired (and not already archived)

Forgejo returns colors without '#'; desired colors include '#'.
Normalization is handled internally.

Usage: diff_labels "$desired_flat" "$actual_forgejo"
DOC
function diff_labels() {
  local desired="$1" actual="$2"
  local result

  result="$(jq_set '{}' '.create' '[]' '.update' '[]' '.archive' '[]')"

  local desired_entries
  desired_entries="$(jq_project "$desired" '{name, color, description}')"
  result="$(jq_reduce __labels_api__diff_desired "$desired_entries" "$result" "$actual")"
  result="$(jq_reduce __labels_api__diff_stale "$(jq_dump "$actual" '[.[]]')" "$result" "$desired")"

  echo "$result"
}

function __labels_api__normalize_color() {
  local color="$1"
  echo "${color#\#}" | tr '[:upper:]' '[:lower:]'
}

function __labels_api__diff_desired() {
  local acc="$1" desired_label="$2" actual="$3"
  local name desired_color desired_desc

  name="$(jq_get "$desired_label" '.name')"
  desired_color="$(jq_get "$desired_label" '.color')"
  desired_desc="$(jq_get "$desired_label" '.description')"

  local actual_label
  actual_label="$(jq_find __labels_api__match_name "$actual" "$name")"

  if ! jq_defined "$actual_label"; then
    jq_push "$acc" '.create' "$desired_label"
    return
  fi

  local actual_color actual_desc
  actual_color="$(jq_get "$actual_label" '.color')"
  actual_desc="$(jq_get "$actual_label" '.description')"

  local norm_desired norm_actual
  norm_desired="$(__labels_api__normalize_color "$desired_color")"
  norm_actual="$(__labels_api__normalize_color "$actual_color")"

  if [[ "$norm_desired" != "$norm_actual" || "$desired_desc" != "$actual_desc" ]]; then
    local actual_id update_entry
    actual_id="$(jq_get "$actual_label" '.id')"
    update_entry="$(jq_set "$desired_label" '.id' "$actual_id")"
    jq_push "$acc" '.update' "$update_entry"
    return
  fi

  echo "$acc"
}

function __labels_api__diff_stale() {
  local acc="$1" actual_label="$2" desired="$3"
  local name is_archived

  name="$(jq_get "$actual_label" '.name')"
  is_archived="$(jq_get "$actual_label" '.is_archived' 'false')"

  if [[ "$is_archived" == "true" ]]; then
    echo "$acc"
    return
  fi

  local match
  match="$(jq_find __labels_api__match_name "$desired" "$name")"

  if ! jq_defined "$match"; then
    local archive_entry
    archive_entry="$(jq_dump "$actual_label" '{id, name}')"
    jq_push "$acc" '.archive' "$archive_entry"
    return
  fi

  echo "$acc"
}

function __labels_api__match_name() {
  local element="$1" target_name="$2"
  local name

  name="$(jq_get "$element" '.name')"
  [[ "$name" == "$target_name" ]]
}
