#!/usr/bin/env bash
# deploy-postcard-design.sh — Rebuild and atomically deploy the shared public
# postcard design bundle to backend-local storage.
#
# Customer/order content is never fetched, generated, cached, or deployed.
# Database migrations are intentionally handled by deploy-backend-db-migrations.sh.
#
# Usage:
#   cd /home/ubuntu/starpost-backend
#   bash scripts/deploy-postcard-design.sh
#
# Required:
#   APP_URL=https://your-frontend-origin
#
# Optional:
#   POSTCARD_DESIGN_DIR=/path/to/backend/design/postcard
#   LOCAL_API_ORIGIN=http://127.0.0.1:3000
#   PM2_APP=starpost-api

set -Eeuo pipefail

START_TIME="$(date +%s)"
STAMP="$(date +%Y%m%d-%H%M%S)"

GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
CYAN='\033[1;36m'
BOLD='\033[1m'
NC='\033[0m'

ok()      { printf "${GREEN}   ✓ %s${NC}\n" "$1"; }
warn()    { printf "${YELLOW}   ⚠ %s${NC}\n" "$1"; }
section() { printf "\n${CYAN}${BOLD}%s${NC}\n" "$1"; }
die()     { printf "${RED}ERROR: %s${NC}\n" "$1" >&2; exit 1; }

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
ENV_FILE="$PROJECT_DIR/.env"

LOCK_FILE="/tmp/starpost-postcard-design-deploy.lock"
exec 9>"$LOCK_FILE"
flock -n 9 || die "Another postcard-design deployment is already running."

command -v node >/dev/null 2>&1 || die "Node.js is required on the backend server."
command -v curl >/dev/null 2>&1 || die "curl is required."
command -v pm2 >/dev/null 2>&1 || die "PM2 is required."
command -v flock >/dev/null 2>&1 || die "flock is required."

read_env() {
  local name="$1" value=""
  if [[ -f "$ENV_FILE" ]]; then
    value="$(sed -n "s/^${name}=//p" "$ENV_FILE" | tail -n 1)"
    value="${value%\"}"; value="${value#\"}"
    value="${value%\'}"; value="${value#\'}"
  fi
  printf '%s' "$value"
}

APP_URL="${APP_URL:-$(read_env APP_URL)}"
[[ -n "$APP_URL" ]] || die "APP_URL must be set in the environment or $ENV_FILE"
APP_URL="${APP_URL%/}"

POSTCARD_DESIGN_DIR="${POSTCARD_DESIGN_DIR:-$(read_env POSTCARD_DESIGN_DIR)}"
POSTCARD_DESIGN_DIR="${POSTCARD_DESIGN_DIR:-$PROJECT_DIR/design/postcard}"
BASE_POSTCARD_DESIGN_DIR="$POSTCARD_DESIGN_DIR"

DESIGN_VERSION="${DESIGN_VERSION:-$(read_env DESIGN_VERSION)}"
[[ -n "$DESIGN_VERSION" ]] || die "DESIGN_VERSION is required, e.g. DESIGN_VERSION=20260719-op2-template bash scripts/deploy-postcard-design.sh"
[[ "$DESIGN_VERSION" =~ ^[A-Za-z0-9._-]+$ ]] || die "DESIGN_VERSION may only contain letters, numbers, dot, underscore, and hyphen."
[[ "$DESIGN_VERSION" != "legacy-v1" ]] || die "Do not overwrite reserved legacy-v1 bundle."
[[ "$DESIGN_VERSION" != "current" ]] || die "Do not use reserved version name: current."

POSTCARD_DESIGN_DIR="$BASE_POSTCARD_DESIGN_DIR/$DESIGN_VERSION"
[[ ! -e "$POSTCARD_DESIGN_DIR" ]] || die "Design version already exists: $POSTCARD_DESIGN_DIR. Immutable bundles must not be overwritten."

