#!/bin/bash
# ontoref — global entry point for the ontoref protocol CLI.
# Release: 0.1.0
#
# Discovers project root by walking up from CWD looking for .ontology/.
# Fully self-contained: does NOT delegate to the dev repo.
#
# ONTOREF_ROOT is baked at install time to the data directory
# (macOS: ~/Library/Application Support/ontoref, Linux: ~/.local/share/ontoref)
# where reflection/ scripts are installed.
#
# Usage:
#   ontoref adr l
#   ontoref sync
#   ONTOREF_PROJECT_ROOT=/path/to/project ontoref adr l   # explicit override

set -euo pipefail

_release() { grep "^# Release:" "$0" | sed "s/# Release: //g"; }

# Baked at install time — replaced by install.nu with the actual data dir path.
ONTOREF_ROOT="${ONTOREF_ROOT:-ontoref}"

readonly DISPATCHER="${ONTOREF_ROOT}/reflection/bin/ontoref.nu"

# ── Nushell check ─────────────────────────────────────────────────────────────

if ! command -v nu &>/dev/null; then
  echo ""
  echo "  ontoref requires Nushell (>= 0.110.0)"
  echo ""
  echo "  Install:"
  echo "    cargo install nu          # via Rust"
  echo "    brew install nushell      # macOS"
  echo "    winget install nushell    # Windows"
  echo ""
  echo "  Then re-run: ontoref $*"
  echo ""
  exit 1
fi

# ── Version gate ──────────────────────────────────────────────────────────────

NU_VERSION="$(nu --version 2>/dev/null | tr -d '[:space:]')"
NU_MAJOR="${NU_VERSION%%.*}"
NU_MAJOR="${NU_MAJOR%%[^0-9]*}"
NU_MINOR="${NU_VERSION#*.}"; NU_MINOR="${NU_MINOR%%.*}"
NU_MINOR="${NU_MINOR%%[^0-9]*}"

if [[ "${NU_MAJOR}" -lt 0 ]] || { [[ "${NU_MAJOR}" -eq 0 ]] && [[ "${NU_MINOR}" -lt 110 ]]; }; then
  echo ""
  echo "  Nushell ${NU_VERSION} found — 0.110.0+ required"
  echo "  Update: cargo install nu --force"
  echo ""
  exit 1
fi

# ── Guard: dispatcher must exist ──────────────────────────────────────────────

if [[ ! -f "${DISPATCHER}" ]]; then
  echo "ontoref: reflection scripts not found at ${ONTOREF_ROOT}/reflection/" >&2
  echo "  Re-run the installer: nu install/install.nu" >&2
  exit 1
fi

# ── Version flag (early exit) ─────────────────────────────────────────────────

for _arg in "$@"; do
  case "${_arg}" in
    -V|-v|--version|version)
      echo "ontoref $(_release)"
      exit 0
      ;;
  esac
done

# ── Actor detection ────────────────────────────────────────────────────────────

ACTOR_FROM_ARGS=""
ENV_ONLY=0
REMAINING_ARGS=()
while [[ $# -gt 0 ]]; do
  case "$1" in
    --actor)
      shift
      ACTOR_FROM_ARGS="${1:-}"
      shift
      ;;
    --actor=*)
      ACTOR_FROM_ARGS="${1#--actor=}"
      shift
      ;;
    --env-only)
      ENV_ONLY=1
      shift
      ;;
    *)
      REMAINING_ARGS+=("$1")
      shift
      ;;
  esac
done

if [[ -n "${ACTOR_FROM_ARGS}" ]]; then
  export ONTOREF_ACTOR="${ACTOR_FROM_ARGS}"
elif [[ -n "${ONTOREF_ACTOR:-}" ]]; then
  : # already set by caller
elif [[ -n "${CI:-}" ]] || [[ -n "${GITHUB_ACTIONS:-}" ]] || [[ -n "${WOODPECKER_BUILD_NUMBER:-}" ]]; then
  export ONTOREF_ACTOR="ci"
elif [[ ! -t 0 ]]; then
  export ONTOREF_ACTOR="agent"
