#!/usr/bin/env bash
set -euo pipefail

# Build a Windows release, create the in-app update package, and deploy the
# exact package/feed that were just built.
CONFIGURATION="${CONFIGURATION:-Release}"
RUNTIME="${RUNTIME:-win-x64}"
BASE_URL="${BASE_URL:-https://craftplusplus.216.128.145.79.nip.io/updateddata}"
PUBLIC_VERIFY="${PUBLIC_VERIFY:-auto}"
RUN_TESTS="${RUN_TESTS:-true}"
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SOURCE_DIR="$REPO_ROOT/codelanguage"
ARTIFACTS="$SOURCE_DIR/artifacts"
PROPS_FILE="$SOURCE_DIR/Directory.Build.props"
PUBLISH_DIR="$ARTIFACTS/publish/$RUNTIME"
UPDATEDATA_DIR="$ARTIFACTS/updateddata"
STAGING_UPDATEDATA_DIR="$ARTIFACTS/staging/updateddata"
INSTALLER_DIR="$ARTIFACTS/installer"
LEGACY_UPDATE_DIR="${LEGACY_UPDATE_DIR:-}"

# The official HTTPS feed is backed by this VPS directory. Use it automatically
# for both local builds on the VPS and SSH uploads from a workstation.
if [[ -n "${UPDATE_SERVER_SSH:-}" && -z "${UPDATE_SERVER_DIR:-}" ]]; then
  UPDATE_SERVER_DIR="$UPDATEDATA_DIR"
fi

section() { printf '\n=== %s ===\n' "$1"; }
fail() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }

[[ "$RUNTIME" == "win-x64" || "$RUNTIME" == "win-arm64" ]] || fail "Unsupported RUNTIME '$RUNTIME'."
command -v dotnet >/dev/null || fail ".NET SDK is required."
command -v zip >/dev/null || fail "zip is required."
command -v sha256sum >/dev/null || fail "sha256sum is required."
command -v python3 >/dev/null || fail "python3 is required."
command -v curl >/dev/null || fail "curl is required to verify the deployed update feed."
[[ "$PUBLIC_VERIFY" == "auto" || "$PUBLIC_VERIFY" == "true" || "$PUBLIC_VERIFY" == "false" ]] \
  || fail "PUBLIC_VERIFY must be auto, true, or false."
python3 - "$BASE_URL" <<'PY' || fail "BASE_URL must be the official HTTPS /updateddata endpoint."
import sys
from urllib.parse import urlparse
url = urlparse(sys.argv[1])
assert url.scheme == "https"
assert url.hostname in {"craftplusplus.216.128.145.79.nip.io", "craftplusplus.216-128-145-79.nip.io", "216-128-145-79.nip.io", "216.128.145.79"}
assert url.path.rstrip("/") == "/updateddata"
PY

if [[ -n "${UPDATE_SERVER_SSH:-}" ]]; then
  [[ -n "${UPDATE_SERVER_DIR:-}" ]] || fail "Set UPDATE_SERVER_DIR to the remote Hub updateddata directory."
  [[ "$UPDATE_SERVER_SSH" =~ ^[A-Za-z0-9._@-]+$ ]] || fail "UPDATE_SERVER_SSH must be a user@host SSH destination."
  [[ "$UPDATE_SERVER_DIR" =~ ^/[A-Za-z0-9._/-]+$ ]] || fail "UPDATE_SERVER_DIR must be an absolute remote path without shell metacharacters."
  command -v ssh >/dev/null || fail "ssh is required for remote deployment."
  command -v scp >/dev/null || fail "scp is required for remote deployment."
  ssh -o BatchMode=yes "$UPDATE_SERVER_SSH" true || fail "Could not connect to UPDATE_SERVER_SSH using configured SSH authentication."
elif [[ -n "${UPDATE_SERVER_DIR:-}" ]]; then
  [[ -d "$UPDATE_SERVER_DIR" ]] || fail "UPDATE_SERVER_DIR does not exist: $UPDATE_SERVER_DIR"