LOCAL_API_ORIGIN="${LOCAL_API_ORIGIN:-$(read_env LOCAL_API_ORIGIN)}"
if [[ -z "$LOCAL_API_ORIGIN" ]]; then
  PORT="${PORT:-$(read_env PORT)}"
  LOCAL_API_ORIGIN="http://127.0.0.1:${PORT:-3000}"
fi
LOCAL_API_ORIGIN="${LOCAL_API_ORIGIN%/}"

PM2_APP="${PM2_APP:-$(read_env PM2_APP)}"
PM2_APP="${PM2_APP:-starpost-api}"

pm2 describe "$PM2_APP" >/dev/null 2>&1 \
  || die "PM2 application '$PM2_APP' does not exist."

BUILD_SCRIPT="$SCRIPT_DIR/build-postcard-design.mjs"
FONT_SCRIPT="$SCRIPT_DIR/bundle-postcard-fonts.mjs"

[[ -f "$BUILD_SCRIPT" ]] || die "Missing $BUILD_SCRIPT"
[[ -f "$FONT_SCRIPT" ]] || die "Missing $FONT_SCRIPT"

WORK_DIR="$(mktemp -d "$PROJECT_DIR/.postcard-design-build.XXXXXX")"
SOURCE_DIR="$WORK_DIR/frontend"
BUILD_DIR="$WORK_DIR/design"
PREVIOUS_DIR="${POSTCARD_DESIGN_DIR}.previous"
PREVIOUS_ACTIVE_DESIGN_VERSION="$(read_env ACTIVE_POSTCARD_DESIGN_VERSION)"
INSTALLED=0
ACTIVE_VERSION_UPDATED=0

cleanup() {
  rm -rf "$WORK_DIR"
}

rollback() {
  local status=$?

  if [[ $status -ne 0 && $ACTIVE_VERSION_UPDATED -eq 1 ]]; then
    python3 - "$ENV_FILE" "$PREVIOUS_ACTIVE_DESIGN_VERSION" <<'PYENV'
from pathlib import Path
import re
import sys

path = Path(sys.argv[1])
previous = sys.argv[2]
text = path.read_text() if path.exists() else ''
if previous:
    replacement = f'ACTIVE_POSTCARD_DESIGN_VERSION={previous}'
    text = re.sub(r'^ACTIVE_POSTCARD_DESIGN_VERSION=.*$', replacement, text, flags=re.M) if re.search(r'^ACTIVE_POSTCARD_DESIGN_VERSION=', text, flags=re.M) else text.rstrip() + '\n' + replacement + '\n'
else:
    text = re.sub(r'^ACTIVE_POSTCARD_DESIGN_VERSION=.*\n?', '', text, flags=re.M)
path.write_text(text)
PYENV
  fi

  if [[ $status -ne 0 && $INSTALLED -eq 1 ]]; then
    printf "${YELLOW}Deployment failed; removing the failed postcard bundle and restoring the previous API version...${NC}\n" >&2
    rm -rf "$POSTCARD_DESIGN_DIR"
    if [[ -d "$PREVIOUS_DIR" ]]; then
      mv "$PREVIOUS_DIR" "$POSTCARD_DESIGN_DIR"
    fi
    export POSTCARD_DESIGN_DIR="$BASE_POSTCARD_DESIGN_DIR"
    export ACTIVE_POSTCARD_DESIGN_VERSION="$PREVIOUS_ACTIVE_DESIGN_VERSION"
    pm2 restart "$PM2_APP" --update-env --time >/dev/null 2>&1 || true
  fi

  cleanup
  exit "$status"
}

trap rollback EXIT

mkdir -p "$SOURCE_DIR/css" "$SOURCE_DIR/js"

printf "${CYAN}${BOLD}"
echo "╔══════════════════════════════════════════════════════╗"
echo "║  Star Post — Postcard Design Bundle Deployment      ║"
echo "╚══════════════════════════════════════════════════════╝"
printf "${NC}"
echo
printf "  Frontend source: %s\n" "$APP_URL"
printf "  Backend API:     %s\n" "$LOCAL_API_ORIGIN"
printf "  Design base:     %s\n" "$BASE_POSTCARD_DESIGN_DIR"
printf "  Design version:  %s\n" "$DESIGN_VERSION"
printf "  Output bundle:   %s\n" "$POSTCARD_DESIGN_DIR"
printf "  PM2 app:         %s\n" "$PM2_APP"