else
  export ONTOREF_ACTOR="developer"
fi

# ── Project root discovery ─────────────────────────────────────────────────────

_find_project_root() {
  local dir
  dir="$(pwd)"
  while [[ "${dir}" != "/" ]]; do
    [[ -d "${dir}/.ontology" ]] && echo "${dir}" && return 0
    dir="$(dirname "${dir}")"
  done
  return 1
}

if [[ -z "${ONTOREF_PROJECT_ROOT:-}" ]]; then
  if ! ONTOREF_PROJECT_ROOT="$(_find_project_root)"; then
    echo "ontoref: no .ontology/ found from $(pwd) up to /" >&2
    echo "  Run from within a project that has .ontology/, or set ONTOREF_PROJECT_ROOT." >&2
    exit 1
  fi
fi

export ONTOREF_ROOT
export ONTOREF_PROJECT_ROOT

readonly CONFIG_NCL="${ONTOREF_PROJECT_ROOT}/.ontoref/config.ncl"
readonly LOCKS_DIR="${ONTOREF_PROJECT_ROOT}/.ontoref/locks"

# ── Load project env vars (NICKEL_IMPORT_PATH) ────────────────────────────────

load_ontoref_env() {
  # Installed schemas dir provides ontoref-project.ncl and other protocol schemas.
  local installed_schemas="${HOME}/.config/ontoref/schemas"

  # $ONTOREF_ROOT/ontology is included so consumer projects can resolve:
  #   import "defaults/state.ncl"   → $ONTOREF_ROOT/ontology/defaults/state.ncl
  #   import "schemas/manifest.ncl" → $ONTOREF_ROOT/ontology/schemas/manifest.ncl
  local ontoref_ontology="${ONTOREF_ROOT}/ontology"
  local fallback_paths="${ONTOREF_PROJECT_ROOT}:${ONTOREF_PROJECT_ROOT}/.ontology:${ONTOREF_PROJECT_ROOT}/adrs:${installed_schemas}:${ontoref_ontology}:${ONTOREF_ROOT}"

  if [[ ! -f "${CONFIG_NCL}" ]]; then
    export NICKEL_IMPORT_PATH="${fallback_paths}"
    return 0
  fi

  local raw_paths=""
  if command -v nickel &>/dev/null; then
    # nickel_import_paths from project config are relative to ONTOREF_PROJECT_ROOT.
    # shellcheck disable=SC2016
    raw_paths="$(nickel export "${CONFIG_NCL}" 2>/dev/null | ONTOREF_ROOT="${ONTOREF_PROJECT_ROOT}" nu -c 'from json | get nickel_import_paths | each { |p| $env.ONTOREF_ROOT + "/" + $p } | str join ":"' 2>/dev/null)" || true
  fi

  if [[ -n "${raw_paths}" ]]; then
    export NICKEL_IMPORT_PATH="${raw_paths}:${installed_schemas}:${ontoref_ontology}:${ONTOREF_ROOT}"
  else
    export NICKEL_IMPORT_PATH="${fallback_paths}"
  fi
}

if [[ -z "${NICKEL_IMPORT_PATH:-}" ]]; then
  load_ontoref_env
fi

# ── Export ONTOREF_DOMAIN_ROOT from domain_origin.path ────────────────────────
# If the project declares domain_origin in its manifest, expose the framework path
# so NCL extension files can import from: import "%{env.ONTOREF_DOMAIN_ROOT}/.ontology/..."
# Uses grep (no nickel penalty) — path line expected to be a simple string literal.
if [[ -z "${ONTOREF_DOMAIN_ROOT:-}" ]]; then
  _manifest="${ONTOREF_PROJECT_ROOT}/.ontology/manifest.ncl"
  if grep -q "domain_origin" "${_manifest}" 2>/dev/null; then
    _domain_path="$(grep -A5 "domain_origin" "${_manifest}" | grep 'path\s*=' | head -1 | sed 's/.*=\s*"\(.*\)".*/\1/')"
    if [[ -n "${_domain_path}" && -d "${_domain_path}" ]]; then
      export ONTOREF_DOMAIN_ROOT="${_domain_path}"
    fi
    unset _domain_path
  fi
  unset _manifest
