#!/usr/bin/env bash
# Compound Engineering environment health check.
# Reports optional tool capabilities and repo-local config safety in one pass.

set -o pipefail

# =====================================================
#  Optional capability config
# =====================================================
# Format: name|install_cmd|url|capability

deps=(
  "agent-browser|CI=true npm install -g agent-browser --no-audit --no-fund --loglevel=error && agent-browser install|https://github.com/vercel-labs/agent-browser|browser testing and dogfood QA"
  "gh|NONINTERACTIVE=1 HOMEBREW_NO_AUTO_UPDATE=1 brew install -q gh|https://cli.github.com|GitHub PR, issue, and review workflows"
  "jq|NONINTERACTIVE=1 HOMEBREW_NO_AUTO_UPDATE=1 brew install -q jq|https://jqlang.github.io/jq/|JSON inspection in shell-based workflows"
  "ast-grep|NONINTERACTIVE=1 HOMEBREW_NO_AUTO_UPDATE=1 brew install -q ast-grep|https://ast-grep.github.io|syntax-aware structural code search"
  "ffmpeg|NONINTERACTIVE=1 HOMEBREW_NO_AUTO_UPDATE=1 brew install -q ffmpeg|https://ffmpeg.org/download.html|media chunking and screenshot extraction for Riffrec analysis"
)

# =====================================================
#  Args
# =====================================================

plugin_version=""
while [ $# -gt 0 ]; do
  case "$1" in
    --version) [ -n "$2" ] && plugin_version="$2" && shift 2 || shift ;;
    *) shift ;;
  esac
done

# =====================================================
#  Helpers
# =====================================================

ok()      { echo "  🟢  $1"; }
warn()    { echo "  🟡  $1"; }
skip()    { echo "  ➖  $1"; }
detail()  { echo "       $1"; }
section() { echo ""; echo " $1"; }

