#!/usr/bin/env bash
# /Users/kinsleykeli/Sites/milleionskob/alphakob/scripts/make_share_zip.sh
# Robust, deterministic ZIP builder with manifest, self-verify, and archive test.
# Combines:
#   A) Archive test + COPYFILE_DISABLE + fixed shasum check
#   B) Per-file manifest (size, lines, sha256) inside and alongside ZIP
#   C) Self-verify by extracting and re-hashing
#   D) Deterministic file ordering for reproducible archives

set -euo pipefail

# ---------- Config / paths ----------
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
PROJECT_NAME="$(basename "$ROOT")"       # e.g., alphakob

# Optional flags:
#   --manifest   Include .share-manifest.tsv inside ZIP and next to ZIP
#   --sha256     Write ZIP SHA-256 file next to ZIP
#   --dbbackup   Include a PostgreSQL SQL dump from php/.env
# Optional positional arg:
#   output directory (defaults to parent of project root)
OUT_DIR="$(dirname "$ROOT")"
GENERATE_MANIFEST=0
GENERATE_SHA256=0
GENERATE_DB_BACKUP=0
OUT_DIR_SET=0

for arg in "$@"; do
  case "$arg" in
    --manifest)
      GENERATE_MANIFEST=1
      ;;
    --sha256)
      GENERATE_SHA256=1
      ;;
    --dbbackup)
      GENERATE_DB_BACKUP=1
      ;;
    --help|-h)
      cat <<EOF
Usage: $(basename "$0") [output_dir] [--manifest] [--sha256] [--dbbackup]
  output_dir   Directory where ZIP is written (optional)
  --manifest   Include/write manifest TSV files
  --sha256     Write ZIP SHA-256 file
  --dbbackup   Include a PostgreSQL SQL dump from php/.env
EOF
      exit 0
      ;;
    --*)
      echo "ERROR: Unknown option: $arg" >&2
      exit 1
      ;;
    *)
      if [[ $OUT_DIR_SET -eq 0 ]]; then
        OUT_DIR="$arg"
        OUT_DIR_SET=1
      else
        echo "ERROR: Multiple output directories provided: '$OUT_DIR' and '$arg'" >&2
        exit 1
      fi
      ;;
  esac
done

mkdir -p "$OUT_DIR"

# Timestamped output filename
TS="$(date +%Y%m%d-%H%M%S)"
OUT_ZIP_SUFFIX=""
if [[ $GENERATE_DB_BACKUP -eq 1 ]]; then
  OUT_ZIP_SUFFIX="-dbbackup"
fi
OUT_ZIP="${OUT_DIR}/${PROJECT_NAME}-share${OUT_ZIP_SUFFIX}-${TS}.zip"

# Avoid macOS resource forks/AppleDouble junk; ensure stable sort/locale
export COPYFILE_DISABLE=1
export LC_ALL=C

echo "• Project root : $ROOT"
echo "• Output dir   : $OUT_DIR"
echo "• Output file  : $OUT_ZIP"
echo

# ---------- Pre-flight checks ----------
if ! command -v zip >/dev/null 2>&1; then
  echo "ERROR: 'zip' is not installed. On macOS: 'brew install zip'." >&2
  exit 1
fi

if [[ $GENERATE_DB_BACKUP -eq 1 ]] && ! command -v pg_dump >/dev/null 2>&1; then
  echo "ERROR: 'pg_dump' is not installed or not in PATH." >&2
  exit 1
fi

HAVE_ZIPINFO=0
if command -v zipinfo >/dev/null 2>&1; then
  HAVE_ZIPINFO=1
fi

HASH_CMD=""
if command -v shasum >/dev/null 2>&1; then
  HASH_CMD="shasum -a 256"
elif command -v sha256sum >/dev/null 2>&1; then
  HASH_CMD="sha256sum"
else
  echo "WARNING: No 'shasum' or 'sha256sum' found. SHA-256 hashing disabled." >&2
fi

# ---------- Database backup config ----------
DB_DUMP_REL=""
DB_DUMP_INSIDE=""
DB_DUMP_ROOT_PATH=""
DB_DUMP_TMP=""
DB_HOST=""
DB_PORT=""
DB_NAME=""
DB_USER=""
DB_PASS=""

if [[ $GENERATE_DB_BACKUP -eq 1 ]]; then
  ENV_FILE="$ROOT/php/.env"
  if [[ ! -f "$ENV_FILE" ]]; then
    echo "ERROR: Expected env file not found: $ENV_FILE" >&2
    exit 1
  fi

  DB_HOST="$(sed -n 's/^DB_HOST=//p' "$ENV_FILE" | head -n 1)"
  DB_PORT="$(sed -n 's/^DB_PORT=//p' "$ENV_FILE" | head -n 1)"
  DB_NAME="$(sed -n 's/^DB_NAME=//p' "$ENV_FILE" | head -n 1)"
  DB_USER="$(sed -n 's/^DB_USER=//p' "$ENV_FILE" | head -n 1)"
  DB_PASS="$(sed -n 's/^DB_PASS=//p' "$ENV_FILE" | head -n 1)"

  if [[ -z "$DB_HOST" || -z "$DB_PORT" || -z "$DB_NAME" || -z "$DB_USER" ]]; then
    echo "ERROR: Missing required DB settings in $ENV_FILE" >&2
    exit 1
  fi

  DB_DUMP_REL=".share-db-backup.sql"
  DB_DUMP_INSIDE="${PROJECT_NAME}/${DB_DUMP_REL}"
  DB_DUMP_ROOT_PATH="${ROOT}/${DB_DUMP_REL}"