fetch() {
  local remote="$1"
  local local_file="$2"
  local separator='?'

  [[ "$remote" == *'?'* ]] && separator='&'

  curl \
    --fail \
    --silent \
    --show-error \
    --location \
    --retry 3 \
    --retry-all-errors \
    --connect-timeout 10 \
    --max-time 60 \
    -H 'Cache-Control: no-cache' \
    -H 'Pragma: no-cache' \
    "$APP_URL/$remote${separator}deploy=${STAMP}" \
    --output "$local_file"

  [[ -s "$local_file" ]] || die "Empty frontend file: $remote"
}

section "① Fetching public frontend design sources"
fetch "index.html" "$SOURCE_DIR/index.html"
ok "index.html"
fetch "css/main.css" "$SOURCE_DIR/css/main.css"
ok "css/main.css"

fetch "decrypt.html" "$SOURCE_DIR/decrypt.html"
ok "decrypt.html"
fetch "js/stamp.js" "$SOURCE_DIR/js/stamp.js"
ok "js/stamp.js"
fetch "js/postcard-viewer.js" "$SOURCE_DIR/js/postcard-viewer.js"
ok "js/postcard-viewer.js"
fetch "js/postcard-surface.js" "$SOURCE_DIR/js/postcard-surface.js"
ok "js/postcard-surface.js"

grep -q 'id="pf-image"' "$SOURCE_DIR/index.html" \
  || die "index.html lacks #pf-image"
grep -q 'id="pc-back"' "$SOURCE_DIR/index.html" \
  || die "index.html lacks #pc-back"

node --check "$SOURCE_DIR/js/stamp.js"
node --check "$SOURCE_DIR/js/postcard-viewer.js"
node --check "$SOURCE_DIR/js/postcard-surface.js"
ok "Frontend structure and JavaScript validated"

section "② Building versioned shared design bundle"
node "$BUILD_SCRIPT" "$SOURCE_DIR" "$BUILD_DIR"
ok "Build completed"

section "③ Bundling canonical OP2 fonts"
node "$FONT_SCRIPT" "$BUILD_DIR"
ok "Font stylesheet and files bundled locally"

section "④ Validating shared OP2 design contract"

# ── Preflight: OP2 quill font options must be registered for shared viewers ──
SOURCE_DIR="$SOURCE_DIR" BUILD_DIR="$BUILD_DIR" node <<'NODE'
const fs = require('fs');

const sourceDir = process.env.SOURCE_DIR;
const buildDir = process.env.BUILD_DIR;

if (!sourceDir || !buildDir) {
  console.error('ERROR: SOURCE_DIR or BUILD_DIR missing during font preflight.');
  process.exit(1);
}

const index = fs.readFileSync(sourceDir + '/index.html', 'utf8');
const css = fs.readFileSync(buildDir + '/main.css', 'utf8');

const qcardFonts = [...index.matchAll(/data-font="([^"]+)"/g)]
  .map(m => m[1])
  .filter(Boolean);

const keyMatch = css.match(/--sp-message-font-keys:\s*([^;]+);/);
if (!keyMatch) {
  console.error('ERROR: --sp-message-font-keys missing from bundled main.css');
  process.exit(1);
}

const sharedKeys = keyMatch[1].trim().split(/\s+/);
const missing = [...new Set(qcardFonts)].filter(font => !sharedKeys.includes(font));

if (missing.length) {
  console.error('ERROR: OP2 font option(s) missing from shared viewer font keys:');
  missing.forEach(font => console.error(' - ' + font));
  console.error('Fix --sp-message-font-keys in frontend css/main.css before deploying postcard design.');
  process.exit(1);
}

console.log('OK: OP2 font options are registered in shared viewer font keys.');
NODE

