#!/usr/bin/env bash

:<<'DOC'
API for filesystem-driven area label generation.

Scans a repo's source directory tree and produces area definitions that feed
into the label taxonomy. Optionally merges with manual overrides from
manifest.json's "areas.overrides" block.

Designed to replace hand-maintained labels.area items with auto-discovered
directory-based areas, while still supporting virtual areas (overrides that
don't map to a physical directory).
DOC

__areas_api__EMPTY_AREAS='{}'

:<<'DOC'
Loads the areas config from manifest.json. Returns the JSON object or "null".

Usage: load_areas_config
DOC
function load_areas_config() {
  local manifest config

  manifest="$(<"$PROJ/manifest.json")"
  config="$(jq_get "$manifest" '.areas' 'null')"
  jq_compact "$config"
}

:<<'DOC'
Returns true (0) if the manifest has an areas config block.

Usage: has_areas_config
DOC
function has_areas_config() {
  local config
  config="$(load_areas_config)"
  jq_defined "$config"
}

# ── Scanning ──────────────────────────────────────────────────────────

:<<'DOC'
Scans directories under a source path up to the given depth.

Returns a JSON object keyed by area name (directory basename) with default
metadata: {"paths": ["<relative_path>"], "tests": []}.

Usage: scan_areas "$source" "$depth"
DOC
function scan_areas() {
  local source="$1" depth="$2"
  local base_dir

  if ! __areas_api__is_scan_enabled "$source" "$depth"; then
    echo "$__areas_api__EMPTY_AREAS"
    return
  fi

  base_dir="$(__areas_api__resolve_source "$source")"
  __areas_api__scan_at_depth "$base_dir" "$depth"
}

function __areas_api__is_scan_enabled() {
  local source="$1" depth="$2"

  if [[ -z "$source" || "$source" == "null" ]]; then
    return 1
  fi

  if [[ ! "$depth" =~ ^[0-9]+$ ]]; then
    return 1
  fi

  if ((depth < 1)); then
    return 1
  fi

  [[ -d "$(__areas_api__resolve_source "$source")" ]]
}

function __areas_api__resolve_source() {
  local source="$1"

  if [[ "$source" = /* ]]; then
    echo "$source"
  else
    echo "$PROJ/$source"
  fi
}

function __areas_api__scan_at_depth() {
  local base_dir="$1" depth="$2"
  local dir_paths

  dir_paths="$(__areas_api__dir_paths_at_depth "$base_dir" "$depth")"
  jq_reduce __areas_api__add_scanned_area "$dir_paths" '{}' "$dir_paths"
}

function __areas_api__dir_paths_at_depth() {
  local base_dir="$1" depth="$2"
  local paths

  paths="$(__areas_api__walk_dirs "$base_dir" "$depth" 0 | sort)"
  jq_split "$paths"
}

function __areas_api__walk_dirs() {
  local dir_path="$1" target_depth="$2" current_depth="$3"

  if ((current_depth == target_depth)); then
    if ! __areas_api__has_scannable_file "$dir_path"; then
      return
    fi

    echo "$dir_path"
    return
  fi

  local next_depth
  next_depth=$((current_depth + 1))

  jq_foreach __areas_api__walk_child_dir \
    "$(__areas_api__child_dirs "$dir_path")" \
    "$target_depth" \
    "$next_depth"
}

function __areas_api__walk_child_dir() {
  local child_dir_json="$1" target_depth="$2" next_depth="$3"
  local child_dir

  child_dir="$(jq_unwrap "$child_dir_json")"
  if __areas_api__should_skip_dir "$child_dir"; then
    return 0
  fi

  __areas_api__walk_dirs "$child_dir" "$target_depth" "$next_depth"
}

function __areas_api__has_scannable_file() {
  local dir_path="$1"
  local scannable_file scannable_dir

  scannable_file="$(jq_find __areas_api__file_is_scannable "$(__areas_api__child_files "$dir_path")")"
  if jq_defined "$scannable_file"; then
    return 0
  fi

  scannable_dir="$(jq_find __areas_api__dir_contains_scannable_file "$(__areas_api__child_dirs "$dir_path")")"
  jq_defined "$scannable_dir"
}

function __areas_api__file_is_scannable() {
  local child_file_json="$1"
  local child_file

  child_file="$(jq_unwrap "$child_file_json")"
  ! __areas_api__should_skip_file "$child_file"
}

function __areas_api__dir_contains_scannable_file() {
  local child_dir_json="$1"
  local child_dir

  child_dir="$(jq_unwrap "$child_dir_json")"
  if __areas_api__should_skip_dir "$child_dir"; then
    return 1
  fi

  __areas_api__has_scannable_file "$child_dir"
}

function __areas_api__child_dirs() {
  __areas_api__child_paths "$1" d
}

function __areas_api__child_files() {
  __areas_api__child_paths "$1" f
}

function __areas_api__child_paths() {
  local dir_path="$1" type="$2"
  local paths

  paths="$(find "$dir_path" -mindepth 1 -maxdepth 1 -type "$type" -print | sort)"
  jq_split "$paths"
}

function __areas_api__should_skip_dir() {
  local dir_path="$1"
  local name

  name="$(basename "$dir_path")"
  if [[ "$name" == .* ]]; then
    return 0
  fi

  __areas_api__is_git_ignored_dir "$dir_path"
}

function __areas_api__should_skip_file() {
  local file_path="$1"
  local name

  name="$(basename "$file_path")"
  if [[ "$name" == .* ]]; then
    return 0
  fi

  __areas_api__is_git_ignored_path "$file_path"
}

function __areas_api__is_git_ignored_dir() {
  local dir_path="$1"
  local rel_path

  [[ "$dir_path" == "$PROJ"/* ]] || return 1

  rel_path="$(__areas_api__relative_path "$dir_path" "$PROJ")"
  __areas_api__is_git_ignored_path "$dir_path" ||
    git -C "$PROJ" check-ignore -q -- "$rel_path/" 2>/dev/null
}

function __areas_api__is_git_ignored_path() {
  local path="$1"
  local rel_path

  [[ "$path" == "$PROJ"/* ]] || return 1

  rel_path="$(__areas_api__relative_path "$path" "$PROJ")"
  git -C "$PROJ" check-ignore -q -- "$rel_path" 2>/dev/null
}

function __areas_api__add_scanned_area() {
  local acc="$1" dir_path_json="$2" dir_paths="$3"
  local dir_path name rel_path area_entry

  dir_path="$(jq_unwrap "$dir_path_json")"
  name="$(__areas_api__area_name "$dir_path" "$dir_paths")"
  rel_path="$(__areas_api__relative_path "$dir_path" "$PROJ")"

  area_entry="$(jq_set '{}' \
    '.paths' "[\"$rel_path\"]" \
    '.tests' '[]')"

  jq_set "$acc" ".\"$name\"" "$area_entry"
}

function __areas_api__area_name() {
  local dir_path="$1" dir_paths="$2"
  local rel_path part_count suffix_len candidate

  rel_path="$(__areas_api__relative_path "$dir_path" "$PROJ")"
  part_count="$(__areas_api__path_part_count "$rel_path")"

  for ((suffix_len = 1; suffix_len <= part_count; suffix_len++)); do
    candidate="$(__areas_api__path_suffix_name "$rel_path" "$suffix_len")"
    if __areas_api__is_unique_area_name "$candidate" "$suffix_len" "$dir_paths"; then
      echo "$candidate"
      return
    fi
  done

  echo "${rel_path//\//-}"
}

function __areas_api__path_part_count() {
  local path="$1"
  local parts

  IFS='/' read -r -a parts <<< "$path"
  echo "${#parts[@]}"
}

function __areas_api__path_suffix_name() {
  local path="$1" suffix_len="$2"
  local parts count start candidate i

  IFS='/' read -r -a parts <<< "$path"
  count="${#parts[@]}"
  start=$((count - suffix_len))
  candidate="${parts[$start]}"

  for ((i = start + 1; i < count; i++)); do
    candidate+="-${parts[$i]}"
  done

  echo "$candidate"
}

function __areas_api__is_unique_area_name() {
  local candidate="$1" suffix_len="$2" dir_paths="$3"
  local match_count

  match_count="$(jq_reduce __areas_api__count_area_name_match "$dir_paths" 0 "$candidate" "$suffix_len")"

  ((match_count == 1))
}

function __areas_api__count_area_name_match() {
  local acc="$1" dir_path_json="$2" candidate="$3" suffix_len="$4"
  local dir_path rel_path other_candidate

  dir_path="$(jq_unwrap "$dir_path_json")"
  rel_path="$(__areas_api__relative_path "$dir_path" "$PROJ")"
  other_candidate="$(__areas_api__path_suffix_name "$rel_path" "$suffix_len")"

  if [[ "$candidate" == "$other_candidate" ]]; then
    echo $((acc + 1))
    return
  fi

  echo "$acc"
}

function __areas_api__relative_path() {
  local full="$1" base="$2"
  local rel="${full#"$base"/}"
  rel="${rel#./}"
  echo "$rel"
}

# ── Override merging ──────────────────────────────────────────────────

:<<'DOC'
Combines scanned areas with manual overrides from the manifest.

Rules:
  - Override matches scanned area -> replace paths and tests
  - Override has no matching scanned area -> add as virtual area
  - Scanned area with no override -> keep defaults

Usage: merge_area_overrides "$scanned" "$overrides"
DOC
function merge_area_overrides() {
  local scanned="$1" overrides="$2"

  if ! jq_defined "$overrides" || [[ "$overrides" == "{}" ]]; then
    echo "$scanned"
    return
  fi

  local entries
  entries="$(jq_entries "$overrides")"
  jq_reduce __areas_api__apply_override "$entries" "$scanned"
}

function __areas_api__apply_override() {
  local acc="$1" entry="$2"
  local name override_value

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

  local paths tests
  paths="$(jq_get "$override_value" '.paths' '[]')"
  tests="$(jq_get "$override_value" '.tests' '[]')"

  local area_entry
  area_entry="$(jq_set '{}' '.paths' "$paths" '.tests' "$tests")"
  jq_set "$acc" ".\"$name\"" "$area_entry"
}

# ── Orchestration ─────────────────────────────────────────────────────

:<<'DOC'
Computes the canonical area set from a manifest areas config.

Reads source/depth/overrides, runs the scan, merges overrides, and returns
the final area definitions.

Usage: compute_areas "$areas_config"
DOC
function compute_areas() {
  local config="$1"
  local source depth overrides scanned

  source="$(jq_get "$config" '.source')"
  depth="$(jq_get "$config" '.depth')"
  overrides="$(jq_get "$config" '.overrides' 'null')"

  if ! __areas_api__is_scan_enabled "$source" "$depth"; then
    echo "$__areas_api__EMPTY_AREAS"
    return
  fi

  scanned="$(scan_areas "$source" "$depth")" || return 1
  merge_area_overrides "$scanned" "$overrides"
}

:<<'DOC'
Converts a canonical area set into the flat {"name": "description"} format
that merged_labels() expects for injection into the area category.

Description is auto-generated from the area's paths.

Usage: areas_to_label_items "$canonical_areas"
DOC
function areas_to_label_items() {
  local areas="$1"
  local entries

  entries="$(jq_entries "$areas")"
  jq_reduce __areas_api__area_to_label_item "$entries" '{}'
}

function __areas_api__area_to_label_item() {
  local acc="$1" entry="$2"
  local name paths description

  name="$(jq_get "$entry" '.key')"
  paths="$(jq_get "$entry" '.value.paths')"
  description="$(jq_join "$paths" ", ")"

  jq_set "$acc" ".\"$name\"" "Source: $description"
}