# Read one top-level scalar from the repo's flat local-config convention. This is
# intentionally not a general YAML parser: comments and absent/empty keys are
# ignored, surrounding quotes are removed, and values are never evaluated.
read_flat_config_value() {
  local config_file="$1"
  local config_key="$2"
  local value
  value=$(awk -v key="$config_key" '
    $0 ~ "^" key "[[:space:]]*:" {
      sub("^" key "[[:space:]]*:[[:space:]]*", "")
      sub(/[[:space:]]+#.*$/, "")
      sub(/^[[:space:]]+/, "")
      sub(/[[:space:]]+$/, "")
      print
      exit
    }
  ' "$config_file")

  case "$value" in
    \"*\") value=${value#\"}; value=${value%\"} ;;
    \'*\') value=${value#\'}; value=${value%\'} ;;
  esac
  printf '%s' "$value"
}

# Resolve the symlink-followed absolute path of $1 without relying on `realpath -m`
# (absent on stock macOS/BSD). Resolves the existing prefix with `cd`/`pwd -P`,
# then re-appends any not-yet-created tail — enough to catch a symlink in the
# existing portion escaping the repository, which is the only way a relative
# value can escape.
resolve_real_best_effort() {
  local target="$1" tail="" existing base
  existing="$target"
  while [ ! -e "$existing" ] && [ "$existing" != "/" ] && [ "$existing" != "." ] && [ "$existing" != "$(dirname "$existing")" ]; do
    tail="/$(basename "$existing")$tail"
    existing="$(dirname "$existing")"
  done
  if [ -d "$existing" ]; then
    base="$(cd "$existing" 2>/dev/null && pwd -P)" || return 1
  elif [ -e "$existing" ]; then
    base="$(cd "$(dirname "$existing")" 2>/dev/null && pwd -P)/$(basename "$existing")" || return 1
  else
    return 1
  fi
  printf '%s' "${base}${tail}"
}

# Resolve docs_root from tracked config.yaml only. A docs_root in
# config.local.yaml is ignored. Unset -> default root `docs`. A configured
# value is validated and rejected (never silently defaulted) when it is
# absolute, escapes the repository via path or symlink, resolves to the repo
# root or inside .git/, or names an existing non-directory. Sets
# docs_root_status (default|configured|invalid), docs_root_value,
# docs_root_source, docs_root_error, and docs_root_local_ignored.
resolve_docs_root() {
  local repo_root="$1"
  local local_cfg="$repo_root/.compound-engineering/config.local.yaml"
  local tracked_cfg="$repo_root/.compound-engineering/config.yaml"
  local value="" source="" repo_real real local_docs probe

  docs_root_error=""
  docs_root_local_ignored=""
  if [ -f "$local_cfg" ]; then
    local_docs=$(read_flat_config_value "$local_cfg" "docs_root")
    [ -n "$local_docs" ] && docs_root_local_ignored="$local_docs"
  fi
  if [ -f "$tracked_cfg" ]; then
    value=$(read_flat_config_value "$tracked_cfg" "docs_root")
    [ -n "$value" ] && source="config.yaml"
  fi

  if [ -z "$value" ]; then
    docs_root_status="default"; docs_root_value="docs"; docs_root_source="default"; return
  fi

  docs_root_value="$value"; docs_root_source="$source"; docs_root_status="invalid"
  case "$value" in
    /*) docs_root_error="absolute paths are not allowed; use a repo-relative path"; return ;;
  esac
  # Reject any `..` path component up front. resolve_real_best_effort only
  # collapses `..` within the existing prefix (via cd/pwd -P); a `..` that
  # traverses a not-yet-created segment would otherwise survive literally and
  # escape the repo, hit the repo root, or reach .git/ while still string-prefix
  # matching. A legitimate repo-relative artifact root never needs `..`.
  case "/$value/" in
    */../*) docs_root_error="path traversal ('..') is not allowed; use a plain repo-relative path"; return ;;
  esac
  repo_real="$(cd "$repo_root" 2>/dev/null && pwd -P)" || { docs_root_error="could not resolve the repository root"; return; }
  real="$(resolve_real_best_effort "$repo_root/$value")" || { docs_root_error="could not resolve the path"; return; }
  case "$real/" in
    "$repo_real/"*) ;;
    *) docs_root_error="resolves outside the repository ($real)"; return ;;
  esac
  [ "$real" = "$repo_real" ] && { docs_root_error="resolves to the repository root itself"; return; }
  case "$real/" in
    "$repo_real/.git/"*) docs_root_error="resolves inside .git/"; return ;;
  esac
  if [ -e "$real" ] && [ ! -d "$real" ]; then
    docs_root_error="names an existing non-directory"; return
  fi
  # Reject when an intermediate component is an existing non-directory (e.g.
  # `afile/nested` where `afile` is a file): the leaf-only check above passes
  # because `$real` itself does not exist, but `mkdir -p` would fail. Walk up to
  # the deepest existing ancestor and require it be a directory.
  probe="$real"
  while [ ! -e "$probe" ] && [ "$probe" != "/" ] && [ "$probe" != "$(dirname "$probe")" ]; do
    probe="$(dirname "$probe")"
  done
  if [ -e "$probe" ] && [ ! -d "$probe" ]; then
    docs_root_error="an intermediate path component is not a directory"; return
  fi
  docs_root_status="configured"
}

# Report whether a retired top-level key is still present without interpreting
# its value. Detection is intentionally separate from routing so legacy scalar
# settings can receive a migration diagnostic without remaining supported.
has_flat_config_key() {
  local config_file="$1"
  local config_key="$2"
  awk -v key="$config_key" '
    $0 ~ "^" key "[[:space:]]*:" { found = 1 }
    END { exit(found ? 0 : 1) }
  ' "$config_file"
}

# First non-empty ordinary scalar across local then tracked. Empty/commented
# local continues to tracked. Sets ordinary_value and ordinary_source.
resolve_ordinary_scalar() {
  local local_file="$1"
  local tracked_file="$2"
  local config_key="$3"
  local value=""
  ordinary_value=""
  ordinary_source=""
  if [ -f "$local_file" ]; then
    value=$(read_flat_config_value "$local_file" "$config_key")
    if [ -n "$value" ]; then
      ordinary_value="$value"
      ordinary_source="config.local.yaml"
      return
    fi
  fi
  if [ -f "$tracked_file" ]; then
    value=$(read_flat_config_value "$tracked_file" "$config_key")
    if [ -n "$value" ]; then
      ordinary_value="$value"
      ordinary_source="config.yaml"
    fi
  fi
}

# Present list/map key (including `[]`) wins that layer. Sets
# structured_file, or empty when neither layer sets the key.
resolve_structured_layer() {
  local local_file="$1"
  local tracked_file="$2"
  local config_key="$3"
  structured_file=""
  if [ -f "$local_file" ] && has_flat_config_key "$local_file" "$config_key"; then
    structured_file="$local_file"
    return
  fi
  if [ -f "$tracked_file" ] && has_flat_config_key "$tracked_file" "$config_key"; then
    structured_file="$tracked_file"
  fi
}

# Read the narrow YAML sequence used by work_engine_preferences. The local
# config remains human-authored YAML, but setup health needs only this one
# deterministic shape rather than a general parser.
read_work_engine_preferences() {
  local config_file="$1"
  awk '
    function trim(value) {
      sub(/^[[:space:]]+/, "", value)
      sub(/[[:space:]]+$/, "", value)
      return value
    }
    function scalar(value) {
      sub(/[[:space:]]+#.*$/, "", value)
      value = trim(value)
      if (value ~ /^".*"$/ || value ~ /^\047.*\047$/) {
        value = substr(value, 2, length(value) - 2)
      }
      return value
    }
    function emit_item() {
      if (!have_item) return
      if (harness == "" && model != "") {
        print "ERROR\tmodel \047" model "\047 has no harness in work_engine_preferences"
      } else if (harness == "") {
        print "ERROR\tempty item in work_engine_preferences"
      } else {
        print "ITEM\t" harness "\t" (model == "" ? "default" : model)
      }
      have_item = 0
      harness = ""
      model = ""
    }
    /^[^[:space:]#-][^:]*[[:space:]]*:/ {
      if (active) {
        emit_item()
        active = 0
      }
    }
    /^work_engine_preferences[[:space:]]*:/ {
      active = 1
      next
    }
    active {
      line = $0
      if (line ~ /^[[:space:]]*($|#)/) next
      if (line ~ /^[[:space:]]*-[[:space:]]*harness[[:space:]]*:/) {
        emit_item()
        sub(/^[[:space:]]*-[[:space:]]*harness[[:space:]]*:[[:space:]]*/, "", line)
        harness = scalar(line)
        have_item = 1
        next
      }
      if (line ~ /^[[:space:]]*-[[:space:]]*model[[:space:]]*:/) {
        emit_item()
        sub(/^[[:space:]]*-[[:space:]]*model[[:space:]]*:[[:space:]]*/, "", line)
        model = scalar(line)
        have_item = 1
        next
      }
      if (line ~ /^[[:space:]]+model[[:space:]]*:/) {
        sub(/^[[:space:]]+model[[:space:]]*:[[:space:]]*/, "", line)
        model = scalar(line)
        if (!have_item) have_item = 1
        next
      }
      if (line ~ /^[[:space:]]+harness[[:space:]]*:/) {
        sub(/^[[:space:]]+harness[[:space:]]*:[[:space:]]*/, "", line)
        harness = scalar(line)
        if (!have_item) have_item = 1
        next
      }
      print "ERROR\tunsupported work_engine_preferences entry: " trim(line)
    }
    END {
      if (active) emit_item()
    }
  ' "$config_file"
}

