#!/usr/bin/env bash
# deckctl doctor — validate modules, workstation deny-list, dangling symlinks.
set -euo pipefail

cmd_doctor() {
  local root="$DECKCTL_ROOT"
  local schema="$root/spec/deck-module.schema.json"
  local findings=0

  report() {
    echo "doctor: $*"
    findings=$((findings + 1))
  }

  [[ -d "$root/modules" ]] || die "missing modules/ directory at $root"
  [[ -f "$schema" ]] || die "missing schema at $schema"

  validate_deck_module() {
    jq -e '
      def kebab: test("^[a-z][a-z0-9]*(-[a-z0-9]+)*$");
      def allowed: ["schema_version","name","kind","services","bootstrap","health","install","state_dirs",
                    "system_units","system_user_root","system_bin","system_sbin"];
      def root_paths: type == "array" and all(type == "string" and length > 0);
      def keys_ok: (keys - allowed | length) == 0;
      def valid_install:
        if has("install") then
          (.install | type == "object")
          and ((.install | keys - ["mode","needs_git"] | length) == 0)
          and (.install.mode | type == "string" and IN("in-place","artifact"))
          and (.install.needs_git | type == "boolean")
          and (.install | has("mode") and has("needs_git"))
        else true end;

      . as $doc
      | ($doc | type == "object")
      and ($doc | keys_ok)
      and ($doc | has("schema_version") and has("name") and has("kind"))
      and ($doc.schema_version | type == "number" and . == 1)
      and ($doc.name | type == "string" and kebab)
      and ($doc.kind | type == "string" and IN("service","cli","config","library"))
      and (if $doc | has("services") then
             ($doc.services | type == "array" and all(type == "string"))
           else true end)
      and (if $doc | has("bootstrap") then ($doc.bootstrap | type == "string") else true end)
      and (if $doc | has("health") then ($doc.health | type == "string") else true end)
      and ($doc | valid_install)
      and (if $doc | has("state_dirs") then
             ($doc.state_dirs | type == "array" and all(type == "string"))
           else true end)
      and (all(["system_units","system_user_root","system_bin","system_sbin"][];
               . as $k | if $doc | has($k) then ($doc[$k] | root_paths) else true end))
    ' "$1" >/dev/null 2>&1
  }

  check_relpath() { # $1=module_dir $2=rel $3=label $4=module $5=require_exec
    local moddir="$1" rel="$2" label="$3" mod="$4" require_exec="${5:-1}"
    local path="$moddir/$rel"
    if [[ ! -e "$path" ]]; then
      report "$mod: missing $label $rel"
      return
    fi
    if [[ "$require_exec" == 1 && ! -x "$path" ]]; then
      report "$mod: $label not executable: $rel"
    fi
  }

  deny_matches() { # $1=path $2=pattern
    local path="$1" pat="$2" base
    base=$(basename "$path")
    case "$pat" in
      \#*|"") return 1 ;;
    esac
    [[ "$path" == $pat || "$base" == $pat ]] && return 0
    case "$base" in
      $pat) return 0 ;;
    esac
    return 1
  }

  check_workstation_deny() {
    local ws="$root/modules/workstation"
    local deny="$ws/deny.list"
    local manifest_dir="$ws/manifest"
    [[ -f "$deny" && -d "$manifest_dir" ]] || return 0

    local patterns=()
    while IFS= read -r line || [[ -n "$line" ]]; do
      line="${line%%#*}"
      line="${line#"${line%%[![:space:]]*}"}"
      line="${line%"${line##*[![:space:]]}"}"
      [[ -n "$line" ]] && patterns+=("$line")
    done <"$deny"

    local mf entry path pat
    for mf in "$manifest_dir"/*.json; do
      [[ -f "$mf" ]] || continue
      while IFS= read -r entry; do
        [[ -n "$entry" ]] || continue
        for pat in "${patterns[@]}"; do
          if deny_matches "$entry" "$pat"; then
            report "workstation: deny-listed path in $(basename "$mf"): $entry (pattern $pat)"
          fi
        done
      done < <(jq -r '.entries[]?.path // empty' "$mf" 2>/dev/null || true)
    done
  }

  check_dangling_symlinks() {
    local base
    for base in "${HOME:?}/.local/opt/overdeck" "${HOME:?}/.local/state/overdeck"; do
      [[ -d "$base" ]] || continue
      while IFS= read -r -d '' link; do
        if [[ ! -e "$link" ]]; then
          report "dangling symlink: $link"
        fi
      done < <(find "$base" -type l -print0 2>/dev/null || true)
    done
  }

  local manifest moddir modname
  while IFS= read -r -d '' manifest; do
    moddir=$(dirname "$manifest")
    modname=$(basename "$moddir")

    if ! validate_deck_module "$manifest"; then
      report "$modname: invalid deck.module.json"
      continue
    fi

    local declared
    declared=$(jq -r '.name' "$manifest")
    if [[ "$declared" != "$modname" ]]; then
      report "$modname: manifest name '$declared' does not match directory"
    fi

    if jq -e 'has("bootstrap")' "$manifest" >/dev/null; then
      check_relpath "$moddir" "$(jq -r '.bootstrap' "$manifest")" "bootstrap" "$modname" 1
    fi
    if jq -e 'has("health")' "$manifest" >/dev/null; then
      check_relpath "$moddir" "$(jq -r '.health' "$manifest")" "health" "$modname" 1
    fi
    if jq -e 'has("services")' "$manifest" >/dev/null; then
      local svc
      while IFS= read -r svc; do
        [[ -n "$svc" ]] || continue
        check_relpath "$moddir" "$svc" "service" "$modname" 0
      done < <(jq -r '.services[]?' "$manifest")
    fi
  done < <(find "$root/modules" -mindepth 2 -maxdepth 2 -name 'deck.module.json' -print0 | sort -z)

  check_workstation_deny
  check_dangling_symlinks

  [[ "$findings" -eq 0 ]] || exit 1
}