else
  mkdir -p "$UPDATEDATA_DIR"
  UPDATE_SERVER_DIR="$UPDATEDATA_DIR"
  # Keep the old HTTP feed in sync for installations still configured to use it.
  if [[ -z "$LEGACY_UPDATE_DIR" && -d /root/Desktop/updateddata && \
        "$UPDATE_SERVER_DIR" != /root/Desktop/updateddata ]]; then
    LEGACY_UPDATE_DIR=/root/Desktop/updateddata
  fi
fi

if [[ -z "${UPDATE_SERVER_SSH:-}" && -z "$LEGACY_UPDATE_DIR" ]]; then
  if [[ "$UPDATE_SERVER_DIR" == "$UPDATEDATA_DIR" && \
        -d /root/Desktop/updateddata ]]; then
    LEGACY_UPDATE_DIR=/root/Desktop/updateddata
  elif [[ "$UPDATE_SERVER_DIR" == "/root/Desktop/updateddata" && \
          -d "$UPDATEDATA_DIR" ]]; then
    LEGACY_UPDATE_DIR="$UPDATEDATA_DIR"
  fi
fi
if [[ -n "$LEGACY_UPDATE_DIR" ]]; then
  [[ -d "$LEGACY_UPDATE_DIR" ]] || fail "LEGACY_UPDATE_DIR does not exist: $LEGACY_UPDATE_DIR"
fi

# Resolve the next release version without mutating source-controlled version
# files. A failed build therefore does not consume a version number.
CURRENT_VERSION="$(sed -n 's/.*<CraftPlusPlusVersion[^>]*>\([0-9][^<]*\)<\/CraftPlusPlusVersion>.*/\1/p' "$PROPS_FILE" | head -n1)"
[[ -n "$CURRENT_VERSION" ]] || fail "Could not read CraftPlusPlusVersion from $PROPS_FILE."
IFS=. read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION"
[[ "$MAJOR" =~ ^[0-9]+$ && "$MINOR" =~ ^[0-9]+$ && "$PATCH" =~ ^[0-9]+$ ]] || fail "Version must be MAJOR.MINOR.PATCH; found '$CURRENT_VERSION'."
PATCH=$((PATCH + 1))
if (( PATCH >= 100 )); then PATCH=0; MINOR=$((MINOR + 1)); fi
if (( MINOR >= 100 )); then MINOR=0; MAJOR=$((MAJOR + 1)); fi
VERSION="$MAJOR.$MINOR.$PATCH"
section "Building Craft++ $VERSION ($RUNTIME)"

PROJECTS=(
  "$SOURCE_DIR/codelanguage.csproj"
  "$SOURCE_DIR/UpdateInstaller/CraftUpdateInstaller.csproj"
)
for project in "${PROJECTS[@]}"; do
  dotnet restore "$project" -r "$RUNTIME" -m:1 -nodeReuse:false -p:EnableWindowsTargeting=true
done

if [[ "$RUN_TESTS" == "true" ]]; then
  dotnet test "$SOURCE_DIR/CraftPlusPlus.Tests/CraftPlusPlus.Tests.csproj" -c "$CONFIGURATION" -m:1 -nodeReuse:false -p:CraftPlusPlusVersion="$VERSION" -p:EnableWindowsTargeting=true
  dotnet test "$SOURCE_DIR/CraftPlusPlus.Tests/CompilerValidation/CompilerValidation.csproj" -c "$CONFIGURATION" -m:1 -nodeReuse:false -p:CraftPlusPlusVersion="$VERSION" -p:EnableWindowsTargeting=true
  dotnet test "$SOURCE_DIR/CraftPlusPlus.Tests/RuntimeValidation/RuntimeValidation.csproj" -c "$CONFIGURATION" -m:1 -nodeReuse:false -p:CraftPlusPlusVersion="$VERSION" -p:EnableWindowsTargeting=true
