#!/usr/bin/env bash
# repo-decl.sh — shared .mosaic/repo.json consumption for the git wrappers (T51 WP5b).
#
# Spec of record: docs/plans/2026-08-23_repo-structure-declaration.md (brain
# repo) sections 4 (consumption contract), 5.3 (normalization), 5.4
# (enforcement points), 1.2a (root anchoring). ALL consumers invoke the SAME
# WP1 validator (spec 5.1 — no in-process-only parsing of the declaration).
#
# Source this file, then call repo_decl_load once. It sets:
#   DECL_STATE    absent | invalid | valid
#   DECL_FILE     the declaration path that was inspected
#   DECL_ERROR    the validator's error line when DECL_STATE=invalid
#   DECL_TRUNK / DECL_RELEASE / DECL_FLOW / DECL_REMOTE / DECL_POLICY /
#   DECL_WT_ROOT  / DECL_CLONE   (populated only when DECL_STATE=valid)
#   DECL_ORIGIN_N the normalized origin URL (when resolvable)
#
# Enforcement point 5.4(1): an invalid or unknown-version file counts as
# ABSENT for behavior, PLUS a loud error naming the file and the validator's
# key/reason — callers print DECL_ERROR (repo_decl_report_invalid) whenever
# they loaded something that failed validation; they do not silently ignore a
# broken file.
#
# Absence behavior (4.3) is the CALLER's policy (reversible vs irreversible;
# managed vs unmanaged — the adoption register is WP6, so during rollout every
# repo is unmanaged: warn + legacy). Helpers below provide the shared wordings.
#
# Root-dependent fields: consumers here read branch/flow/remote/policy only —
# NO path resolution happens in this library. The one helper that would
# resolve a host:/ path (repo_decl_path) fails closed while MOSAIC_HOST_ROOT
# is unset (1.2a: never guess a root), for any future caller that needs it.
#
# No output on success; diagnostics go to stderr.

REPO_DECL_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_DECL_VALIDATOR="$REPO_DECL_SCRIPT_DIR/../structure/validate-repo-json.sh"

repo_decl_warn() { printf 'repo-decl: %s\n' "$*" >&2; }

# Load and classify the declaration for the repo containing the current
# directory. Never fatal — classification is the product.
repo_decl_load() {
    DECL_STATE=absent
    DECL_FILE=""
    DECL_ERROR=""
    DECL_SCHEMA=""
    DECL_TRUNK=""; DECL_RELEASE=""; DECL_FLOW=""; DECL_REMOTE=""
    DECL_POLICY=""; DECL_WT_ROOT=""; DECL_CLONE=""
    DECL_ORIGIN_N=""

    local root
    root="$(git rev-parse --show-toplevel 2>/dev/null)" || {
        repo_decl_warn "no git repository — declaration consumption skipped"
        return 0
    }
    DECL_FILE="$root/.mosaic/repo.json"
    [ -f "$DECL_FILE" ] || return 0

    if [ ! -x "$REPO_DECL_VALIDATOR" ] && [ ! -f "$REPO_DECL_VALIDATOR" ]; then
        # 5.1 mandates the shared validator; a missing validator is an
        # infrastructure failure, not an absent declaration.
        repo_decl_warn "validator not found at $REPO_DECL_VALIDATOR — treating declaration as invalid"
        DECL_STATE=invalid
        DECL_ERROR="VALIDATION_ERROR validator: the shared validator is missing"
        return 0
    fi

    local vout
    if ! vout="$("$REPO_DECL_VALIDATOR" "$DECL_FILE" --mode display 2>&1)"; then
        DECL_STATE=invalid
        DECL_ERROR="$(printf '%s\n' "$vout" | grep -m1 'VALIDATION_ERROR' || printf '%s\n' "$vout" | head -1)"
        return 0
    fi

    # Valid: extract the consumed fields via the same python stdlib the
    # ecosystem already uses. Field-level grammar was the validator's job.
    eval "$(python3 - "$DECL_FILE" <<'PY'
import json, sys
d = json.load(open(sys.argv[1]))
def q(k):
    v = d.get(k, "")
    return v if isinstance(v, str) else ""
sv = d.get("schema_version", 1)
print(f"DECL_SCHEMA={sv if isinstance(sv, int) and not isinstance(sv, bool) else 0!r}")
print(f"DECL_TRUNK={q('integration_trunk')!r}")
print(f"DECL_RELEASE={q('release_branch')!r}")
print(f"DECL_FLOW={q('flow')!r}")
print(f"DECL_REMOTE={q('canonical_remote')!r}")
print(f"DECL_POLICY={q('worktree_policy')!r}")
print(f"DECL_WT_ROOT={q('worktree_root')!r}")
print(f"DECL_CLONE={q('canonical_clone')!r}")
PY
)" || {
        DECL_STATE=invalid
        DECL_ERROR="VALIDATION_ERROR internal: field extraction failed"
        return 0
    }
    DECL_STATE=valid

    # 5.3: normalize origin once for remote comparisons (read callers warn,
    # write callers refuse). An unresolvable origin is left empty — callers
    # treat empty as "cannot compare" and act per their read/write policy.
    local ourl
    if ourl="$(git remote get-url origin 2>/dev/null)" && [ -n "$ourl" ]; then
        DECL_ORIGIN_N="$("$REPO_DECL_VALIDATOR" --normalize-remote "$ourl" 2>/dev/null || true)"
    fi
    return 0
}

