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

remote="${MAILHUB_DEPLOY_REMOTE:-}"
remote_dir="${MAILHUB_DEPLOY_DIR:-}"
branch="${MAILHUB_DEPLOY_BRANCH:-$(git branch --show-current)}"
git_url="${MAILHUB_DEPLOY_GIT_URL:-$(git remote get-url origin)}"
stash_remote="${MAILHUB_DEPLOY_STASH_REMOTE:-0}"
run_maildir_migration="${MAILHUB_DEPLOY_RUN_MAILDIR_MIGRATION:-0}"

if [[ -z "${remote}" || -z "${remote_dir}" ]]; then
  echo "Set MAILHUB_DEPLOY_REMOTE and MAILHUB_DEPLOY_DIR before deploying." >&2
  echo "Example: MAILHUB_DEPLOY_REMOTE=deploy@example.com MAILHUB_DEPLOY_DIR=/opt/mailhub npm run deploy:remote" >&2
  exit 1
fi

if [[ -z "${branch}" ]]; then
  echo "Unable to detect current git branch." >&2
  exit 1
fi

git fetch origin "${branch}"

local_head="$(git rev-parse HEAD)"
remote_head="$(git rev-parse "origin/${branch}")"
if [[ "${local_head}" != "${remote_head}" ]]; then
  echo "Local HEAD is not pushed to origin/${branch}." >&2
  echo "Commit and push first, then run this deploy script." >&2
  exit 1
fi

ssh -o ServerAliveInterval=15 -o ServerAliveCountMax=4 "${remote}" \
  'bash -s' -- "${remote_dir}" "${branch}" "${git_url}" "${stash_remote}" "${run_maildir_migration}" <<'REMOTE'
set -euo pipefail

remote_dir="$1"
branch="$2"
git_url="$3"
stash_remote="$4"
run_maildir_migration="$5"

cd "${remote_dir}"

wait_for_compose_health() {
  local timeout="${MAILHUB_DEPLOY_HEALTH_TIMEOUT:-180}"
  local deadline=$((SECONDS + timeout))
  local service container_id snapshot state health all_ready

  while (( SECONDS < deadline )); do
    all_ready=1
    for service in "$@"; do
      container_id="$(docker compose ps --all --quiet "${service}" </dev/null 2>/dev/null | tail -n 1 || true)"
      if [[ -z "${container_id}" ]]; then
        all_ready=0
        continue
      fi
      snapshot="$(docker inspect \
        --format '{{.State.Status}} {{if .State.Health}}{{.State.Health.Status}}{{else}}missing{{end}}' \
        "${container_id}" </dev/null 2>/dev/null || true)"
      state="${snapshot%% *}"
      health="${snapshot#* }"
      if [[ "${state}" == "exited" || "${state}" == "dead" ]]; then
        docker compose logs --tail=100 "${service}" </dev/null >&2 || true
        return 1
      fi
      [[ "${state}" == "running" && "${health}" == "healthy" ]] || all_ready=0
    done
    [[ "${all_ready}" == "1" ]] && return 0
    sleep 2
  done

  docker compose ps </dev/null >&2 || true
  docker compose logs --tail=100 "$@" </dev/null >&2 || true
  return 1
}