fi

rm -rf "$PUBLISH_DIR" "$STAGING_UPDATEDATA_DIR" "$INSTALLER_DIR"
mkdir -p "$PUBLISH_DIR" "$STAGING_UPDATEDATA_DIR" "$INSTALLER_DIR"

dotnet publish "$SOURCE_DIR/codelanguage.csproj" \
  -c "$CONFIGURATION" -f net8.0-windows -r "$RUNTIME" --self-contained true \
  -m:1 -nodeReuse:false \
  -o "$PUBLISH_DIR" -p:CraftPlusPlusVersion="$VERSION" -p:Version="$VERSION" -p:EnableWindowsTargeting=true \
  -p:ContinuousIntegrationBuild=true -p:PublishSingleFile=false

dotnet publish "$SOURCE_DIR/UpdateInstaller/CraftUpdateInstaller.csproj" \
  -c "$CONFIGURATION" -r "$RUNTIME" --self-contained true \
  -m:1 -nodeReuse:false \
  -o "$PUBLISH_DIR/UpdateInstaller" -p:CraftPlusPlusVersion="$VERSION" -p:Version="$VERSION" \
  -p:EnableWindowsTargeting=true -p:ContinuousIntegrationBuild=true

APP_EXE="$PUBLISH_DIR/codelanguage.exe"
UPDATER_EXE="$PUBLISH_DIR/UpdateInstaller/CraftUpdateInstaller.exe"
[[ -f "$APP_EXE" ]] || fail "Application publish did not produce $APP_EXE."
[[ -f "$UPDATER_EXE" ]] || fail "Updater publish did not produce $UPDATER_EXE."

# Build the matching setup installer when running on Windows with Inno Setup.
if command -v powershell.exe >/dev/null 2>&1 || command -v pwsh >/dev/null 2>&1; then
  PS="$(command -v pwsh || command -v powershell.exe)"
  "$PS" -NoProfile -File "$REPO_ROOT/installer/build-installer.ps1" \
    -Configuration "$CONFIGURATION" -Runtime "$RUNTIME" -ArtifactRoot "$ARTIFACTS"
fi

ARCHIVE_NAME="CraftPlusPlus-$VERSION-$RUNTIME.zip"
ARCHIVE_PATH="$STAGING_UPDATEDATA_DIR/$ARCHIVE_NAME"
(cd "$PUBLISH_DIR" && zip -qr "$ARCHIVE_PATH" .)
SHA256="$(sha256sum "$ARCHIVE_PATH" | awk '{print $1}')"

