#!/usr/bin/env bash
# Shared bash reader for framework-manifest.txt (#791).
#
# This is the bash half of the SSOT ownership resolver; the TypeScript half is
# packages/mosaic/src/framework/manifest.ts. BOTH read the same
# framework-manifest.txt and MUST resolve identical ownership for any path — the
# parity test (manifest-parity.spec.ts) invokes this file's `resolve` CLI and
# compares it against the TS resolver, so the two can never drift (the #631
# two-copies failure class this closes).
#
# Ownership resolution (deny-wins / fail-safe):
#   1. operator glob matches  -> operator
#   2. else framework glob    -> framework
#   3. else                   -> operator   (UNKNOWN defaults to operator, #791)
#
# Globs are compiled once at load into exact-prefix checks or POSIX EREs, so the
# hot resolver (manifest_is_framework) forks no subprocesses — the installer
# calls it once per file across the whole tree.
#
# Usage as a library (source it, then):
#   manifest_load [manifest-file]      # populates + compiles the manifest
#   manifest_is_framework <rel-path>   # rc 0 = framework-owned, rc 1 = operator
#   manifest_resolve <rel-path>        # echoes: framework | operator
#   manifest_subtree_roots             # echoes shipped framework `dir/**` roots
#
# Usage as a CLI (parity harness):
#   bash manifest.sh resolve <rel-path>
#   bash manifest.sh subtree-roots
#   bash manifest.sh classify          # reads paths on stdin -> "<own>\t<path>"

MANIFEST_FRAMEWORK=()
MANIFEST_OPERATOR=()

# Compiled forms (parallel arrays). _*_KIND[i] is "exact" or "re".
_MF_KIND=(); _MF_EXACT=(); _MF_RE=()
_MO_KIND=(); _MO_EXACT=(); _MO_RE=()
_MF_ROOTS=()

_manifest_default_root() { cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd; }

# Normalize a path/glob: backslashes -> slashes, strip leading ./ and /, strip
# trailing / (mirrors normalizeRel in manifest.ts).
_manifest_norm() {
  local p="$1"
  p="${p//\\//}"
  p="${p#./}"
  while [[ "$p" == /* ]]; do p="${p#/}"; done
  while [[ "$p" == */ ]]; do p="${p%/}"; done
  printf '%s' "$p"
}

# Translate a normalized glob into a POSIX ERE body (mirrors globToRegExpBody).
_manifest_glob_to_ere() {
  local pattern; pattern="$(_manifest_norm "$1")"
  local out="" c n i len=${#pattern} trailing
  for (( i = 0; i < len; i++ )); do
    c="${pattern:i:1}"
    if [[ "$c" == "*" ]]; then
      n="${pattern:i+1:1}"
      if [[ "$n" == "*" ]]; then
        i=$((i + 1))
        trailing=0
        if [[ "${pattern:i+1:1}" == "/" ]]; then i=$((i + 1)); trailing=1; fi
        if [[ "$out" == */ ]]; then
          out="${out%/}(/.*)?"
        elif [[ "$trailing" -eq 1 ]]; then
          out="$out(.*/)?"
        else
          out="$out.*"
        fi
      else
        out="${out}[^/]*"
      fi
    else
      case "$c" in
        .|+|\?|^|\$|\{|\}|\(|\)|\||\[|\]|\\) out="$out\\$c" ;;
        *) out="$out$c" ;;
      esac
    fi
  done
  printf '%s' "$out"
}

# Compile one raw glob into (kind, exact, re) appended to the given section.
# $1 = raw glob, $2 = section letter (F|O).
_manifest_compile_one() {
  local norm; norm="$(_manifest_norm "$1")"
  [[ -n "$norm" ]] || return 0
  if [[ "$norm" == *"*"* ]]; then
    local re
    re="^$(_manifest_glob_to_ere "$norm")\$"
    if [[ "$2" == F ]]; then
      _MF_KIND+=(re); _MF_EXACT+=(""); _MF_RE+=("$re")
    else
      _MO_KIND+=(re); _MO_EXACT+=(""); _MO_RE+=("$re")
    fi
  else
    if [[ "$2" == F ]]; then
      _MF_KIND+=(exact); _MF_EXACT+=("$norm"); _MF_RE+=("")
    else
      _MO_KIND+=(exact); _MO_EXACT+=("$norm"); _MO_RE+=("")
    fi
  fi
  [[ "$2" == F && "$norm" == */"**" ]] && _MF_ROOTS+=("${norm%/**}")
  return 0
}

_manifest_compile() {
  _MF_KIND=(); _MF_EXACT=(); _MF_RE=(); _MF_ROOTS=()
  _MO_KIND=(); _MO_EXACT=(); _MO_RE=()
  local g
  for g in "${MANIFEST_FRAMEWORK[@]:-}"; do [[ -n "$g" ]] && _manifest_compile_one "$g" F; done
  for g in "${MANIFEST_OPERATOR[@]:-}"; do [[ -n "$g" ]] && _manifest_compile_one "$g" O; done
  # Explicit success: an empty operator array makes the final `[[ -n "" ]] && …`
  # short-circuit to rc 1, which would otherwise become this function's (and
  # manifest_load's) return code — a spurious failure (#791 B2). Never rely on
  # the last loop's exit status here.
  return 0
}