verify_mail_runtime() {
  local dovecot_probe_name=".mailhub-dovecot-runtime-probe"

  docker compose exec -T app </dev/null node -e '
    const fs = require("node:fs");
    const dovecotProbe = `/data/maildir/${process.argv[1]}`;
    try {
      fs.unlinkSync(dovecotProbe);
    } catch (error) {
      if (error.code !== "ENOENT") throw error;
    }
    const secret = fs.readFileSync("/run/secrets/dovecot_auth_secret", "utf8").trim();
    fs.accessSync("/data/maildir", fs.constants.R_OK | fs.constants.W_OK | fs.constants.X_OK);
    const probe = `/data/maildir/.mailhub-app-health-${process.pid}`;
    try {
      fs.writeFileSync(probe, "", { mode: 0o600, flag: "wx" });
    } finally {
      try { fs.unlinkSync(probe); } catch {}
    }
    fetch("http://app:3001/internal/dovecot/auth", {
      method: "POST",
      headers: {
        authorization: `Bearer ${secret}`,
        "content-type": "application/json"
      },
      body: JSON.stringify({
        username: "mailhub-healthcheck@invalid.invalid",
        password: "mailhub-healthcheck-invalid-password",
        service: "imap",
        remoteIp: "127.0.0.1"
      }),
      signal: AbortSignal.timeout(5000)
    }).then(async (response) => {
      const payload = await response.json();
      if (response.status !== 200 || payload.authenticated !== false) process.exit(1);
    }).catch(() => process.exit(1));
  ' "${dovecot_probe_name}" >/dev/null 2>&1 || {
    echo "MailHub authentication bridge or app Maildir access check failed." >&2
    return 1
  }

  docker compose exec -T app </dev/null node -e '
    const tls = require("node:tls");

    const socket = tls.connect({
      host: "dovecot",
      port: 31993,
      servername: process.env.MAIL_HOSTNAME || "mailhub.local",
      rejectUnauthorized: false
    });
    socket.setEncoding("utf8");

    let buffer = "";
    let finished = false;
    let loginSent = false;
    const timer = setTimeout(() => finish(false), 15000);

    function finish(ok) {
      if (finished) return;
      finished = true;
      clearTimeout(timer);
      socket.destroy();
      process.exit(ok ? 0 : 1);
    }

    socket.on("data", (chunk) => {
      buffer += chunk;
      const lines = buffer.split(/\r?\n/).filter(Boolean);
      if (!loginSent && lines.some((line) => /^\* OK\b/i.test(line))) {
        loginSent = true;
        socket.write("A1 LOGIN \"mailhub-healthcheck@invalid.invalid\" \"mailhub-healthcheck-invalid-password\"\r\n");
      }

      const tagged = lines.find((line) => /^A1\b/i.test(line));
      if (!tagged) return;
      if (/temporary authentication failure|unavailable/i.test(tagged)) return finish(false);
      finish(/\bNO\b/i.test(tagged) && /auth/i.test(tagged));
    });
    socket.on("error", () => finish(false));
    socket.on("end", () => finish(false));
  ' >/dev/null 2>&1 || {
    echo "Dovecot IMAPS authentication path check failed." >&2
    return 1
  }

  if ! docker compose exec -T --user 1000:1000 dovecot </dev/null sh -ec '
    test -r /run/secrets/dovecot_auth_secret
    test -s /run/secrets/dovecot_auth_secret
    probe="/srv/vmail/$1"
    test ! -e "$probe"
    umask 077
    : >"$probe"
    test -f "$probe"
  ' -- "${dovecot_probe_name}" >/dev/null 2>&1; then
    docker compose exec -T app </dev/null node -e '
      const fs = require("node:fs");
      try { fs.unlinkSync(`/data/maildir/${process.argv[1]}`); } catch {}
    ' "${dovecot_probe_name}" >/dev/null 2>&1 || true
    echo "Dovecot secret or Maildir write access check failed." >&2
    return 1
  fi

  docker compose exec -T app </dev/null node -e '
    const fs = require("node:fs");
    const probe = `/data/maildir/${process.argv[1]}`;
    try {
      if (!fs.statSync(probe).isFile()) process.exitCode = 1;
    } finally {
      try { fs.unlinkSync(probe); } catch {}
    }
  ' "${dovecot_probe_name}" >/dev/null 2>&1 || {
    echo "Dovecot Maildir write probe was not visible to the MailHub app." >&2
    return 1
  }
}

if ! git remote get-url origin </dev/null >/dev/null 2>&1; then
  git remote add origin "${git_url}" </dev/null