node "$SCRIPT_DIR/validate-postcard-design-contract.mjs" "$SOURCE_DIR" "$BUILD_DIR" "$PROJECT_DIR"

MANIFEST_VERSION="$(
  node -p "JSON.parse(require('fs').readFileSync('$BUILD_DIR/manifest.json','utf8')).version"
)"

bundle_fingerprint() {
  node - "$1" <<'NODE'
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const root = path.resolve(process.argv[2]);
const files = [];

function walk(directory) {
  for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
    const absolute = path.join(directory, entry.name);
    if (entry.isDirectory()) walk(absolute);
    else if (entry.isFile()) files.push(absolute);
  }
}

walk(root);
files.sort((a, b) => a.localeCompare(b));

const hash = crypto.createHash('sha256');
for (const absolute of files) {
  const relative = path.relative(root, absolute).split(path.sep).join('/');
  let content = fs.readFileSync(absolute);
  if (relative === 'manifest.json') {
    const manifest = JSON.parse(content.toString('utf8'));
    delete manifest.generatedAt;
    content = Buffer.from(JSON.stringify(manifest));
  }
  hash.update(relative);
  hash.update('\0');
  hash.update(content);
  hash.update('\0');
}

process.stdout.write(hash.digest('hex'));
NODE
}

NEW_BUNDLE_FINGERPRINT="$(bundle_fingerprint "$BUILD_DIR")"
ACTIVE_DESIGN_DIR="$BASE_POSTCARD_DESIGN_DIR/$PREVIOUS_ACTIVE_DESIGN_VERSION"
if [[ -n "$PREVIOUS_ACTIVE_DESIGN_VERSION" && -d "$ACTIVE_DESIGN_DIR" ]]; then
  ACTIVE_BUNDLE_FINGERPRINT="$(bundle_fingerprint "$ACTIVE_DESIGN_DIR")"
  if [[ "$NEW_BUNDLE_FINGERPRINT" == "$ACTIVE_BUNDLE_FINGERPRINT" ]]; then
    ok "OP2 design already matches active backend bundle; deployment skipped"
    trap - EXIT
    cleanup
    exit 3
  fi
fi

section "⑤ Syncing default ink fallback from CSS"

INK="$(grep -oP '(?<=--sp-default-ink:)\s*[^;]+' "$BUILD_DIR/main.css" | tr -d ' ')"
if [[ -z "$INK" ]]; then
  warn "--sp-default-ink not found in main.css — skipping ink sync"
else
  echo "   Default ink: $INK"

  # Backend: default-ink.js
  if [[ -f "$PROJECT_DIR/src/services/default-ink.js" ]]; then
    sed -i "s/const FALLBACK = '.*';/const FALLBACK = '${INK}';/" "$PROJECT_DIR/src/services/default-ink.js"
    ok "src/services/default-ink.js"
  fi

  # Backend: postcard-back.js CSS var fallback
  if [[ -f "$PROJECT_DIR/src/pages/postcard-back.js" ]]; then
    sed -i "s/var(--sp-default-ink, #[0-9A-Fa-f]\{6\})/var(--sp-default-ink, ${INK})/" "$PROJECT_DIR/src/pages/postcard-back.js"
    ok "src/pages/postcard-back.js"
  fi

  # Frontend (if accessible from this server): config.js, op2.js
  NGINX_JS="/usr/share/nginx/html/starpost/js"
  if [[ -f "$NGINX_JS/config.js" ]]; then
    sed -i "s/var _SP_INK_FALLBACK = '.*';/var _SP_INK_FALLBACK = '${INK}';/" "$NGINX_JS/config.js"
    ok "frontend config.js"
  fi
  if [[ -f "$NGINX_JS/op2.js" ]]; then
    sed -i "s/cInk='#[0-9A-Fa-f]\{6\}'/cInk='${INK}'/" "$NGINX_JS/op2.js"
    ok "frontend op2.js"
  fi
fi

section "⑤ Validating generated bundle"
REQUIRED=(
  main.css
  fonts.css
  manifest.json
  postcard-template.html
  stamp.js
  postcard-viewer.js
  postcard-surface.js
)