# The mandatory loud error for an invalid file (5.4 point 1). Callers invoke
# this whenever DECL_STATE=invalid, regardless of their proceed/refuse policy.
repo_decl_report_invalid() {
    repo_decl_warn "declaration INVALID at $DECL_FILE — $DECL_ERROR"
    repo_decl_warn "treating the declaration as ABSENT (spec 5.4); legacy behavior follows"
}

# Shared absence wordings (4.3, rollout window: no adoption register yet, so
# every repo is unmanaged; warn + legacy per the Q-C ruling).
repo_decl_warn_absent_reversible() { # $1 = operation name
    repo_decl_warn "no .mosaic/repo.json — $1 is unmanaged during rollout: legacy behavior, no declaration guarantees (spec 4.3)"
}
repo_decl_warn_absent_irreversible() { # $1 = operation name
    repo_decl_warn "no .mosaic/repo.json — $1 proceeds on LEGACY assumptions during the rollout window; declaration-validated transitions unavailable (spec 4.3)"
}

# Remote comparison (5.3). rc 0 match/unknown, rc 1 mismatch.
repo_decl_remote_matches() {
    [ "$DECL_STATE" = valid ] || return 0
    [ -n "$DECL_ORIGIN_N" ] && [ -n "$DECL_REMOTE" ] || return 0
    local want
    want="$("$REPO_DECL_VALIDATOR" --normalize-remote "$DECL_REMOTE" 2>/dev/null || true)"
    [ -n "$want" ] || return 0
    [ "$DECL_ORIGIN_N" = "$want" ]
}

# Resolve a host:/-anchored declaration path (1.2a). Fails CLOSED while
# MOSAIC_HOST_ROOT is unset or empty — never guesses a root. No WP5b consumer
# calls this today; it exists so the first one that needs a path cannot
# silently guess.
repo_decl_path() { # $1 = host:/... value; prints the resolved absolute path
    local v="${1:-}" root="${MOSAIC_HOST_ROOT:-}"
    case "$v" in
        host:/*) ;;
        *) return 1 ;;
    esac
    if [ -z "$root" ]; then
        repo_decl_warn "MOSAIC_HOST_ROOT is unset — refusing to resolve '$v' (spec 1.2a fail-closed; never guess a root)"
        return 1
    fi
    printf '%s/%s\n' "${root%/}" "${v#host:/}"
}

# Transition validation (4.2: a CLI flag is input, not authority).
# rc 0 = allowed; rc 1 = forbidden (message on stderr); rc 2 = no valid
# declaration (caller applies its absence policy).
#   flow=direct:       base must be the trunk (trunk == release); head must
#                      differ from it.
#   flow=trunk-release: feature->trunk allowed; trunk->release allowed (release
#                      promotion: head IS the trunk); anything else refused —
#                      feature->release explicitly REJECTED.
repo_decl_check_transition() { # $1 head, $2 base
    [ "$DECL_STATE" = valid ] || return 2
    local head="$1" base="$2"
    if [ -z "$head" ] || [ -z "$base" ]; then
        repo_decl_warn "transition check needs a head and a base (got head='$head' base='$base')"
        return 1
    fi
    if [ "$head" = "$base" ]; then
        repo_decl_warn "forbidden transition: head '$head' equals base '$base'"
        return 1
    fi
    case "$DECL_FLOW" in
        direct)
            if [ "$base" = "$DECL_TRUNK" ]; then
                return 0
            fi
            repo_decl_warn "forbidden transition (flow=direct): base must be the trunk '$DECL_TRUNK', got '$base'"
            return 1
            ;;
        trunk-release)
            if [ "$base" = "$DECL_TRUNK" ] && [ "$head" != "$DECL_TRUNK" ] && [ "$head" != "$DECL_RELEASE" ]; then
                return 0   # feature -> trunk
            fi
            if [ "$head" = "$DECL_TRUNK" ] && [ "$base" = "$DECL_RELEASE" ]; then
                return 0   # release promotion: trunk -> release
            fi
            if [ "$base" = "$DECL_RELEASE" ] && [ "$head" != "$DECL_TRUNK" ]; then
                repo_decl_warn "forbidden transition (flow=trunk-release): feature->release is REJECTED (head '$head' -> release '$DECL_RELEASE'); promote via $DECL_TRUNK"
                return 1
            fi
            repo_decl_warn "forbidden transition (flow=trunk-release): '$head' -> '$base' is not a declared transition (feature->$DECL_TRUNK or $DECL_TRUNK->$DECL_RELEASE)"
            return 1
            ;;
        *)
            repo_decl_warn "unknown declared flow '$DECL_FLOW'"
            return 1
            ;;
    esac
}