fi

if [[ -n "$(git status --porcelain </dev/null)" ]]; then
  if [[ "${stash_remote}" == "1" ]]; then
    git stash push -u -m "pre-deploy-$(date -u +%Y%m%d-%H%M%S)" </dev/null
  else
    echo "Remote working tree is dirty. Set MAILHUB_DEPLOY_STASH_REMOTE=1 to stash it before pulling." >&2
    git status --short </dev/null >&2
    exit 1
  fi
fi

previous_revision="$(git rev-parse HEAD </dev/null)"
stopped_app_container=""
stopped_dovecot_container=""
mail_services_stopped_for_migration=0
maildir_cutover_committed=0
on_deploy_exit() {
  local status=$?
  trap - EXIT
  if [[ "${status}" != "0" ]]; then
    echo "Deployment failed. Previous revision was ${previous_revision}; inspect the running containers before recovery." >&2
    if [[ "${mail_services_stopped_for_migration}" == "1" ]]; then
      echo "Restarting the pre-migration MailHub mail services." >&2
      if [[ -n "${stopped_app_container}" ]]; then
        docker start "${stopped_app_container}" </dev/null >/dev/null 2>&1 || \
          echo "Unable to restart the pre-migration app container ${stopped_app_container}." >&2
      fi
      if [[ -n "${stopped_dovecot_container}" ]]; then
        docker start "${stopped_dovecot_container}" </dev/null >/dev/null 2>&1 || \
          echo "Unable to restart the pre-migration Dovecot container ${stopped_dovecot_container}." >&2
      fi
    elif [[ "${maildir_cutover_committed}" == "1" ]]; then
      docker compose stop app dovecot </dev/null >/dev/null 2>&1 || \
        echo "Unable to stop the post-cutover mail services; inspect their port exposure immediately." >&2
      echo "Maildir cutover is already committed; legacy mail services will not be restarted because that would create two conflicting sources of truth." >&2
      echo "The maintenance window remains active; recover the current Compose services or perform an explicit revision rollback before reopening mail traffic." >&2
    fi
    docker compose ps </dev/null >&2 || true
  fi
  exit "${status}"
}
trap on_deploy_exit EXIT

git fetch origin "${branch}" </dev/null
git checkout "${branch}" </dev/null
git pull --ff-only origin "${branch}" </dev/null
./scripts/prepare-dovecot.sh </dev/null
if [[ "$(id -u)" == "0" ]]; then
  MAILHUB_CERT_READER_GID=1000 MAILHUB_CERT_RESTART=0 ./scripts/sync-tls-certificate.sh </dev/null
else
  MAILHUB_CERT_RESTART=0 ./scripts/sync-tls-certificate.sh </dev/null
fi
docker compose build app postfix </dev/null
docker compose pull dovecot </dev/null
if [[ "${run_maildir_migration}" == "1" ]]; then
  stopped_app_container="$(docker compose ps --all --quiet app </dev/null 2>/dev/null | tail -n 1 || true)"
  stopped_dovecot_container="$(docker compose ps --all --quiet dovecot </dev/null 2>/dev/null | tail -n 1 || true)"
  mail_services_stopped_for_migration=1
  docker compose stop app dovecot </dev/null
  docker compose run --rm --no-deps -T app </dev/null node scripts/migrate-sqlite-maildir.js
  mail_services_stopped_for_migration=0
else
  echo "Skipping Maildir migration check; set MAILHUB_DEPLOY_RUN_MAILDIR_MIGRATION=1 to run it."
fi
maildir_cutover_committed=1
docker compose up -d </dev/null
wait_for_compose_health app postfix dovecot
verify_mail_runtime
MAILHUB_CERT_RESTART=1 ./scripts/sync-tls-certificate.sh </dev/null
wait_for_compose_health app postfix dovecot
verify_mail_runtime
docker compose ps </dev/null
trap - EXIT
REMOTE