for file in "${REQUIRED[@]}"; do
  [[ -s "$BUILD_DIR/$file" ]] || die "Bundle lacks $file"
  ok "$file"
done

if ! find "$BUILD_DIR/fonts" -type f -name '*.woff2' -print -quit 2>/dev/null | grep -q .; then
  die "Bundle lacks local font files"
fi
ok "Local font files"

grep -q 'AUTO-GENERATED BACKEND MIRROR' "$BUILD_DIR/main.css" \
  || die "Generated CSS warning is missing"
ok "AUTO-GENERATED warning present in main.css"

ok "Manifest version: $MANIFEST_VERSION"

section "⑥ Verifying affected backend JavaScript"
CHECKS=(
  src/server.js
  src/pages/open.js
  src/pages/open-sender.js
  src/routes/decrypt-postcard.js
  src/routes/config.js
  src/routes/open.js
  src/routes/open-sender.js
  src/routes/postcard-template.js
  src/routes/print-preview.js
  src/services/print.js
  src/services/browser-stamp.js
)

for file in "${CHECKS[@]}"; do
  [[ -f "$PROJECT_DIR/$file" ]] || die "Missing $file"
  node --check "$PROJECT_DIR/$file"
  ok "$file"
done

section "⑦ Installing bundle atomically"
mkdir -p "$(dirname "$POSTCARD_DESIGN_DIR")"
rm -rf "$PREVIOUS_DIR"

if [[ -d "$POSTCARD_DESIGN_DIR" ]]; then
  mv "$POSTCARD_DESIGN_DIR" "$PREVIOUS_DIR"
fi

mv "$BUILD_DIR" "$POSTCARD_DESIGN_DIR"
INSTALLED=1

ok "Installed at $POSTCARD_DESIGN_DIR"
if [[ -d "$PREVIOUS_DIR" ]]; then
  ok "Previous bundle retained until verification completes"
fi

section "⑧ Restarting backend"
python3 - "$ENV_FILE" "$DESIGN_VERSION" <<'PYENV'
from pathlib import Path
import re
import sys

path = Path(sys.argv[1])
version = sys.argv[2]
text = path.read_text() if path.exists() else ''
replacement = f'ACTIVE_POSTCARD_DESIGN_VERSION={version}'
text = re.sub(r'^ACTIVE_POSTCARD_DESIGN_VERSION=.*$', replacement, text, flags=re.M) if re.search(r'^ACTIVE_POSTCARD_DESIGN_VERSION=', text, flags=re.M) else text.rstrip() + '\n' + replacement + '\n'
path.write_text(text)
PYENV
ACTIVE_VERSION_UPDATED=1
export POSTCARD_DESIGN_DIR="$BASE_POSTCARD_DESIGN_DIR"
export ACTIVE_POSTCARD_DESIGN_VERSION="$DESIGN_VERSION"
pm2 restart "$PM2_APP" --update-env --time
ok "PM2 restart requested for $PM2_APP"

sleep 2

PM2_STATUS="$(
  pm2 jlist | node -e "
    const fs = require('fs');
    const apps = JSON.parse(fs.readFileSync(0, 'utf8'));
    const app = apps.find(a => a.name === process.argv[1]);
    process.stdout.write(app?.pm2_env?.status || '');
  " "$PM2_APP"
)"

if [[ "$PM2_STATUS" != "online" ]]; then
  pm2 logs "$PM2_APP" --lines 50 --nostream || true
  die "PM2 application '$PM2_APP' is not online after restart."
fi
ok "PM2 application is online"

PM2_ACTIVE_DESIGN_VERSION="$(
  pm2 jlist | node -e "
    const fs = require('fs');
    const apps = JSON.parse(fs.readFileSync(0, 'utf8'));
    const app = apps.find(a => a.name === process.argv[1]);
    process.stdout.write(
      app?.pm2_env?.ACTIVE_POSTCARD_DESIGN_VERSION || ''
    );
  " "$PM2_APP"
)"