fi

# ---------- Helpers ----------
# line_count POSIX-safe: count like editors do (include final unterminated line).
# Strategy:
#   L = wc -l (newline count)
#   if file non-empty AND last byte != '\n' (10), then L = L + 1
line_count() {
  local f="$1"
  # wc -l counts newline characters; trim spaces for BSD wc
  local L
  L="$(wc -l < "$f" | tr -d ' ')"

  # If file is empty, L stays 0 regardless of trailing newline logic
  if [[ ! -s "$f" ]]; then
    printf "%s" "$L"
    return 0
  fi

  # Read last byte and check if it's a newline (10).
  # BSD tail supports `-c 1`. Use `od` to get byte value reliably.
  # If od returns nothing (odd binary), treat as non-newline.
  local last_byte_dec
  last_byte_dec="$(tail -c 1 "$f" | od -An -t u1 | tr -d ' ')"
  if [[ -z "$last_byte_dec" || "$last_byte_dec" != "10" ]]; then
    L="$((L + 1))"
  fi

  printf "%s" "$L"
}

# ---------- Build the deterministic file list ----------
cd "$ROOT"
mapfile -t REL_FILES < <(
  find . -type d \( \
      -path './.git' -o \
      -path './vendor' -o \
      -path './temp' -o \
      -path './node_modules' -o \
      -path './scripts/vendor' -o \
      -path './storage/logs' -o \
      -path './storage/cache' -o \
      -path './.idea' \
    \) -prune -o -type f -print \
  | sed 's#^\./##' \
  | grep -Ev '^(\.DS_Store|.*\/\.DS_Store)$' \
  | grep -Ev '^\.(DS_Store|_*|Spotlight-V100|Trashes)$' \
  | grep -Ev '(^|/)\._' \
  | grep -Ev '^config/local\.php$' \
  | grep -Ev '^php/runtime/' \
  | grep -Ev '(^|/)\.env(\..*)?$' \
  | grep -Ev '(^|/)php_errors\.log$' \
  | sort
)