fi

# ── Advisory locking (mkdir-based, POSIX-atomic) ──────────────────────────────

determine_lock() {
  local cmd="${REMAINING_ARGS[0]:-}"
  local sub="${REMAINING_ARGS[1]:-}"
  case "${cmd}" in
    config)
      case "${sub}" in
        apply|rollback) echo "manifest" ;;
        *) echo "" ;;
      esac
      ;;
    register) echo "changelog" ;;
    backlog)
      case "${sub}" in
        done|cancel) echo "backlog" ;;
        *) echo "" ;;
      esac
      ;;
    *) echo "" ;;
  esac
}

is_stale_lock() {
  local lockdir="$1"
  local owner_file="${lockdir}/owner"
  if [[ ! -f "${owner_file}" ]]; then return 0; fi
  local owner_pid
  owner_pid="$(cut -d: -f1 "${owner_file}" 2>/dev/null)" || return 0
  if [[ -z "${owner_pid}" ]]; then return 0; fi
  if ! kill -0 "${owner_pid}" 2>/dev/null; then return 0; fi
  return 1
}

ACQUIRED_LOCK=""

acquire_lock() {
  local resource="$1"
  local timeout="${2:-30}"
  local lockdir="${LOCKS_DIR}/${resource}.lock"
  local elapsed=0

  mkdir -p "${LOCKS_DIR}"

  local stale_retries=0
  while ! mkdir "${lockdir}" 2>/dev/null; do
    local stale=0
    # shellcheck disable=SC2310
    is_stale_lock "${lockdir}" && stale=1 || true
    if [[ "${stale}" -eq 1 ]]; then
      stale_retries=$((stale_retries + 1))
      if [[ "${stale_retries}" -gt 5 ]]; then
        echo "  ontoref: stale lock on '${resource}' could not be cleared after 5 attempts" >&2
        return 1
      fi
      rm -rf "${lockdir}"
      sleep 0.1
      continue
    fi
    if [[ "${elapsed}" -ge "${timeout}" ]]; then
      local owner="unknown"
      [[ -f "${lockdir}/owner" ]] && owner="$(cat "${lockdir}/owner")"
      echo "  ontoref: lock timeout on '${resource}' after ${timeout}s (held by: ${owner})" >&2
      return 1
    fi
    sleep 1
    elapsed=$(( elapsed + 1 ))
  done

  local now
  now="$(date -u +%Y%m%dT%H%M%SZ)"
  echo "$$:${ONTOREF_ACTOR}:${now}" > "${lockdir}/owner"
  ACQUIRED_LOCK="${lockdir}"
}

release_lock() {
  if [[ -n "${ACQUIRED_LOCK}" ]]; then
    rm -rf "${ACQUIRED_LOCK}" 2>/dev/null || true
    ACQUIRED_LOCK=""
  fi
}

# ── Export caller identity ─────────────────────────────────────────────────────

export ONTOREF_CALLER="${ONTOREF_CALLER:-ontoref}"

if [[ "${ENV_ONLY}" -eq 1 ]]; then
  # shellcheck disable=SC2317
  return 0 2>/dev/null || exit 0
fi

# ── No-args usage ─────────────────────────────────────────────────────────────

if [[ "${#REMAINING_ARGS[@]}" -eq 0 ]]; then
  echo ""
  echo "  ontoref"
  echo "  Usage: ontoref <command> [options]"
  echo ""
  echo "  Use 'ontoref help' for available commands"
  echo ""
  exit 0
fi

# ── Rewrite help flags ────────────────────────────────────────────────────────

_has_help=0
_non_help_args=()
for _a in "${REMAINING_ARGS[@]+"${REMAINING_ARGS[@]}"}"; do
  case "${_a}" in
    --help|-help|-h) _has_help=1 ;;
    *) _non_help_args+=("${_a}") ;;
  esac