cat > "$STAGING_UPDATEDATA_DIR/version.json" <<EOF
{
  "application": "Craft++",
  "version": "$VERSION",
  "minimumSupportedVersion": "0.1.0",
  "title": "Craft++ update available",
  "message": "A newer Craft++ version is available.",
  "platforms": ["win"],
  "runtime": "$RUNTIME",
  "downloadUrl": "$BASE_URL/$ARCHIVE_NAME",
  "updateArchiveUrl": "$BASE_URL/$ARCHIVE_NAME",
  "updateArchiveSha256": "$SHA256",
  "releaseNotesUrl": "$BASE_URL/$VERSION-notes.html",
  "publishedUtc": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
EOF

NOTES_NAME="$VERSION-notes.html"
NOTES_PATH="$STAGING_UPDATEDATA_DIR/$NOTES_NAME"
cat > "$NOTES_PATH" <<EOF
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><title>Craft++ $VERSION release notes</title>
<style>body{font-family:system-ui,sans-serif;max-width:760px;margin:2rem auto;padding:0 1rem;line-height:1.5}code{background:#f1f5f9;padding:2px 4px;border-radius:4px}</style></head>
<body><h1>Craft++ $VERSION</h1>
<p>Released $(date -u +%Y-%m-%d).</p>
<p>Update feed: <a href="$BASE_URL/version.json">$BASE_URL/version.json</a></p>
<p>Archive: <code>$ARCHIVE_NAME</code><br>SHA-256: <code>$SHA256</code></p>
<p>This release includes the current Hub URL and synchronization fixes.</p>
</body></html>
EOF
[[ -f "$NOTES_PATH" ]] || fail "Release notes were not created."

# Validate the generated pair before copying anywhere.
python3 - "$STAGING_UPDATEDATA_DIR/version.json" "$ARCHIVE_PATH" "$SHA256" "$VERSION" <<'PY'
import hashlib, json, sys, zipfile
feed_path, archive_path, expected_hash, expected_version = sys.argv[1:]
feed = json.load(open(feed_path, encoding="utf-8"))
assert feed["version"] == expected_version, "feed version mismatch"
assert feed["updateArchiveSha256"] == expected_hash, "feed hash mismatch"
assert hashlib.sha256(open(archive_path, "rb").read()).hexdigest() == expected_hash, "archive hash mismatch"
with zipfile.ZipFile(archive_path) as archive:
    names = {name.lower() for name in archive.namelist()}
    assert "codelanguage.exe" in names, "archive missing codelanguage.exe"
    assert "updateinstaller/craftupdateinstaller.exe" in names, "archive missing update installer"
print("Update feed and archive validated.")
PY

# Setup installer is optional on non-Windows builders; when present, make sure
# it is the one produced from this same versioned payload.
SETUP="$INSTALLER_DIR/CraftPlusPlus-Setup-$VERSION.exe"
[[ ! -f "$SETUP" ]] || cp "$SETUP" "$STAGING_UPDATEDATA_DIR/"

# Never replace a newer release with an older artifact, even if source version
# metadata has fallen behind the live update feed.
if [[ -n "${UPDATE_SERVER_SSH:-}" ]]; then
  CURRENT_FEED_JSON="$(ssh "$UPDATE_SERVER_SSH" "if [ -f '$UPDATE_SERVER_DIR/version.json' ]; then cat '$UPDATE_SERVER_DIR/version.json'; fi")"
elif [[ -f "$UPDATE_SERVER_DIR/version.json" ]]; then
  CURRENT_FEED_JSON="$(cat "$UPDATE_SERVER_DIR/version.json")"
else
  CURRENT_FEED_JSON=""
fi
if [[ -n "$CURRENT_FEED_JSON" ]]; then
  python3 - "$CURRENT_FEED_JSON" "$VERSION" <<'PY' || fail "Refusing to upload a release that is not newer than the current update feed."
import json, re, sys
current = json.loads(sys.argv[1]).get("version", "")
candidate = sys.argv[2]
pattern = re.compile(r"^(\d+)\.(\d+)\.(\d+)$")
def key(value):
    match = pattern.fullmatch(str(value))
    if not match:
        raise SystemExit(f"Invalid version in update feed: {value!r}")
    return tuple(map(int, match.groups()))
if key(candidate) <= key(current):
    print(f"Current feed version is {current}; candidate version is {candidate}.", file=sys.stderr)
    raise SystemExit(1)
PY
fi

# Deploy the archive first and publish version.json last. Readers can never see
# metadata that points to an archive that has not finished uploading.
if [[ -n "${UPDATE_SERVER_SSH:-}" ]]; then
  [[ -n "${UPDATE_SERVER_DIR:-}" ]] || fail "Set UPDATE_SERVER_DIR to the remote Hub updateddata directory."
  [[ "$UPDATE_SERVER_SSH" =~ ^[A-Za-z0-9._@-]+$ ]] || fail "UPDATE_SERVER_SSH must be a user@host SSH destination."
  [[ "$UPDATE_SERVER_DIR" =~ ^/[A-Za-z0-9._/-]+$ ]] || fail "UPDATE_SERVER_DIR must be an absolute remote path without shell metacharacters."
  command -v ssh >/dev/null || fail "ssh is required for remote deployment."
  command -v scp >/dev/null || fail "scp is required for remote deployment."
  ssh "$UPDATE_SERVER_SSH" "mkdir -p '$UPDATE_SERVER_DIR'"
  scp "$ARCHIVE_PATH" "$UPDATE_SERVER_SSH:$UPDATE_SERVER_DIR/$ARCHIVE_NAME.uploading"
  scp "$NOTES_PATH" "$UPDATE_SERVER_SSH:$UPDATE_SERVER_DIR/$NOTES_NAME.uploading"
  scp "$STAGING_UPDATEDATA_DIR/version.json" "$UPDATE_SERVER_SSH:$UPDATE_SERVER_DIR/version.json.uploading"
  ssh "$UPDATE_SERVER_SSH" "mv '$UPDATE_SERVER_DIR/$ARCHIVE_NAME.uploading' '$UPDATE_SERVER_DIR/$ARCHIVE_NAME' && mv '$UPDATE_SERVER_DIR/$NOTES_NAME.uploading' '$UPDATE_SERVER_DIR/$NOTES_NAME' && mv '$UPDATE_SERVER_DIR/version.json.uploading' '$UPDATE_SERVER_DIR/version.json'"
elif [[ -n "${UPDATE_SERVER_DIR:-}" ]]; then
  DESTINATIONS=("$UPDATE_SERVER_DIR")
  [[ -z "$LEGACY_UPDATE_DIR" || "$LEGACY_UPDATE_DIR" == "$UPDATE_SERVER_DIR" ]] || DESTINATIONS+=("$LEGACY_UPDATE_DIR")
  for destination in "${DESTINATIONS[@]}"; do
    [[ -d "$destination" ]] || fail "Update destination does not exist: $destination"
    cp "$ARCHIVE_PATH" "$destination/$ARCHIVE_NAME.uploading"
    cp "$NOTES_PATH" "$destination/$NOTES_NAME.uploading"
    cp "$STAGING_UPDATEDATA_DIR/version.json" "$destination/version.json.uploading"
  done
  for destination in "${DESTINATIONS[@]}"; do
    mv -f "$destination/$ARCHIVE_NAME.uploading" "$destination/$ARCHIVE_NAME"
    mv -f "$destination/$NOTES_NAME.uploading" "$destination/$NOTES_NAME"
  done
  for destination in "${DESTINATIONS[@]}"; do
    mv -f "$destination/version.json.uploading" "$destination/version.json"
  done
else
  fail "Build completed, but nothing was uploaded. Set UPDATE_SERVER_SSH and UPDATE_SERVER_DIR (or UPDATE_SERVER_DIR for a local Hub directory)."
fi

VERIFY_PUBLIC=false
if [[ "$PUBLIC_VERIFY" == "true" ]]; then
  VERIFY_PUBLIC=true
elif [[ "$PUBLIC_VERIFY" == "auto" ]] && [[ -n "${UPDATE_SERVER_SSH:-}" ]]; then
  VERIFY_PUBLIC=true
elif [[ "$PUBLIC_VERIFY" == "auto" ]]; then
  # A local copy can be verified without network access, but probe the actual
  # advertised endpoint whenever it is reachable. If it serves stale metadata,
  # treat that as a deployment failure instead of claiming success.
  PUBLIC_PROBE="$(mktemp)"
  if curl --fail --silent --show-error --location --max-time 10 \
      -H 'Cache-Control: no-cache, no-store' "$BASE_URL/version.json" -o "$PUBLIC_PROBE" 2>/dev/null; then
    VERIFY_PUBLIC=true
  else
    printf 'Public feed is unreachable; verifying local deployment only. Set PUBLIC_VERIFY=true to make this fatal.\n'
  fi
  rm -f "$PUBLIC_PROBE"
fi

if [[ "$VERIFY_PUBLIC" == "true" ]]; then
  DEPLOYED_FEED="$(mktemp)"
  trap 'rm -f "$DEPLOYED_FEED"' EXIT
  curl --fail --silent --show-error --location --max-time 20 \
    -H 'Cache-Control: no-cache, no-store' "$BASE_URL/version.json" -o "$DEPLOYED_FEED" \
    || fail "Uploaded files, but could not read the public update feed at $BASE_URL/version.json."
  DEPLOYED_VERSION="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["version"])' "$DEPLOYED_FEED")"
  DEPLOYED_SHA256="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["updateArchiveSha256"])' "$DEPLOYED_FEED")"
  [[ "$DEPLOYED_VERSION" == "$VERSION" ]] || fail "Public update feed reports version '$DEPLOYED_VERSION', expected '$VERSION'."
  [[ "$DEPLOYED_SHA256" == "$SHA256" ]] || fail "Public feed archive digest does not match the release artifact."
  curl --fail --silent --show-error --location --head --max-time 20 "$BASE_URL/$ARCHIVE_NAME" >/dev/null \
    || fail "Public feed is current, but update archive is not downloadable at $BASE_URL/$ARCHIVE_NAME."
  curl --fail --silent --show-error --location --head --max-time 20 "$BASE_URL/$NOTES_NAME" >/dev/null \
    || fail "Public feed is current, but release notes are missing at $BASE_URL/$NOTES_NAME."
  PUBLIC_ARCHIVE="$(mktemp)"
  trap 'rm -f "$DEPLOYED_FEED" "$PUBLIC_ARCHIVE"' EXIT
  curl --fail --silent --show-error --location --max-time 600 "$BASE_URL/$ARCHIVE_NAME" -o "$PUBLIC_ARCHIVE" \
    || fail "Could not download the published update archive for integrity verification."
  PUBLIC_SHA256="$(sha256sum "$PUBLIC_ARCHIVE" | awk '{print $1}')"
  [[ "$PUBLIC_SHA256" == "$SHA256" ]] || fail "Published archive SHA-256 mismatch: got $PUBLIC_SHA256, expected $SHA256."
  printf 'Public update feed and archive are reachable at %s (version %s).\n' "$BASE_URL" "$VERSION"
else
  for destination in "${DESTINATIONS[@]:-}"; do
    [[ -n "$destination" ]] || continue
    DEPLOYED_VERSION="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["version"])' "$destination/version.json")"
    DEPLOYED_SHA256="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["updateArchiveSha256"])' "$destination/version.json")"
    [[ "$DEPLOYED_VERSION" == "$VERSION" ]] || fail "Local feed at $destination reports version '$DEPLOYED_VERSION', expected '$VERSION'."
    [[ "$DEPLOYED_SHA256" == "$SHA256" ]] || fail "Local feed at $destination has the wrong archive digest."
    [[ "$(sha256sum "$destination/$ARCHIVE_NAME" | awk '{print $1}')" == "$SHA256" ]] \
      || fail "Local deployed archive at $destination failed SHA-256 verification."
    [[ -f "$destination/$NOTES_NAME" ]] || fail "Local release notes are missing at $destination/$NOTES_NAME."
  done
  if [[ -n "${UPDATE_SERVER_SSH:-}" ]]; then
    fail "Set PUBLIC_VERIFY=true to verify the remote public feed, or use local deployment for offline verification."
  fi
  printf 'Local update feed and archive verified. Set PUBLIC_VERIFY=true to also check the public HTTPS endpoint.\n'
fi

# Consume the version only after the public feed and archive both verify.
printf '%s\n' "$VERSION" > "$SOURCE_DIR/version.txt"
sed -i -E "s/(<CraftPlusPlusVersion[^>]*>)[^<]+(<\/CraftPlusPlusVersion>)/\\1$VERSION\\2/" "$PROPS_FILE"

printf '\nRelease %s is ready.\nArchive: %s\nFeed: %s\nSHA-256: %s\n' \
  "$VERSION" "$UPDATE_SERVER_DIR/$ARCHIVE_NAME" "$UPDATE_SERVER_DIR/version.json" "$SHA256"