[[ "$PM2_ACTIVE_DESIGN_VERSION" == "$DESIGN_VERSION" ]] \
  || die "PM2 active design version mismatch: expected $DESIGN_VERSION, got ${PM2_ACTIVE_DESIGN_VERSION:-missing}"

ok "Active postcard design version: $DESIGN_VERSION"

section "⑨ Verifying backend-local endpoints"
status="000"
for _attempt in {1..15}; do
  status="$(
    curl --silent --output /dev/null --write-out '%{http_code}' \
      "$LOCAL_API_ORIGIN/api/postcard-template" || true
  )"
  [[ "$status" == "200" ]] && break
  sleep 1
done

[[ "$status" == "200" ]] \
  || die "postcard-template returned $status"
ok "/api/postcard-template → 200"

health_json="$(
  curl --fail --silent --show-error \
    "$LOCAL_API_ORIGIN/api/postcard-design/health" || true
)"

[[ -n "$health_json" ]] \
  || die "/api/postcard-design/health returned no response"

health_ok="$(
  node -e "
    const fs=require('fs');
    const h=JSON.parse(fs.readFileSync(0,'utf8'));
    process.stdout.write(h.ok === true ? 'yes' : 'no');
  " <<<"$health_json"
)"

health_version="$(
  node -e "
    const fs=require('fs');
    const h=JSON.parse(fs.readFileSync(0,'utf8'));
    process.stdout.write(h.version || '');
  " <<<"$health_json"
)"

if [[ "$health_ok" != "yes" ]]; then
  echo "$health_json" | node -e "
    const fs=require('fs');
    console.error(JSON.stringify(JSON.parse(fs.readFileSync(0,'utf8')), null, 2));
  "
  die "Postcard design health check failed."
fi
ok "/api/postcard-design/health → ok ($health_version)"

for asset in main.css fonts.css manifest.json; do
  asset_status="$(
    curl --silent --output /dev/null --write-out '%{http_code}' \
      "$LOCAL_API_ORIGIN/api/postcard-design/$asset" || true
  )"

  [[ "$asset_status" == "200" ]] \
    || die "/api/postcard-design/$asset returned $asset_status"
  ok "/api/postcard-design/$asset → 200"
done

FONT_FILE="$(
  node -p "JSON.parse(require('fs').readFileSync('$POSTCARD_DESIGN_DIR/manifest.json','utf8')).fontFiles[0]"
)"

[[ -n "$FONT_FILE" ]] || die "Manifest contains no bundled font file."

font_status="$(
  curl --silent --output /dev/null --write-out '%{http_code}' \
    "$LOCAL_API_ORIGIN/api/postcard-design/fonts/$FONT_FILE" || true
)"

[[ "$font_status" == "200" ]] \
  || die "Local font endpoint returned $font_status"
ok "/api/postcard-design/fonts/$FONT_FILE → 200"

section "⑩ Running complete backend public-asset smoke tests"
bash "$SCRIPT_DIR/smoke-test-backend-public-assets.sh" "$LOCAL_API_ORIGIN"
ok "Backend-local config and all public assets verified"
bash "$SCRIPT_DIR/smoke-test-backend-public-assets.sh" "$APP_URL"
ok "Canonical public config, routing, and all public assets verified"

rm -rf "$PREVIOUS_DIR"
INSTALLED=0
trap - EXIT
cleanup

END_TIME="$(date +%s)"

echo
printf "${GREEN}${BOLD}"
echo "╔══════════════════════════════════════════════════════╗"
echo "║  ✓ Postcard design deployment complete              ║"
echo "╚══════════════════════════════════════════════════════╝"
printf "${NC}"
echo
printf "  Manifest version: %s\n" "$MANIFEST_VERSION"
printf "  Installed at:     %s\n" "$POSTCARD_DESIGN_DIR"
printf "  Duration:         %ss\n" "$((END_TIME - START_TIME))"
echo
echo "Installed files:"
ls -lah "$POSTCARD_DESIGN_DIR"

