#!/usr/bin/env bash
# Usage: ./start.sh
#
# Everyday entry point for the pharmacy-starter dev stack. Brings up the
# Docker compose stack on free host ports and converges the database to a
# fully-seeded state, then prints the URLs you need.
#
# Use this instead of a bare `docker compose up -d` (or `make up`) when you
# want a conflicting port (e.g. another project already on :80 or :3306) to
# bump to the next free port automatically rather than failing the `up`.
#
# Re-run any time - every step is idempotent. `up -d` keeps the ports this
# stack already holds; the DB is only seeded when it's actually empty; the
# image is only rebuilt when it's missing.
#
# For fully isolated per-branch stacks, use ./scripts/worktree-create.sh -
# each worktree writes its own .env (unique project name + port block) and
# runs this same script.

set -euo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$ROOT"

# ── First-run bootstrap: ensure .env exists ─────────────────────────────
# Mirrors docker-setup.sh's source (.docker/config/.env.local) so a fresh
# clone can go straight to ./start.sh without a separate setup step.
if [[ ! -f .env ]]; then
  if [[ -f .docker/config/.env.local ]]; then
    echo "No .env found - seeding it from .docker/config/.env.local…"
    cp .docker/config/.env.local .env
  else
    echo "ERROR: no .env and no .docker/config/.env.local to copy from." >&2
    exit 1
  fi
fi

# ── Preflight ───────────────────────────────────────────────────────────
if ! command -v docker >/dev/null 2>&1; then
  echo "ERROR: docker not found on PATH. Install Docker Desktop or Engine first." >&2
  exit 1
fi
if ! docker compose version >/dev/null 2>&1; then
  echo "ERROR: 'docker compose' (v2) not found. Update Docker or install the compose plugin." >&2
  exit 1
fi

# ── Prepare the app filesystem ──────────────────────────────────────────
# storage/framework/* and bootstrap/cache are gitignored, so a fresh clone
# or a new worktree has none of them - and a missing storage/framework/views
# makes Laravel's blade.compiler fail to resolve, 500-ing every request.
# These are host-side (the tree is bind-mounted) and owned by the host user,
# which the container runs as (uid 1000). Idempotent.
mkdir -p \
  storage/framework/cache/data \
  storage/framework/sessions \
  storage/framework/views \
  storage/logs \
  bootstrap/cache

# Seed the uploads folder from the tracked seeder on first run (gitignored,
# so absent in a fresh worktree - without it media/images 404).
if [[ ! -d uploads && -d uploads_seeder ]]; then
  echo "Seeding uploads/ from uploads_seeder/…"
  cp -r uploads_seeder uploads
fi

# ── Resolve free host ports for every published service ─────────────────
# Sourced so the exported *_PORT vars reach the `docker compose up` below,
# and so DB_* from .env are available to the seed check later.
# shellcheck source=scripts/resolve-ports.sh
source "$ROOT/scripts/resolve-ports.sh"

# ── Build the image on first run only ───────────────────────────────────
if [[ -z "$(docker compose images -q php-apache 2>/dev/null)" ]]; then
  echo "php-apache image missing - building (first run, this takes a few minutes)…"
  docker compose build
fi

echo "Bringing up the Docker compose stack…"
docker compose up -d 2>&1 | cat; echo

# ── Repair: sync deps if lock files changed since the last run ──────────
STATE_FILE="$ROOT/.dev-state"
prev_composer="" prev_package=""
if [[ -f "$STATE_FILE" ]]; then
  prev_composer=$(grep "^composer_lock=" "$STATE_FILE" 2>/dev/null | cut -d= -f2 || true)
  prev_package=$(grep "^package_lock=" "$STATE_FILE" 2>/dev/null | cut -d= -f2 || true)
fi
# md5sum is coreutils-only (macOS ships `md5` instead), so pick whichever
# hasher is present. Only ever compared against the digest this same script
# stamped into .dev-state, so any stable hasher works - it just has to be
# consistent within a machine. Reads stdin so no hasher echoes the filename.
_file_hash() {
  [[ -f "$1" ]] || return 0
  if   command -v md5sum  >/dev/null 2>&1; then md5sum  < "$1"
  elif command -v shasum  >/dev/null 2>&1; then shasum  < "$1"
  elif command -v sha1sum >/dev/null 2>&1; then sha1sum < "$1"
  else md5 < "$1"
  fi | awk '{print $1}'
}
curr_composer=$(_file_hash "$ROOT/composer.lock")
curr_package=$(_file_hash "$ROOT/package-lock.json")

# Wait for the PHP container to accept exec before touching composer/npm.
wait_for_php() {
  local delay=1
  for _ in $(seq 1 10); do
    docker compose exec -T php-apache true 2>/dev/null && return 0
    sleep "$delay"
    (( delay = delay * 2 > 4 ? 4 : delay * 2 ))
  done
  return 1
}

if [[ ! -d "$ROOT/vendor" || "$curr_composer" != "$prev_composer" ]]; then
  echo -n "Installing PHP dependencies (composer)…"
  wait_for_php || { echo " ✗"; echo "ERROR: php-apache never became exec-ready." >&2; exit 1; }
  docker compose exec -T php-apache composer install --no-interaction --no-progress --quiet
  echo " ✓"
fi
if [[ ! -d "$ROOT/node_modules" || "$curr_package" != "$prev_package" ]]; then
  echo -n "Installing frontend dependencies (npm)…"
  wait_for_php || { echo " ✗"; echo "ERROR: php-apache never became exec-ready." >&2; exit 1; }
  docker compose exec -T php-apache npm install --no-audit --no-fund --silent
  echo " ✓"