# Load + compile the manifest. Rejects a malformed file the same way
# parseManifest() does (entry before a section header / unknown header).
manifest_load() {
  local file="${1:-}"
  [[ -n "$file" ]] || file="$(_manifest_default_root)/framework-manifest.txt"
  # Fail CLOSED on a missing/unreadable manifest. Without this, `done < "$file"`
  # aborts on a raw redirection error with no explanation; downstream that reads
  # as "no framework paths" and an upgrade could no-op silently (#791 B2/B3).
  if [[ ! -r "$file" ]]; then
    echo "manifest: cannot read manifest file: $file — refusing to sync (fail-closed)." >&2
    return 1
  fi
  MANIFEST_FRAMEWORK=()
  MANIFEST_OPERATOR=()
  local section="" line
  while IFS= read -r line || [[ -n "$line" ]]; do
    line="${line#"${line%%[![:space:]]*}"}" # ltrim
    line="${line%"${line##*[![:space:]]}"}" # rtrim
    [[ -z "$line" || "${line:0:1}" == "#" ]] && continue
    case "$line" in
      "[framework]") section=framework; continue ;;
      "[operator]") section=operator; continue ;;
      "["*) echo "manifest: unknown section header: $line" >&2; return 1 ;;
    esac
    if [[ -z "$section" ]]; then
      echo "manifest: entry before any [section] header: $line" >&2
      return 1
    fi
    if [[ "$section" == framework ]]; then
      MANIFEST_FRAMEWORK+=("$line")
    else
      MANIFEST_OPERATOR+=("$line")
    fi
  done < "$file"
  # An empty or comment-only manifest defines NO framework-owned paths. Treating
  # that as valid would make every path resolve operator and an upgrade prune
  # nothing / write nothing — a silent no-op indistinguishable from success.
  # Fail loud instead, mirroring parseManifest()'s throw in manifest.ts (#791 B2).
  if [[ ${#MANIFEST_FRAMEWORK[@]} -eq 0 ]]; then
    echo "manifest: no [framework] entries in $file — refusing to sync (empty or malformed manifest)." >&2
    return 1
  fi
  # An entry like `/` or `./` normalizes to nothing and compiles to a glob that
  # matches no path — so a manifest whose only [framework] entries are degenerate
  # passes the count guard above but leaves the framework matcher empty: every
  # path resolves operator, the exact silent no-op we fail closed against. Require
  # at least one entry with a real (non-slash, non-dot) character. Mirrors
  # parseManifest()'s `isUsableFrameworkGlob` `/[^/.]/` test in manifest.ts (#791 blocker-B).
  local _g _usable=0
  for _g in "${MANIFEST_FRAMEWORK[@]:-}"; do
    if [[ "$(_manifest_norm "$_g")" =~ [^/.] ]]; then _usable=1; break; fi
  done
  if [[ "$_usable" -eq 0 ]]; then
    echo "manifest: no usable [framework] entries in $file (every entry is empty or a bare dot segment) — refusing to sync (malformed manifest)." >&2
    return 1
  fi
  _manifest_compile
  return 0
}

# Fork-free: does $1 (a mosaic-home-relative path) match an operator glob?
_mo_matches() {
  local path="$1" i n=${#_MO_KIND[@]} re pat
  for (( i = 0; i < n; i++ )); do
    if [[ "${_MO_KIND[i]}" == exact ]]; then
      pat="${_MO_EXACT[i]}"
      # Operator exact entries are file carve-outs, not implicit directory
      # prefixes. Subtree ownership must be declared explicitly as `dir/**`;
      # otherwise one bare directory entry can hide all drift beneath it.
      [[ "$path" == "$pat" ]] && return 0
    else
      re="${_MO_RE[i]}"
      [[ "$path" =~ $re ]] && return 0
    fi
  done
  return 1
}

# Fork-free: does $1 match a framework glob?
_mf_matches() {
  local path="$1" i n=${#_MF_KIND[@]} re pat
  for (( i = 0; i < n; i++ )); do
    if [[ "${_MF_KIND[i]}" == exact ]]; then
      pat="${_MF_EXACT[i]}"
      [[ "$path" == "$pat" || "$path" == "$pat/"* ]] && return 0
    else
      re="${_MF_RE[i]}"
      [[ "$path" =~ $re ]] && return 0
    fi
  done
  return 1
}

# The installer hot path — no subshell. rc 0 = framework-owned, rc 1 = operator
# (deny-wins / fail-safe). Assumes an already-clean POSIX relative path.
manifest_is_framework() {
  _mo_matches "$1" && return 1
  _mf_matches "$1" && return 0
  return 1
}

# Echo the ownership of a path: framework | operator. Normalizes first, so it is
# safe for CLI / test callers passing unnormalized input.
manifest_resolve() {
  local path; path="$(_manifest_norm "$1")"
  if manifest_is_framework "$path"; then echo framework; else echo operator; fi
}

# Echo each shipped framework subtree root (a `dir/**` entry, without the /**).
manifest_subtree_roots() {
  local r
  for r in "${_MF_ROOTS[@]:-}"; do [[ -n "$r" ]] && printf '%s\n' "$r"; done
}

# CLI dispatch — only when executed directly, never when sourced.
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
  set -o pipefail
  # Propagate a fail-closed manifest_load (missing/empty/malformed) as a non-zero
  # exit instead of continuing to resolve against empty compiled arrays — that is
  # what lets the parity test assert bash and TS reject the same bad inputs (#791 B2).
  manifest_load "${MANIFEST_FILE:-}" || exit 1
  cmd="${1:-}"
  case "$cmd" in
    resolve) manifest_resolve "${2:?path required}" ;;
    subtree-roots) manifest_subtree_roots ;;
    classify)
      while IFS= read -r p; do
        [[ -z "$p" ]] && continue
        printf '%s\t%s\n' "$(manifest_resolve "$p")" "$p"
      done
      ;;
    *)
      echo "usage: manifest.sh {resolve <path>|subtree-roots|classify}" >&2
      exit 2
      ;;
  esac
fi
