#!/usr/bin/env bash
set -euo pipefail

usage() {
  cat >&2 <<'EOF'
Usage:
  harness-adapter list --root <dir> --machine <id>
  harness-adapter show --root <dir> --machine <id> --adapter <id>
  harness-adapter enable --root <dir> --machine <id> --adapter <id>
  harness-adapter disable --root <dir> --machine <id> --adapter <id>
  harness-adapter config --root <dir> --machine <id> --adapter <id> --json <object-json>
  harness-adapter set-models --root <dir> --machine <id> --adapter <id> [model...]
  harness-adapter set-secret --root <dir> --machine <id> --adapter <id> --secret <name> < <value>
  harness-adapter available-models --root <dir> --machine <id> --adapter <id>
EOF
  exit 2
}

die() {
  printf '%s\n' "$1" >&2
  exit 2
}

require_jq() {
  command -v jq >/dev/null 2>&1 || die "jq is required"
}

validate_name() {
  local kind=$1
  local value=$2
  [[ $value =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || die "invalid ${kind}: ${value}"
}

catalog_dir() {
  local root=$1
  printf '%s/catalog/adapters' "$root"
}

catalog_file() {
  local root=$1
  local adapter=$2
  printf '%s/%s.json' "$(catalog_dir "$root")" "$adapter"
}

state_file() {
  local root=$1
  local machine=$2
  printf '%s/%s/adapters-state.json' "$root" "$machine"
}

secret_dir() {
  local root=$1
  local machine=$2
  local adapter=$3
  printf '%s/%s/secrets/%s' "$root" "$machine" "$adapter"
}

secret_file() {
  local root=$1
  local machine=$2
  local adapter=$3
  local secret=$4
  printf '%s/%s' "$(secret_dir "$root" "$machine" "$adapter")" "$secret"
}

models_cache_file() {
  local root=$1
  local machine=$2
  local adapter=$3
  printf '%s/%s/cache/available-models/%s.json' "$root" "$machine" "$adapter"
}

load_state() {
  local file=$1
  local machine=$2

  if [[ ! -e $file ]]; then
    jq -cn --arg machine "$machine" \
      '{version:"adapters-state/v1",machine:$machine,adapters:{}}'
    return 0
  fi

  jq -e --arg machine "$machine" '
    .version == "adapters-state/v1" and
    .machine == $machine and
    (.adapters | type == "object")
  ' "$file" >/dev/null 2>&1 || die "state file is invalid or version-mismatched: $file"

  jq -c . "$file"
}

atomic_write() {
  local file=$1
  local content=$2
  local dir tmp

  dir=$(dirname "$file")
  mkdir -p "$dir"
  tmp=$(mktemp "$dir/.adapters-state.tmp.XXXXXX")
  printf '%s\n' "$content" > "$tmp"
  mv -f "$tmp" "$file"
}

atomic_write_mode() {
  local file=$1
  local mode=$2
  local content=$3
  local dir tmp

  dir=$(dirname "$file")
  mkdir -p "$dir"
  tmp=$(mktemp "$dir/.tmp.XXXXXX")
  chmod "$mode" "$tmp"
  printf '%s' "$content" > "$tmp"
  mv -f "$tmp" "$file"
}

catalog_descriptor() {
  local root=$1
  local adapter=$2
  local file

  file=$(catalog_file "$root" "$adapter")
  if [[ ! -e $file ]]; then
    jq -cn --arg adapter "$adapter" '{id:$adapter}'
    return 0
  fi

  jq -e --arg adapter "$adapter" '
    type == "object" and
    ((.id // $adapter) == $adapter) and
    ((.config // {}) | type == "object") and
    ((.models // []) | type == "array") and
    ((.secrets // []) | type == "array") and
    ((.availableModelsCommand // []) | type == "array")
  ' "$file" >/dev/null 2>&1 || die "catalog descriptor is invalid: $file"

  jq -c --arg adapter "$adapter" '. + {id:(.id // $adapter)}' "$file"
}

list_adapter_ids() {
  local root=$1
  local state=$2
  local dir

  dir=$(catalog_dir "$root")
  {
    if [[ -d $dir ]]; then
      local path
      for path in "$dir"/*.json; do
        [[ -e $path ]] || continue
        basename "$path" .json
      done
    fi
    jq -r '.adapters | keys[]?' <<<"$state"
  } | LC_ALL=C sort -u
}

configured_secrets_json() {
  local root=$1
  local machine=$2
  local adapter=$3
  local dir

  dir=$(secret_dir "$root" "$machine" "$adapter")
  if [[ ! -d $dir ]]; then
    printf '[]\n'
    return 0
  fi

  find "$dir" -maxdepth 1 -type f -printf '%f\n' | LC_ALL=C sort | jq -R . | jq -s .
}

effective_descriptor() {
  local root=$1
  local machine=$2
  local adapter=$3
  local state=$4
  local descriptor configured_secrets

  descriptor=$(catalog_descriptor "$root" "$adapter")
  configured_secrets=$(configured_secrets_json "$root" "$machine" "$adapter")

  jq -cn \
    --argjson descriptor "$descriptor" \
    --argjson state "$state" \
    --arg adapter "$adapter" \
    --argjson configuredSecrets "$configured_secrets" '
    ($state.adapters[$adapter] // {}) as $adapterState
    | $descriptor
    | .enabled = ($adapterState.enabled // .enabled // false)
    | .config = ((.config // {}) * ($adapterState.config // {}))
    | .models = (if ($adapterState | has("models")) then $adapterState.models else (.models // []) end)
    | .requiredSecrets = (.secrets // [])
    | .configuredSecrets = $configuredSecrets
    | del(.secrets)
  '
}

cache_is_fresh() {
  local file=$1
  [[ -e $file ]] || return 1
  find "$file" -mtime -1 -print -quit | grep -q .
}

available_models_from_descriptor() {
  local descriptor=$1
  local -a cmd=()

  mapfile -t cmd < <(jq -r '.availableModelsCommand[]?' <<<"$descriptor")
  [[ ${#cmd[@]} -gt 0 ]] || die "adapter does not define availableModelsCommand"

  local output
  output=$("${cmd[@]}") || die "availableModelsCommand failed"
  jq -e 'type == "array" and all(.[]; type == "string")' >/dev/null <<<"$output" 2>/dev/null \
    || die "availableModelsCommand must return a JSON array of strings"
  jq -c . <<<"$output"
}

parse_args() {
  ROOT=
  MACHINE=
  ADAPTER=
  CONFIG_JSON=
  SECRET_NAME=
  MODELS=()

  while [[ $# -gt 0 ]]; do
    case "$1" in
      --root)
        [[ $# -ge 2 ]] || usage
        ROOT=$2
        shift 2
        ;;
      --machine)
        [[ $# -ge 2 ]] || usage
        MACHINE=$2
        shift 2
        ;;
      --adapter)
        [[ $# -ge 2 ]] || usage
        ADAPTER=$2
        shift 2
        ;;
      --json)
        [[ $# -ge 2 ]] || usage
        CONFIG_JSON=$2
        shift 2
        ;;
      --secret)
        [[ $# -ge 2 ]] || usage
        SECRET_NAME=$2
        shift 2
        ;;
      --)
        shift
        while [[ $# -gt 0 ]]; do
          MODELS+=("$1")
          shift
        done
        ;;
      -*)
        usage
        ;;
      *)
        MODELS+=("$1")
        shift
        ;;
    esac
  done
}

main() {
  require_jq
  [[ $# -ge 1 ]] || usage

  local command=$1
  shift

  parse_args "$@"

  [[ -n ${ROOT:-} && -n ${MACHINE:-} ]] || usage
  validate_name machine "$MACHINE"

  local file current next descriptor cache_file secret_path secret_value
  file=$(state_file "$ROOT" "$MACHINE")
  current=$(load_state "$file" "$MACHINE")

  case "$command" in
    list)
      local -a adapter_ids=()
      while IFS= read -r adapter_id; do
        [[ -n $adapter_id ]] || continue
        adapter_ids+=("$adapter_id")
      done < <(list_adapter_ids "$ROOT" "$current")

      {
        printf '[\n'
        local first=1
        local adapter_id
        for adapter_id in "${adapter_ids[@]}"; do
          if [[ $first -eq 0 ]]; then
            printf ',\n'
          fi
          effective_descriptor "$ROOT" "$MACHINE" "$adapter_id" "$current"
          first=0
        done
        printf '\n]\n'
      } | jq -c .
      ;;
    show)
      [[ -n ${ADAPTER:-} ]] || usage
      validate_name adapter "$ADAPTER"
      effective_descriptor "$ROOT" "$MACHINE" "$ADAPTER" "$current" | jq -c .
      ;;
    enable)
      [[ -n ${ADAPTER:-} ]] || usage
      validate_name adapter "$ADAPTER"
      next=$(jq -cn --argjson state "$current" --arg adapter "$ADAPTER" '
        $state
        | .adapters[$adapter] = ((.adapters[$adapter] // {enabled:false,config:{},models:[]}) + {enabled:true})
      ')
      atomic_write "$file" "$(jq -c . <<<"$next")"
      ;;
    disable)
      [[ -n ${ADAPTER:-} ]] || usage
      validate_name adapter "$ADAPTER"
      next=$(jq -cn --argjson state "$current" --arg adapter "$ADAPTER" '
        $state
        | .adapters[$adapter] = ((.adapters[$adapter] // {enabled:false,config:{},models:[]}) + {enabled:false})
      ')
      atomic_write "$file" "$(jq -c . <<<"$next")"
      ;;
    config)
      [[ -n ${ADAPTER:-} ]] || usage
      validate_name adapter "$ADAPTER"
      [[ -n ${CONFIG_JSON:-} ]] || usage
      jq -e 'type == "object"' >/dev/null <<<"$CONFIG_JSON" 2>/dev/null || die "config JSON must be an object"
      next=$(jq -cn --argjson state "$current" --arg adapter "$ADAPTER" --argjson config "$CONFIG_JSON" '
        $state
        | .adapters[$adapter] = ((.adapters[$adapter] // {enabled:false,config:{},models:[]}) + {config:$config})
      ')
      atomic_write "$file" "$(jq -c . <<<"$next")"
      ;;
    set-models)
      [[ -n ${ADAPTER:-} ]] || usage
      validate_name adapter "$ADAPTER"
      local models_json='[]'
      if [[ ${#MODELS[@]} -gt 0 ]]; then
        models_json=$(printf '%s\n' "${MODELS[@]}" | jq -R . | jq -s .)
      fi
      next=$(jq -cn --argjson state "$current" --arg adapter "$ADAPTER" --argjson models "$models_json" '
        $state
        | .adapters[$adapter] = ((.adapters[$adapter] // {enabled:false,config:{},models:[]}) + {models:$models})
      ')
      atomic_write "$file" "$(jq -c . <<<"$next")"
      ;;
    set-secret)
      [[ -n ${ADAPTER:-} && -n ${SECRET_NAME:-} ]] || usage
      validate_name adapter "$ADAPTER"
      validate_name secret "$SECRET_NAME"
      [[ ! -t 0 ]] || die "set-secret requires stdin"
      secret_path=$(secret_file "$ROOT" "$MACHINE" "$ADAPTER" "$SECRET_NAME")
      secret_value=$(cat)
      atomic_write_mode "$secret_path" 600 "$secret_value"
      ;;
    available-models)
      [[ -n ${ADAPTER:-} ]] || usage
      validate_name adapter "$ADAPTER"
      cache_file=$(models_cache_file "$ROOT" "$MACHINE" "$ADAPTER")
      if cache_is_fresh "$cache_file"; then
        jq -e 'type == "array" and all(.[]; type == "string")' "$cache_file" >/dev/null 2>&1 \
          || die "available-models cache is invalid: $cache_file"
        jq -c . "$cache_file"
      else
        descriptor=$(effective_descriptor "$ROOT" "$MACHINE" "$ADAPTER" "$current")
        next=$(available_models_from_descriptor "$descriptor")
        atomic_write "$cache_file" "$next"
        jq -c . <<<"$next"
      fi
      ;;
    *)
      usage
      ;;
  esac
}

main "$@"