has_brew=$(command -v brew >/dev/null 2>&1 && echo "yes" || echo "no")
in_repo=$(git rev-parse --is-inside-work-tree >/dev/null 2>&1 && echo "yes" || echo "no")

# =====================================================
#  Check optional capabilities
# =====================================================

capability_ok=0
capability_total=0
capability_missing=0
results=()

for entry in "${deps[@]}"; do
  IFS='|' read -r name install_cmd url capability < <(printf '%s\n' "$entry")
  capability_total=$((capability_total + 1))
  if command -v "$name" >/dev/null 2>&1; then
    capability_ok=$((capability_ok + 1))
    results+=("$name|ok|$install_cmd|$url|$capability")
  else
    capability_missing=$((capability_missing + 1))
    results+=("$name|missing|$install_cmd|$url|$capability")
  fi
done

# =====================================================
#  Project checks (repo only)
# =====================================================

legacy_cfg="skip"
repo_cfg_gitignore="skip"
scratch_gitignore="skip"
local_cfg="skip"
example_cfg="skip"
retired_keys=()
work_engine_cfg="skip"
work_engine_detail=""
work_engine_preferences_note=""
work_engine_preferences_note_level="detail"
work_engine_migration_note=""
docs_root_status="skip"
docs_root_value=""
docs_root_source=""
docs_root_error=""
docs_root_local_ignored=""
tracked_cfg="skip"
project_issues=0