done
if [[ "${_has_help}" -eq 1 ]]; then
  if [[ "${#_non_help_args[@]}" -gt 0 ]]; then
    REMAINING_ARGS=("help" "${_non_help_args[@]}")
  else
    REMAINING_ARGS=("help")
  fi
fi

# ── Normalize --fmt/-f: extract from any position and append after subcommand ─
_fmt_val=""
_no_fmt_args=()
_fi=0
while [[ $_fi -lt ${#REMAINING_ARGS[@]} ]]; do
  _a="${REMAINING_ARGS[$_fi]}"
  case "${_a}" in
    --fmt|-f|--format|-fmt)
      _fi=$(( _fi + 1 ))
      _fmt_val="${REMAINING_ARGS[$_fi]:-}"
      ;;
    --fmt=*|--format=*)
      _fmt_val="${_a#*=}"
      ;;
    *) _no_fmt_args+=("${_a}") ;;
  esac
  _fi=$(( _fi + 1 ))
done
if [[ -n "${_fmt_val}" ]]; then
  REMAINING_ARGS=("${_no_fmt_args[@]+"${_no_fmt_args[@]}"}" "--fmt" "${_fmt_val}")
fi

# ── Fix trailing flags that require a value ────────────────────────────────────
if [[ "${#REMAINING_ARGS[@]}" -gt 0 ]]; then
  _last="${REMAINING_ARGS[${#REMAINING_ARGS[@]}-1]}"
  # shellcheck disable=SC2249
  case "${_last}" in
    --fmt|--format|-fmt|--actor|--context|--severity|--backend|--kind|--priority|--status)
      REMAINING_ARGS+=("select")
      ;;
  esac
fi

# ── Universal --clip: capture stdout, strip ANSI, copy to clipboard ───────────
_has_clip=0
_no_clip_args=()
for _a in "${REMAINING_ARGS[@]+"${REMAINING_ARGS[@]}"}"; do
  case "${_a}" in
    --clip|-c) _has_clip=1 ;;
    *) _no_clip_args+=("${_a}") ;;
  esac
done

_strip_ansi() { sed $'s/\033\\[[0-9;]*[mGKHFJABCDEFM]//g'; }

_copy_to_clipboard() {
  if command -v pbcopy &>/dev/null; then
    printf '%s' "${1}" | pbcopy
  elif command -v xclip &>/dev/null; then
    printf '%s' "${1}" | xclip -selection clipboard
  elif command -v wl-copy &>/dev/null; then
    printf '%s' "${1}" | wl-copy
  else
    echo "  No clipboard tool found (install pbcopy, xclip, or wl-copy)" >&2
    return 1
  fi
  echo "  ✓ Copied to clipboard" >&2
}

# ── Domain extension dispatch ─────────────────────────────────────────────────
# If the first arg matches a domain id and the project's repo_kind is in that
# domain's repo_kinds.txt, delegate to the domain's commands.nu directly.