if [[ ${#REL_FILES[@]} -eq 0 ]]; then
  echo "ERROR: No files found to archive after exclusions." >&2
  exit 1
fi

# ---------- Build manifest (size, lines, sha256) ----------
MANIFEST_TMP="$(mktemp -t "${PROJECT_NAME}.manifest.XXXXXX.tsv")"
{
  echo -e "REL_PATH\tBYTES\tLINES\tSHA256"
  for rel in "${REL_FILES[@]}"; do
    # Byte size (BSD/macOS)
    BYTES="$(stat -f %z -- "$rel")"

    # FIXED: Line count that includes a final unterminated line
    LINES="$(line_count "$rel")"

    # SHA-256
    if [[ -n "$HASH_CMD" ]]; then
      SHA="$($HASH_CMD -- "$rel" | awk '{print $1}')"
    else
      SHA="-"
    fi

    printf "%s\t%s\t%s\t%s\n" "$rel" "$BYTES" "$LINES" "$SHA"
  done
} > "$MANIFEST_TMP"

# ---------- Build the ZIP deterministically ----------
pushd "$(dirname "$ROOT")" >/dev/null

cleanup() {
  rm -rf "$VERIFY_TMP" "$MANIFEST_TMP" "$FILELIST_TMP" "$DB_DUMP_TMP" 2>/dev/null || true
  if [[ -n "$DB_DUMP_ROOT_PATH" ]]; then
    rm -f "$DB_DUMP_ROOT_PATH" 2>/dev/null || true
  fi
}

trap cleanup EXIT

FILELIST_TMP="$(mktemp -t "${PROJECT_NAME}.filelist.XXXXXX.txt")"
for rel in "${REL_FILES[@]}"; do
  printf "%s/%s\n" "$PROJECT_NAME" "$rel" >> "$FILELIST_TMP"
done

MANIFEST_INSIDE="${PROJECT_NAME}/.share-manifest.tsv"
if [[ $GENERATE_MANIFEST -eq 1 ]]; then
  cp "$MANIFEST_TMP" "$MANIFEST_INSIDE"
  printf "%s\n" "$MANIFEST_INSIDE" >> "$FILELIST_TMP"
fi

if [[ $GENERATE_DB_BACKUP -eq 1 ]]; then
  DB_DUMP_TMP="$(mktemp -t "${PROJECT_NAME}.db.XXXXXX.sql")"
  PGPASSWORD="$DB_PASS" pg_dump \
    -h "$DB_HOST" \
    -p "$DB_PORT" \
    -U "$DB_USER" \
    -d "$DB_NAME" \
    --clean --if-exists --no-owner --no-privileges \
    > "$DB_DUMP_TMP"
  cp "$DB_DUMP_TMP" "$DB_DUMP_ROOT_PATH"
  printf "%s\n" "$DB_DUMP_INSIDE" >> "$FILELIST_TMP"
fi

zip -9 -X -q "$OUT_ZIP" -@ < "$FILELIST_TMP"
zip -T "$OUT_ZIP" >/dev/null

if [[ $GENERATE_MANIFEST -eq 1 ]]; then
  rm -f "$MANIFEST_INSIDE"
fi
if [[ $GENERATE_DB_BACKUP -eq 1 ]]; then
  rm -f "$DB_DUMP_ROOT_PATH"
fi
popd >/dev/null

# ---------- Write sibling artifacts ----------
if [[ $GENERATE_MANIFEST -eq 1 ]]; then
  cp "$MANIFEST_TMP" "${OUT_ZIP}.manifest.tsv"
fi

if [[ $GENERATE_SHA256 -eq 1 && -n "$HASH_CMD" ]]; then
  $HASH_CMD -- "$OUT_ZIP" | tee "${OUT_ZIP}.sha256" >/dev/null
fi

# ---------- Quick listing & summary ----------
echo
echo "— Sanity check —"
if [[ $HAVE_ZIPINFO -eq 1 ]]; then
  COUNT="$(zipinfo -1 "$OUT_ZIP" | wc -l | tr -d ' ')"
  echo "Archive contains ${COUNT} entries. First 20:"
  zipinfo -1 "$OUT_ZIP" | sed -n '1,20p'
else
  echo "zipinfo not found; using 'unzip -l' preview:"
  unzip -l "$OUT_ZIP" | sed -n '1,30p'
fi
echo

echo "Top 10 largest files in archive:"
if [[ $HAVE_ZIPINFO -eq 1 ]]; then
  zipinfo -l "$OUT_ZIP" \
    | awk 'BEGIN{hdr=1} NR>3 && $0!~/(^$|^--------|^Total)/ {print $1, substr($0,index($0,$4))}' \
    | sort -nrk1 | head -n 10
else
  unzip -l "$OUT_ZIP" | awk '/^[ ]*[0-9]+[ ]/{print $1" "$4}' | sort -nrk1 | head -n 10
fi
echo

# ---------- Self-verify ----------
echo "— Self-verify —"
VERIFY_TMP="$(mktemp -d -t "${PROJECT_NAME}.verify.XXXXXX")"

unzip -qq "$OUT_ZIP" -d "$VERIFY_TMP"

FAILURES=0
while IFS=$'\t' read -r REL BYTES LINES SHA; do
  if [[ "$REL" == "REL_PATH" ]]; then
    continue
  fi
  SRC_PATH="$ROOT/$REL"
  OUT_PATH="$VERIFY_TMP/$PROJECT_NAME/$REL"

  if [[ ! -f "$OUT_PATH" ]]; then
    echo "FAIL: Missing in extracted archive: $REL"
    ((FAILURES++))
    continue
  fi

  BYTES_OUT="$(stat -f %z -- "$OUT_PATH")"
  LINES_OUT="$(line_count "$OUT_PATH")"

  if [[ -n "$HASH_CMD" && "$SHA" != "-" ]]; then
    SHA_OUT="$($HASH_CMD -- "$OUT_PATH" | awk '{print $1}')"
  else
    SHA_OUT="-"
  fi

  if [[ "$BYTES" != "$BYTES_OUT" ]]; then
    echo "FAIL: Size mismatch for $REL (src:$BYTES vs out:$BYTES_OUT)"
    ((FAILURES++))
  fi
  if [[ "$LINES" != "$LINES_OUT" ]]; then
    echo "FAIL: Line count mismatch for $REL (src:$LINES vs out:$LINES_OUT)"
    ((FAILURES++))
  fi
  if [[ -n "$HASH_CMD" && "$SHA" != "-" && "$SHA" != "$SHA_OUT" ]]; then
    echo "FAIL: SHA mismatch for $REL"
    ((FAILURES++))
  fi
done < "$MANIFEST_TMP"

if [[ $FAILURES -gt 0 ]]; then
  echo "❌ Self-verify failed with $FAILURES mismatches." >&2
  exit 2
fi

echo "✅ Self-verify passed (sizes, lines, hashes match)."
echo
echo "✅ Done: $OUT_ZIP"
if [[ $GENERATE_MANIFEST -eq 1 ]]; then
  echo "   • Manifest inside ZIP: ${PROJECT_NAME}/.share-manifest.tsv"
  echo "   • Manifest next to ZIP: ${OUT_ZIP}.manifest.tsv"
fi
if [[ $GENERATE_SHA256 -eq 1 && -n "$HASH_CMD" ]]; then
  echo "   • ZIP SHA256 file     : ${OUT_ZIP}.sha256"
fi