if [ "$in_repo" = "yes" ]; then
  repo_root=$(git rev-parse --show-toplevel 2>/dev/null)
  local_config_path="$repo_root/.compound-engineering/config.local.yaml"
  tracked_config_path="$repo_root/.compound-engineering/config.yaml"
  resolve_docs_root "$repo_root"
  [ "$docs_root_status" = "invalid" ] && project_issues=$((project_issues + 1))
  legacy_cfg="missing"
  [ -f "$repo_root/compound-engineering.local.md" ] && legacy_cfg="present"

  local_cfg="missing"
  if [ -e "$local_config_path" ]; then
    local_cfg="present"
    if git check-ignore -q "$local_config_path" 2>/dev/null; then
      repo_cfg_gitignore="ok"
    else
      repo_cfg_gitignore="missing"
    fi
  fi

  # Trailing slash is load-bearing: without it an existing directory-only rule
  # is missed before the directory exists, and a configured repo reads as bare.
  scratch_gitignore="missing"
  if git -C "$repo_root" check-ignore -q .context/compound-engineering/ 2>/dev/null; then
    scratch_gitignore="ok"
  fi

  tracked_cfg="missing"
  [ -f "$tracked_config_path" ] && tracked_cfg="present"

  resolve_ordinary_scalar "$local_config_path" "$tracked_config_path" "work_engine_mode"
  work_engine_mode="$ordinary_value"
  case "$work_engine_mode" in
    ""|off|prefer|require) ;;
    *)
      if [ "$ordinary_source" = "config.local.yaml" ]; then
        if [ -f "$tracked_config_path" ]; then
          work_engine_mode=$(read_flat_config_value "$tracked_config_path" "work_engine_mode")
        else
          work_engine_mode=""
        fi
      fi
      ;;
  esac

  resolve_structured_layer "$local_config_path" "$tracked_config_path" "work_engine_preferences"
  work_engine_preferences_count=0
  work_engine_preferences_detail=""
  work_engine_preferences_error=""
  retired_work_engine_keys=""
  for retired_key in work_engine_target work_engine_model; do
    if { [ -f "$local_config_path" ] && has_flat_config_key "$local_config_path" "$retired_key"; } || \
       { [ -f "$tracked_config_path" ] && has_flat_config_key "$tracked_config_path" "$retired_key"; }; then
      if [ -n "$retired_work_engine_keys" ]; then
        retired_work_engine_keys="${retired_work_engine_keys}, ${retired_key}"
      else
        retired_work_engine_keys="$retired_key"
      fi
    fi
  done
  if [ -n "$retired_work_engine_keys" ]; then
    work_engine_migration_note="retired config key(s) ${retired_work_engine_keys} detected; migrate routing to work_engine_preferences entries with harness and optional model fields, then remove the retired keys"
    project_issues=$((project_issues + 1))
  fi
  if [ -n "$structured_file" ]; then
    while IFS=$'\t' read -r preference_kind preference_harness preference_model; do
      case "$preference_kind" in
        ITEM)
          work_engine_preferences_count=$((work_engine_preferences_count + 1))
          case "$preference_harness" in
            codex|claude|grok|cursor) ;;
            *)
              [ -z "$work_engine_preferences_error" ] && work_engine_preferences_error="invalid harness '$preference_harness' in work_engine_preferences"
              ;;
          esac
          if [ "$preference_model" != "default" ]; then
            case "$preference_model" in
              [A-Za-z0-9]*)
                case "$preference_model" in
                  *[!A-Za-z0-9._:/-]*)
                    [ -z "$work_engine_preferences_error" ] && work_engine_preferences_error="invalid model '$preference_model' in work_engine_preferences"
                    ;;
                esac
                ;;
              *)
                [ -z "$work_engine_preferences_error" ] && work_engine_preferences_error="invalid model '$preference_model' in work_engine_preferences"
                ;;
            esac
          fi
          preference_display="${preference_harness}@${preference_model}"
          if [ -n "$work_engine_preferences_detail" ]; then
            work_engine_preferences_detail="${work_engine_preferences_detail}, ${preference_display}"
          else
            work_engine_preferences_detail="$preference_display"
          fi
          ;;
        ERROR)
          [ -z "$work_engine_preferences_error" ] && work_engine_preferences_error="$preference_harness"
          ;;
      esac
    done < <(read_work_engine_preferences "$structured_file")
  fi

  case "$work_engine_mode" in
    ""|off)
      work_engine_cfg="native"
      if [ -n "$work_engine_mode" ]; then
        work_engine_detail="standing preference is off"
      else
        work_engine_detail="setting is commented or missing"
      fi
      if [ -n "$work_engine_preferences_error" ]; then
        work_engine_preferences_note="invalid dormant work_engine_preferences: ${work_engine_preferences_error}"
        work_engine_preferences_note_level="warn"
        project_issues=$((project_issues + 1))
      elif [ "$work_engine_preferences_count" -gt 0 ]; then
        work_engine_preferences_note="ordered preferences ignored while standing mode is off"
      fi
      ;;
    prefer|require)
      if [ -n "$work_engine_preferences_error" ]; then
        work_engine_cfg="unavailable"
        work_engine_detail="$work_engine_preferences_error"
        project_issues=$((project_issues + 1))
      elif [ "$work_engine_preferences_count" -eq 0 ] && [ -n "$retired_work_engine_keys" ]; then
        work_engine_cfg="unavailable"
        work_engine_detail="$work_engine_mode cannot use retired scalar routing; migrate ${retired_work_engine_keys} to work_engine_preferences"
      elif [ "$work_engine_preferences_count" -eq 0 ]; then
        work_engine_cfg="unavailable"
        work_engine_detail="$work_engine_mode requires work_engine_preferences"
        project_issues=$((project_issues + 1))
      else
        work_engine_cfg="$work_engine_mode"
        work_engine_detail="$work_engine_preferences_detail"
      fi
      ;;
    *)
      work_engine_cfg="invalid"
      work_engine_detail="invalid mode '$work_engine_mode' ignored; native is the default"
      project_issues=$((project_issues + 1))
      ;;
  esac

  script_dir="$(cd "$(dirname "$0")" && pwd)"
  template="$script_dir/../references/config-template.yaml"
  example="$repo_root/.compound-engineering/config.example.yaml"
  if [ ! -f "$example" ]; then
    example_cfg="missing"
  elif [ -f "$template" ] && ! diff -q "$template" "$example" >/dev/null 2>&1; then
    example_cfg="outdated"
  else
    example_cfg="ok"
  fi

  # Retired keys set as ACTIVE (non-commented) in either layer. A
  # commented `# plan_use_fable:` line does not match `^[[:space:]]*<key>:`.
  for cfg_file in "$local_config_path" "$tracked_config_path"; do
    [ -f "$cfg_file" ] || continue
    for key in plan_use_fable brainstorm_use_fable fable_nudge; do
      if grep -Eq "^[[:space:]]*${key}[[:space:]]*:" "$cfg_file" 2>/dev/null; then
        already=""
        for seen in "${retired_keys[@]}"; do
          [ "$seen" = "$key" ] && already=1
        done
        [ -z "$already" ] && retired_keys+=("$key")
      fi
    done
  done