_dispatch_domain() {
  local first_arg="${REMAINING_ARGS[0]:-}"
  [[ -z "$first_arg" ]] && return 1

  # Resolve short alias → domain id (e.g. prov → provisioning, jpl → personal).
  local aliases_file="${ONTOREF_ROOT}/domains/aliases.txt"
  if [[ -f "$aliases_file" ]]; then
    local resolved
    resolved="$(grep "^${first_arg}=" "$aliases_file" 2>/dev/null | cut -d= -f2)" || true
    if [[ -n "$resolved" ]]; then
      first_arg="$resolved"
      REMAINING_ARGS=("$first_arg" "${REMAINING_ARGS[@]:1}")
    fi
  fi

  local domain_dir="${ONTOREF_ROOT}/domains/${first_arg}"
  # Not a known domain ID or alias — fall through to Nu dispatcher as normal command.
  [[ ! -d "$domain_dir" ]] && return 1

  # From here on, the arg IS a domain name. All failures become diagnostics, not fall-throughs.

  local repo_kinds_file="${domain_dir}/repo_kinds.txt"
  if [[ ! -f "$repo_kinds_file" ]]; then
    echo "ontoref: domain '${first_arg}' is missing repo_kinds.txt — reinstall ontoref" >&2
    exit 1
  fi

  local manifest="${ONTOREF_PROJECT_ROOT}/.ontology/manifest.ncl"
  if [[ ! -f "$manifest" ]]; then
    echo "" >&2
    echo "  ontoref: domain '${first_arg}' requires a project with .ontology/manifest.ncl" >&2
    echo "  current project: ${ONTOREF_PROJECT_ROOT}" >&2
    echo "" >&2
    exit 1
  fi

  # Extract repo_kind directly from the NCL source — no nickel export needed.
  # The field is always a literal enum tag: repo_kind = 'SomeName,
  # This avoids import-path resolution failures for manifests that import schemas.
  local repo_kind=""
  repo_kind="$(grep -oE "repo_kind\s*=\s*'[A-Za-z_]+" "$manifest" 2>/dev/null | grep -oE "'[A-Za-z_]+$" | tr -d "'")" || true

  if [[ -z "$repo_kind" ]]; then
    echo "" >&2
    echo "  ontoref: domain '${first_arg}' is not available for this project" >&2
    echo "  reason: repo_kind is not set in ${manifest}" >&2
    echo "" >&2
    exit 1
  fi

  if ! grep -qx "$repo_kind" "$repo_kinds_file" 2>/dev/null; then
    local required
    required="$(tr '\n' ' ' < "$repo_kinds_file" | sed 's/ $//')"
    echo "" >&2
    echo "  ontoref: domain '${first_arg}' is not available for this project" >&2
    echo "  requires repo_kind: ${required}" >&2
    echo "  current repo_kind:  ${repo_kind}" >&2
    echo "" >&2
    exit 1
  fi

  local domain_cmd="${domain_dir}/commands.nu"
  if [[ ! -f "$domain_cmd" ]]; then
    echo "ontoref: domain '${first_arg}' commands.nu not found — reinstall ontoref" >&2
    exit 1
  fi

  local domain_args=("${REMAINING_ARGS[@]:1}")
  nu "$domain_cmd" "${domain_args[@]+"${domain_args[@]}"}"
  return 0
}

if _dispatch_domain; then
  exit 0
fi

# ── Delegate to Nushell dispatcher ────────────────────────────────────────────

LOCK_RESOURCE="$(determine_lock)"

# --clip strategy:
#   Structured --fmt (json/yaml/toml/md): non-interactive subprocess capture via stdin redirect.
#   Text (no --fmt or --fmt text): pass --clip to Nushell — it handles clipboard after selection.
_fmt_is_structured=0
case "${_fmt_val}" in
  json|yaml|toml|md|j|y|t|m) _fmt_is_structured=1 ;;
esac

if [[ "${_has_clip}" -eq 1 ]] && [[ "${_fmt_is_structured}" -eq 1 ]]; then
  if [[ -n "${LOCK_RESOURCE}" ]]; then
    acquire_lock "${LOCK_RESOURCE}" 30
    trap 'release_lock' EXIT INT TERM
  fi
  _captured="$(nu "${DISPATCHER}" "${_no_clip_args[@]+"${_no_clip_args[@]}"}" 2>&1 < /dev/null | _strip_ansi)"
  printf '%s\n' "${_captured}"
  _copy_to_clipboard "${_captured}"
elif [[ "${_has_clip}" -eq 1 ]]; then
  if [[ -n "${LOCK_RESOURCE}" ]]; then
    acquire_lock "${LOCK_RESOURCE}" 30
    trap 'release_lock' EXIT INT TERM
  fi
  # Text mode: pass --clip through; Nushell copies after interactive selection.
  nu "${DISPATCHER}" "${_no_clip_args[@]+"${_no_clip_args[@]}"}" "--clip"
else
  if [[ -n "${LOCK_RESOURCE}" ]]; then
    acquire_lock "${LOCK_RESOURCE}" 30
    trap 'release_lock' EXIT INT TERM
  fi
  nu "${DISPATCHER}" "${REMAINING_ARGS[@]+"${REMAINING_ARGS[@]}"}"
fi
