#!/usr/bin/env bash
set -euo pipefail
export PYTHONDONTWRITEBYTECODE=1

fail() { echo "D3 local frontend build: $*" >&2; exit 1; }

usage() {
  cat <<'EOF'
Usage:
  build-d3-local-frontend-image.sh \
    --core-root PATH \
    --app-packs-root PATH \
    --l2-root PATH \
    --core-commit SHA \
    --app-packs-commit SHA \
    --l2-commit SHA \
    --l2-metadata-path PATH \
    --l2-metadata-tree-sha256 DIGEST \
    --tag-prefix PREFIX \
    --output-manifest PATH

Build the exact D3 frontend source tuple from clean local checkouts, load the
unsigned image into the current Docker daemon, and atomically write a
nexuscrm.d3-local-images/v1 manifest. Dependency installation and Buildx may
fetch their pinned build inputs, but this command never logs into a candidate
registry, publishes or pushes the candidate, or signs it. Deployment consumes
the loaded image ID without pulling the candidate image.
EOF
}

core_root=""
app_packs_root=""
l2_root=""
core_commit=""
app_packs_commit=""
l2_commit=""
l2_metadata_path=""
l2_metadata_tree_sha256=""
tag_prefix=""
output_manifest=""

while [[ $# -gt 0 ]]; do
  case "$1" in
    --core-root)
      [[ $# -ge 2 ]] || fail "--core-root requires a value"
      core_root="$2"
      shift 2
      ;;
    --app-packs-root)
      [[ $# -ge 2 ]] || fail "--app-packs-root requires a value"
      app_packs_root="$2"
      shift 2
      ;;
    --l2-root)
      [[ $# -ge 2 ]] || fail "--l2-root requires a value"
      l2_root="$2"
      shift 2
      ;;
    --core-commit)
      [[ $# -ge 2 ]] || fail "--core-commit requires a value"
      core_commit="$2"
      shift 2
      ;;
    --app-packs-commit)
      [[ $# -ge 2 ]] || fail "--app-packs-commit requires a value"
      app_packs_commit="$2"
      shift 2
      ;;
    --l2-commit)
      [[ $# -ge 2 ]] || fail "--l2-commit requires a value"
      l2_commit="$2"
      shift 2
      ;;
    --l2-metadata-path)
      [[ $# -ge 2 ]] || fail "--l2-metadata-path requires a value"
      l2_metadata_path="$2"
      shift 2
      ;;
    --l2-metadata-tree-sha256)
      [[ $# -ge 2 ]] || fail "--l2-metadata-tree-sha256 requires a value"
      l2_metadata_tree_sha256="$2"
      shift 2
      ;;
    --tag-prefix)
      [[ $# -ge 2 ]] || fail "--tag-prefix requires a value"
      tag_prefix="$2"
      shift 2
      ;;
    --output-manifest)
      [[ $# -ge 2 ]] || fail "--output-manifest requires a value"
      output_manifest="$2"
      shift 2
      ;;
    -h|--help)
      usage
      exit 0
      ;;
    *) fail "unsupported argument $1" ;;
  esac
done

[[ -n "$core_root" ]] || fail "--core-root is required"
[[ -n "$app_packs_root" ]] || fail "--app-packs-root is required"
[[ -n "$l2_root" ]] || fail "--l2-root is required"
[[ "$core_commit" =~ ^[0-9a-f]{40}$ ]] || fail "--core-commit must be one exact lowercase commit"
[[ "$app_packs_commit" =~ ^[0-9a-f]{40}$ ]] \
  || fail "--app-packs-commit must be one exact lowercase commit"
[[ "$l2_commit" =~ ^[0-9a-f]{40}$ ]] || fail "--l2-commit must be one exact lowercase commit"
[[ -n "$l2_metadata_path" && "$l2_metadata_path" != /* ]] \
  || fail "--l2-metadata-path must be one relative path"
saved_ifs="$IFS"
IFS='/' read -r -a l2_metadata_parts <<<"$l2_metadata_path"
IFS="$saved_ifs"
for part in "${l2_metadata_parts[@]}"; do
  [[ -n "$part" && "$part" != . && "$part" != .. ]] \
    || fail "--l2-metadata-path must be canonical"
done
[[ "$l2_metadata_tree_sha256" =~ ^sha256:[0-9a-f]{64}$ ]] \
  || fail "--l2-metadata-tree-sha256 must be one exact digest"
[[ -n "$tag_prefix" ]] || fail "--tag-prefix is required"
[[ -n "$output_manifest" ]] || fail "--output-manifest is required"
[[ "$tag_prefix" =~ ^[a-z0-9]+([._-][a-z0-9]+)*(/[a-z0-9]+([._-][a-z0-9]+)*)*$ ]] \
  || fail "--tag-prefix must be a canonical lowercase Docker repository prefix without a tag or digest"
[[ "$output_manifest" != */ ]] || fail "--output-manifest must name a file"

publisher_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)"
canonical_directory() {
  local path="$1" label="$2"
  [[ -d "$path" ]] || fail "$label is not a directory: $path"
  (cd -- "$path" && pwd -P)
}

core_root="$(canonical_directory "$core_root" "Core root")"
app_packs_root="$(canonical_directory "$app_packs_root" "app-packs root")"
l2_root="$(canonical_directory "$l2_root" "L2 root")"
output_manifest="$(python3 - "$output_manifest" <<'PY'
import sys
from pathlib import Path

print(Path(sys.argv[1]).expanduser().resolve(strict=False))
PY
)"
[[ ! -d "$output_manifest" ]] || fail "--output-manifest must name a file"
for checkout_root in "$core_root" "$app_packs_root" "$l2_root"; do
  case "$output_manifest" in
    "$checkout_root"|"$checkout_root"/*)
      fail "output manifest must be outside source checkouts"
      ;;
  esac
done

for command in git python3 docker jq tar cmp grep; do
  command -v "$command" >/dev/null || fail "$command is required"
done
docker buildx version >/dev/null || fail "Docker Buildx is required"

build_root="$(mktemp -d)"
build_root="$(cd -- "$build_root" && pwd -P)"
inspection_container=""
output_manifest_tmp=""
cleanup() {
  if [[ -n "$inspection_container" ]]; then
    docker rm -f "$inspection_container" >/dev/null 2>&1 || true
  fi
  rm -rf -- "$build_root"
  [[ -z "$output_manifest_tmp" ]] || rm -f -- "$output_manifest_tmp"
}
trap cleanup EXIT

context_root="$build_root/context"
source_root="$build_root/source"
materialized_root="$build_root/frontend-app-packs-source"
inspection_root="$build_root/inspection"
mkdir -p -- "$context_root" "$source_root" "$inspection_root"

PYTHONPATH="$publisher_root/scripts" python3 - \
  "$context_root/source-tuple.json" "$core_commit" "$app_packs_commit" "$l2_commit" <<'PY'
import sys
from pathlib import Path

from validate_serviceification_bom import canonical_json_bytes

Path(sys.argv[1]).write_bytes(canonical_json_bytes({
    "nexuscrm_repository": "pingcap-inc/NexusCRM",
    "nexuscrm_commit": sys.argv[2],
    "app_packs_repository": "pingcap-inc/nexuscrm-app-packs",
    "app_packs_commit": sys.argv[3],
    "l2_repository": "pingcap-inc/nexuscrm-pingcap-pack",
    "l2_commit": sys.argv[4],
}))
PY

verify_checkout() {
  local root="$1" expected="$2" label="$3" tracked_tree="${4-}" checkout_state
  [[ "$(git -C "$root" rev-parse --is-inside-work-tree 2>/dev/null)" == true ]] \
    || fail "$label is not a Git checkout"
  [[ "$(git -C "$root" rev-parse HEAD)" == "$expected" ]] \
    || fail "$label HEAD disagrees with the frontend source tuple"
  checkout_state="$(
    git -C "$root" status \
      --porcelain=v1 \
      --untracked-files=all \
      --ignored=matching
  )" || fail "$label checkout state could not be read"
  [[ -z "$checkout_state" ]] \
    || fail "$label checkout must contain only tracked HEAD inputs"
  [[ -z "$tracked_tree" ]] || python3 - "$root" "$expected" "$label" "$tracked_tree" <<'PY' \
    || fail "$label input tree contains a tracked symlink, submodule, or special entry"
import subprocess
import sys

root, expected, label, tracked_tree = sys.argv[1:]
object_type = subprocess.run(
    ["git", "-C", root, "cat-file", "-t", f"{expected}:{tracked_tree}"],
    check=True,
    capture_output=True,
).stdout.strip()
if object_type != b"tree":
    print(f"{label} selected input path is not a committed Git tree", file=sys.stderr)
    raise SystemExit(1)
tree = subprocess.run(
    [
        "git", "-C", root, "ls-tree", "-rz", "--full-tree", expected,
        "--", tracked_tree,
    ],
    check=True,
    capture_output=True,
).stdout
if not tree:
    print(f"{label} selected input tree has no committed entries", file=sys.stderr)
    raise SystemExit(1)
for entry in tree.split(b"\0"):
    if not entry:
        continue
    metadata, separator, path = entry.partition(b"\t")
    fields = metadata.split()
    if not separator or len(fields) != 3 or fields[0] not in (b"100644", b"100755"):
        rendered = path.decode("utf-8", "backslashreplace") or "<malformed>"
        print(f"{label} unsafe tracked entry: {rendered}", file=sys.stderr)
        raise SystemExit(1)
PY
}

verify_checkouts() {
  verify_checkout "$core_root" "$core_commit" "Core" frontend
  verify_checkout "$app_packs_root" "$app_packs_commit" "app-packs"
  verify_checkout "$l2_root" "$l2_commit" "L2" "$l2_metadata_path"
}

require_regular_tree() {
  local root="$1" label="$2"
  python3 - "$root" "$label" <<'PY' || fail "$label contains a symlink or special entry"
import os
import stat
import sys
from pathlib import Path

root = Path(sys.argv[1]).absolute()
label = sys.argv[2]
try:
    resolved = root.resolve(strict=True)
    if resolved != root or not stat.S_ISDIR(root.lstat().st_mode):
        raise ValueError("root is not one canonical directory")
    for directory, directory_names, file_names in os.walk(root, followlinks=False):
        parent = Path(directory)
        for name in directory_names:
            path = parent / name
            if not stat.S_ISDIR(path.lstat().st_mode):
                raise ValueError(f"unsafe directory entry: {path.relative_to(root)}")
        for name in file_names:
            path = parent / name
            if not stat.S_ISREG(path.lstat().st_mode):
                raise ValueError(f"unsafe file entry: {path.relative_to(root)}")
except (OSError, ValueError) as exc:
    print(f"{label}: {exc}", file=sys.stderr)
    raise SystemExit(1)
PY
}

verify_checkouts

l2_metadata_root="$(python3 - "$l2_root" "$l2_metadata_path" <<'PY'
import sys
from pathlib import Path

root = Path(sys.argv[1]).resolve()
target = (root / sys.argv[2]).resolve(strict=True)
target.relative_to(root)
if not target.is_dir():
    raise SystemExit("L2 metadata path is not a directory")
print(target)
PY
)" || fail "L2 metadata path escapes or is not a directory"
actual_l2_tree="$(PYTHONPATH="$publisher_root/scripts" python3 - "$l2_metadata_root" <<'PY'
import sys
from pathlib import Path

from lib.final_pack_contract import metadata_tree_sha256

print(metadata_tree_sha256(Path(sys.argv[1])))
PY
)"
[[ "$actual_l2_tree" == "$l2_metadata_tree_sha256" ]] \
  || fail "L2 metadata tree disagrees with the selected branch candidate"

materialize_result="$(python3 "$publisher_root/scripts/materialize_frontend_app_packs_source.py" \
  --downstream-root "$l2_root" \
  --official-app-packs-root "$app_packs_root" \
  --target-root "$materialized_root")"
materialized_ref="$(jq -er '.ref' <<<"$materialize_result")"
[[ "$(jq -er '.root' <<<"$materialize_result")" == "$materialized_root" ]] \
  || fail "frontend source materializer returned another root"
git -C "$materialized_root" cat-file -e "$materialized_ref^{commit}"

git -C "$core_root" archive --format=tar "$core_commit" frontend \
  | tar -xf - -C "$source_root"
frontend_root="$source_root/frontend"
frontend_build_context="$build_root/frontend-build-context"
frontend_build_output="$build_root/frontend-build-output"
mkdir -p -- "$frontend_build_context"
mv -- "$frontend_root" "$frontend_build_context/frontend"
mv -- "$materialized_root" "$frontend_build_context/frontend-app-packs-source"
materialized_root="$frontend_build_context/frontend-app-packs-source"

echo "[local-frontend] building branch JavaScript in an isolated BuildKit stage"
docker buildx build \
  --platform linux/amd64 \
  --provenance=false \
  --target frontend-dist \
  --output "type=local,dest=$frontend_build_output" \
  --build-arg "APP_PACKS_REF=$materialized_ref" \
  -f "$publisher_root/infra/serviceification-frontend-branch-build.Dockerfile" \
  "$frontend_build_context" >/dev/null

require_regular_tree "$frontend_build_output" "frontend BuildKit export"
[[ -f "$frontend_build_output/index.html" && -f "$frontend_build_output/version.json" ]] \
  || fail "frontend build is incomplete"
grep_status=0
grep -aERn '127\.0\.0\.1:(3001|5173|3198)|localhost:(3001|5173|3198)' \
  "$frontend_build_output" || grep_status=$?
case "$grep_status" in
  0) fail "frontend bundle contains an environment build host" ;;
  1) ;;
  *) fail "frontend bundle host scan failed with status $grep_status" ;;
esac
verify_checkouts

mkdir -p -- "$context_root/frontend-dist"
cp -RP "$frontend_build_output/." "$context_root/frontend-dist/"
source_tuple_binding="sha256:$(python3 - "$context_root/source-tuple.json" <<'PY'
import hashlib
import sys
from pathlib import Path

print(hashlib.sha256(Path(sys.argv[1]).read_bytes()).hexdigest())
PY
)"
build_date="$(git -C "$l2_root" show -s --format=%cI "$l2_commit")"
PYTHONPATH="$publisher_root/scripts" python3 - \
  "$context_root/frontend-dist/version.json" "$source_tuple_binding" "$build_date" <<'PY'
import sys
from pathlib import Path

from validate_serviceification_bom import canonical_json_bytes

Path(sys.argv[1]).write_bytes(canonical_json_bytes({
    "version": sys.argv[2],
    "buildDate": sys.argv[3],
}))
PY
# Keep generated file timestamps fresh: another Core/AppPacks commit can change
# same-sized binding files while L2 remains unchanged. Normalizing their mtimes
# to the L2 commit let BuildKit reuse stale context bytes on this local path.

image_ref="$tag_prefix/frontend:dev-$l2_commit"
echo "[local-frontend] loading unsigned frontend image for $l2_commit"
# Only the cheap final COPY packaging is uncached; the JavaScript build above
# retains its dependency/build cache. Embedded byte verification remains required.
docker buildx build \
  --platform linux/amd64 \
  --provenance=false \
  --no-cache \
  --load \
  --build-arg "SOURCE_SHA=$l2_commit" \
  --build-arg "SOURCE_TUPLE_SHA256=$source_tuple_binding" \
  --label "dev.nexuscrm.frontend.role=frontend" \
  --label "dev.nexuscrm.release.class=test-branch-candidate" \
  --tag "$image_ref" \
  -f "$publisher_root/infra/serviceification-frontend.Dockerfile" \
  "$context_root" >/dev/null

image_field() {
  local image="$1" field="$2"
  docker image inspect "$image" | jq -er ".[0]$field"
}

image_id="$(image_field "$image_ref" '.Id')"
[[ "$image_id" =~ ^sha256:[0-9a-f]{64}$ ]] \
  || fail "frontend build returned no canonical local image ID"

verify_local_image() {
  local resolved_id revision binding role release_class image_os image_architecture
  resolved_id="$(image_field "$image_ref" '.Id')"
  [[ "$resolved_id" == "$image_id" ]] \
    || fail "frontend image reference drifted from $image_id to $resolved_id"
  jq -e --arg ref "$image_ref" '.[0].RepoTags | index($ref) != null' \
    < <(docker image inspect "$image_id") >/dev/null \
    || fail "frontend image ID no longer owns the selected local reference"
  revision="$(image_field "$image_id" '.Config.Labels["org.opencontainers.image.revision"]')"
  binding="$(image_field "$image_id" '.Config.Labels["io.nexuscrm.frontend.source-tuple-sha256"]')"
  role="$(image_field "$image_id" '.Config.Labels["dev.nexuscrm.frontend.role"]')"
  release_class="$(image_field "$image_id" '.Config.Labels["dev.nexuscrm.release.class"]')"
  [[ "$revision" == "$l2_commit" ]] || fail "frontend image revision disagrees with L2 owner"
  [[ "$binding" == "$source_tuple_binding" ]] || fail "frontend image tuple label disagrees with its source"
  [[ "$role" == frontend ]] || fail "frontend image has the wrong role label"
  [[ "$release_class" == test-branch-candidate ]] \
    || fail "frontend image is not marked as a branch Test candidate"
  image_os="$(image_field "$image_id" '.Os')"
  image_architecture="$(image_field "$image_id" '.Architecture')"
  [[ "$image_os/$image_architecture" == linux/amd64 ]] \
    || fail "frontend image platform is $image_os/$image_architecture, expected linux/amd64"

  rm -rf -- "$inspection_root"
  mkdir -p -- "$inspection_root"
  inspection_container="$(docker create "$image_id" /__nexuscrm_frontend_inspection__)"
  docker cp "$inspection_container:/usr/share/nexuscrm/release/source-tuple.json" \
    "$inspection_root/source-tuple.json"
  docker cp "$inspection_container:/usr/share/nexuscrm/frontend/version.json" \
    "$inspection_root/version.json"
  docker rm "$inspection_container" >/dev/null
  inspection_container=""
  cmp -s "$context_root/source-tuple.json" "$inspection_root/source-tuple.json" \
    || fail "frontend image embeds another source tuple"
  cmp -s "$context_root/frontend-dist/version.json" "$inspection_root/version.json" \
    || fail "frontend image embeds another version binding"
}

verify_checkouts
verify_local_image

output_dir="$(dirname -- "$output_manifest")"
output_name="$(basename -- "$output_manifest")"
mkdir -p -- "$output_dir"
output_dir="$(cd -- "$output_dir" && pwd -P)"
output_manifest="$output_dir/$output_name"

verify_checkouts
verify_local_image
output_manifest_tmp="$(mktemp "$output_dir/.$output_name.XXXXXX")"
jq -nS \
  --arg source_commit "$l2_commit" \
  --arg image_ref "$image_ref" \
  --arg image_id "$image_id" \
  --arg source_tuple_sha256 "$source_tuple_binding" \
  '{
    schema:"nexuscrm.d3-local-images/v1",
    release_class:"test-only-unsigned-non-promotable",
    source_commit:$source_commit,
    images:[{
      role:"frontend",
      image_ref:$image_ref,
      image_id:$image_id,
      source_commit:$source_commit,
      source_tuple_sha256:$source_tuple_sha256
    }]
  }' > "$output_manifest_tmp"
if ! python3 - "$output_manifest_tmp" "$output_manifest" <<'PY'
import os
import sys

os.replace(sys.argv[1], sys.argv[2])
PY
then
  fail "could not atomically write output manifest"
fi
output_manifest_tmp=""
echo "[local-frontend] wrote local-only manifest $output_manifest"