fi

# =====================================================
#  Output
# =====================================================

echo ""
if [ -n "$plugin_version" ]; then
  ok "Plugin version v${plugin_version}"
fi

section "Optional capabilities  ${capability_ok}/${capability_total}"

for result in "${results[@]}"; do
  IFS='|' read -r name status install_cmd url capability < <(printf '%s\n' "$result")
  if [ "$status" = "ok" ]; then
    ok "$name -- $capability"
  else
    warn "$name -- unavailable: $capability"
    if [[ "$install_cmd" == *"brew install"* ]] && [ "$has_brew" != "yes" ]; then
      detail "$url"
    else
      detail "$install_cmd"
      detail "$url"
    fi
  fi
done

if [ "$in_repo" = "yes" ]; then
  section "Project config"

  if [ "$legacy_cfg" = "present" ]; then
    warn "Obsolete compound-engineering.local.md exists"
    project_issues=$((project_issues + 1))
  else
    ok "No obsolete compound-engineering.local.md"
  fi

  if [ "$tracked_cfg" = "present" ]; then
    ok ".compound-engineering/config.yaml exists"
  else
    skip "No repo config yet (.compound-engineering/config.yaml)"
  fi

  if [ "$local_cfg" = "present" ]; then
    ok ".compound-engineering/config.local.yaml exists"
    if [ "$repo_cfg_gitignore" = "ok" ]; then
      ok "Local config is gitignored"
    else
      warn "Local config is not safely gitignored"
      project_issues=$((project_issues + 1))
    fi
  else
    skip "No local override yet (.compound-engineering/config.local.yaml)"
  fi

  if [ "$scratch_gitignore" = "ok" ]; then
    ok "CE scratch space is gitignored"
  else
    skip "CE scratch space is not gitignored"
    detail "Skills that keep local scratch write it under .context/compound-engineering/"
  fi

  if [ "$docs_root_status" = "default" ]; then
    ok "Artifact root: docs/ (default — docs_root not set)"
  elif [ "$docs_root_status" = "configured" ]; then
    ok "Artifact root: ${docs_root_value}/ (from ${docs_root_source})"
  elif [ "$docs_root_status" = "invalid" ]; then
    warn "Invalid docs_root '${docs_root_value}' in ${docs_root_source}: ${docs_root_error}"
    detail "CE artifacts will not be written until docs_root is corrected or removed."
  fi
  if [ -n "$docs_root_local_ignored" ]; then
    skip "Local docs_root '${docs_root_local_ignored}' is ignored; set docs_root only in config.yaml"
  fi

  if [ "$work_engine_cfg" = "native" ]; then
    ok "CE Work implementation engine: native (${work_engine_detail})"
    if [ -n "$work_engine_preferences_note" ]; then
      if [ "$work_engine_preferences_note_level" = "warn" ]; then
        warn "$work_engine_preferences_note"
      else
        detail "$work_engine_preferences_note"
      fi
    fi
  elif [ "$work_engine_cfg" = "prefer" ] || [ "$work_engine_cfg" = "require" ]; then
    ok "CE Work implementation engine: ${work_engine_cfg} -> ${work_engine_detail}"
  elif [ "$work_engine_cfg" = "unavailable" ]; then
    warn "CE Work implementation engine unavailable: ${work_engine_detail}"
  elif [ "$work_engine_cfg" = "invalid" ]; then
    warn "CE Work implementation engine: ${work_engine_detail}"
  fi
  [ -n "$work_engine_migration_note" ] && warn "$work_engine_migration_note"

  if [ "$example_cfg" = "ok" ]; then
    ok "Example config is current"
  elif [ "$example_cfg" = "missing" ]; then
    warn "Example config missing (.compound-engineering/config.example.yaml)"
    project_issues=$((project_issues + 1))
  elif [ "$example_cfg" = "outdated" ]; then
    warn "Example config outdated (new settings available)"
    project_issues=$((project_issues + 1))
  fi

  for key in "${retired_keys[@]}"; do
    case "$key" in
      plan_use_fable)       warn "Retired config key '$key' — removed; use 'plan_model' instead" ;;
      brainstorm_use_fable) warn "Retired config key '$key' — removed; use 'brainstorm_model' instead" ;;
      fable_nudge)          warn "Retired config key '$key' — removed; model elevation is always available" ;;
    esac
    project_issues=$((project_issues + 1))
  done
else
  section "Project config"
  skip "Not inside a git repository"
fi

echo ""
if [ "$project_issues" -eq 0 ]; then
  echo " ✅  Project config healthy. Optional capabilities available: ${capability_ok}/${capability_total}"
else
  echo " ⚠️   ${project_issues} project issue(s) found. Optional capabilities available: ${capability_ok}/${capability_total}"
fi

if [ "$capability_missing" -gt 0 ]; then
  echo "     Missing optional tools do not block setup; install them only for the workflows you use."
fi

echo ""