fi

# ── Build frontend assets if the Vite manifest is missing ───────────────
# With VITE_DEV_SERVER=false (the default), templates resolve assets from
# the built manifest, not the dev server - a missing public/build/manifest.json
# makes every page 500 ("Asset manifest not found"). Build on first run /
# whenever the manifest is absent. For live HMR instead, set
# VITE_DEV_SERVER=true in .env and run `make npm-dev`.
if [[ "${VITE_DEV_SERVER:-false}" != "true" && ! -f "$ROOT/public/build/manifest.json" ]]; then
  echo "Building frontend assets (npm run build)…"
  wait_for_php || { echo " ✗"; echo "ERROR: php-apache never became exec-ready." >&2; exit 1; }
  docker compose exec -T php-apache npm run build
fi

# ── Wait for MariaDB to accept connections ──────────────────────────────
echo -n "Waiting for MariaDB"
db_ready=false
for _ in $(seq 1 30); do
  if docker compose exec -T -e MYSQL_PWD="${DB_PASSWORD}" mariadb \
      mariadb -uroot -e "SELECT 1;" >/dev/null 2>&1; then
    db_ready=true
    echo " ✓"
    break
  fi
  echo -n "."
  sleep 2
done
if [[ "$db_ready" != true ]]; then
  echo ""
  echo "ERROR: MariaDB did not become ready in time." >&2
  exit 1
fi

# ── Repair: run pending migrations (retry, fail loudly) ─────────────────
# A DB that's accepting connections doesn't guarantee the app's CLI state
# (autoload, bootstrap cache) is ready, so `migrate` can fail transiently
# right after startup. Retry with backoff; under `set -e` an unhandled
# failure here would abort silently and leave the stack looking "up" but
# never migrated.
migrate_ok=false
delay=1
migrate_out=""
for _ in $(seq 1 10); do
  if migrate_out=$(docker compose exec -T php-apache php artisan migrate --force 2>&1); then
    migrate_ok=true
    break
  fi
  sleep "$delay"
  (( delay = delay * 2 > 5 ? 5 : delay * 2 ))
done
if [[ "$migrate_ok" != true ]]; then
  echo "ERROR: 'php artisan migrate' failed after repeated attempts:" >&2
  echo "$migrate_out" >&2
  exit 1
fi
if [[ "$migrate_out" != *"Nothing to migrate"* ]]; then
  echo "$migrate_out"
fi

# ── Seed only if the database is empty (or a prior seed didn't finish) ──
# The primary source of truth is the DB itself: admin_users is created by a
# migration and populated first by DatabaseSeeder, so a missing table or a
# zero count means a fresh database. db:seed is NOT re-runnable (it would
# duplicate rows), so it is gated strictly on emptiness. The follow-on steps
# (encryption/blocks/media) ARE idempotent but not atomic with the seed, so a
# completion marker (.dev-seeded) is written only after all four succeed —
# if any step fails, set -e aborts before the marker is written and the next
# run re-runs the idempotent tail. The live empty-check still wins over a
# stale marker: a wiped DB (count 0) always re-seeds regardless of the marker.
SEED_MARKER="$ROOT/.dev-seeded"
seed_count=$(docker compose exec -T -e MYSQL_PWD="${DB_PASSWORD}" mariadb \
  mariadb -uroot "${DB_DATABASE}" -N -e \
  "SELECT COUNT(*) FROM admin_users;" 2>/dev/null | tr -d '[:space:]' || true)
db_empty=false
if ! [[ "$seed_count" =~ ^[0-9]+$ ]] || [[ "$seed_count" -eq 0 ]]; then
  db_empty=true
fi

if [[ "$db_empty" == true || ! -f "$SEED_MARKER" ]]; then
  if [[ "$db_empty" == true ]]; then
    echo "Fresh database detected - seeding…"
    docker compose exec -T php-apache php artisan db:seed --force
  else
    echo "Seed marker missing - re-running the idempotent populate steps…"
  fi
  docker compose exec -T php-apache php artisan encryption:run
  docker compose exec -T php-apache php artisan blocks:import --all --force
  # No --force: config/mediamanager.php is committed (and may be customised);
  # we only want to publish the gitignored assets that are actually missing.
  docker compose exec -T php-apache php artisan vendor:publish --tag=media-manager
  touch "$SEED_MARKER"
  echo "✓ Database populated."
else
  echo "Database already seeded (${seed_count} admin users) - skipping seed."
fi

# Persist lock-file hashes so we skip installs on the next run.
{
  echo "composer_lock=$curr_composer"
  echo "package_lock=$curr_package"
} > "$STATE_FILE"

cat <<REPORT

────────────────────────────────────────────────────
  MTC Pharmacy dev stack ready

  App          http://localhost:${APP_PORT}
  Vite (HMR)   http://localhost:${VITE_DEV_SERVER_PORT}
  Mailhog      http://localhost:${MAILHOG_WEB_PORT}   (SMTP :${MAILHOG_SMTP_PORT})
  MariaDB      localhost:${DB_EXTERNAL_PORT}   (db: ${DB_DATABASE})

  Shell        make shell        (docker compose exec php-apache bash)
  Logs         docker compose logs -f
  Stop         make down         (docker compose down)
  Worktree     ./scripts/worktree-create.sh
────────────────────────────────────────────────────
REPORT
