#!/usr/bin/env bash
# reattach.sh — reload a live extension slot in place (no rebuild, no relaunch)
#
# Purpose:
#   Quick-reuse leaf for `mm-harness launch` on an already-healthy extension slot:
#   re-snapshots dist/chrome into the loaded runtime-dist so the running Chrome
#   picks up watched code changes, then reloads the extension over CDP (service
#   worker + extension pages) and re-opens the wallet. Optionally navigates a
#   main tab to --start-url. Runs entirely against the EXISTING slot Chrome
#   profile — it never stops the watcher, kills Chrome, or clears the webpack
#   cache (that is the clean path in live.sh).
#
# Inputs (flags / env):
#   --target <metamask-extension> (default $PWD; becomes the working dir)
#   --cdp-port <port> (required, numeric) — the slot's CDP endpoint
#   --watcher-port <port> (optional; informational only)
#   --start-url <url> (optional; opened as a main tab beside the wallet)
#   --ext-id <id> (optional; else resolved from the extension.id file or CDP)
#   --dist-dir <rel> (default dist/chrome), --settle-ms <ms> (default 8000)
#
# Outputs:
#   Progress on stderr. Exit 0 — reloaded; 1 — repo/reload failed; 2 — bad args
#   / non-numeric port / CDP unreachable.
#
# Never touches: product source files; the webpack watcher; the Chrome process;
# other slots' browsers (single CDP port scope).
set -euo pipefail

TARGET="$PWD"
CDP_PORT="${CDP_PORT:-}"
WATCHER_PORT="${WATCHER_PORT:-}"
START_URL="${EXTENSION_START_URL:-}"
EXT_ID="${EXT_ID:-}"
DIST_DIR="dist/chrome"
SETTLE_MS="8000"
DISPLAY_MODE="${EXTENSION_DISPLAY_MODE:-fullscreen}"

require_value() { [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; }
while [ "$#" -gt 0 ]; do
  case "$1" in
    --target) require_value "$@"; TARGET="$2"; shift 2 ;;
    --cdp-port) require_value "$@"; CDP_PORT="$2"; shift 2 ;;
    --watcher-port) require_value "$@"; WATCHER_PORT="$2"; shift 2 ;;
    --start-url) require_value "$@"; START_URL="$2"; shift 2 ;;
    --ext-id) require_value "$@"; EXT_ID="$2"; shift 2 ;;
    --dist-dir) require_value "$@"; DIST_DIR="$2"; shift 2 ;;
    --settle-ms) require_value "$@"; SETTLE_MS="$2"; shift 2 ;;
    --display-mode) require_value "$@"; DISPLAY_MODE="$2"; shift 2 ;;
    -h|--help)
      echo "Usage: reattach.sh --target <metamask-extension> --cdp-port <port> [--watcher-port <port>] [--start-url <url>] [--ext-id <id>] [--dist-dir <rel>] [--settle-ms <ms>] [--display-mode fullscreen|sidepanel]"
      exit 0
      ;;
    *) echo "reattach: unknown arg: $1" >&2; exit 2 ;;
  esac
done

if [ -z "$CDP_PORT" ]; then
  echo "reattach: --cdp-port is required" >&2
  echo "  Next: mm-harness launch --build   (clean build + relaunch a fresh slot)" >&2
  exit 2
fi
case "$CDP_PORT" in
  *[!0-9]*) echo "reattach: --cdp-port must be numeric (got: $CDP_PORT)" >&2; exit 2 ;;
esac
case "$DISPLAY_MODE" in
  fullscreen|sidepanel) ;;
  *) echo "reattach: --display-mode must be fullscreen or sidepanel (got: $DISPLAY_MODE)" >&2; exit 2 ;;
esac

if [ ! -f "$TARGET/package.json" ]; then
  echo "reattach: --target is not a checkout (no package.json): $TARGET" >&2
  echo "  Next: mm-harness launch --target /path/to/metamask-extension --build" >&2
  exit 1
fi
TARGET="$(cd "$TARGET" && pwd)"

if ! curl -s -m 3 "http://127.0.0.1:${CDP_PORT}/json/version" >/dev/null 2>&1; then
  echo "reattach: CDP not reachable on port ${CDP_PORT}; the slot browser is not live." >&2
  echo "  Next: mm-harness launch --build   (clean build + relaunch)" >&2
  exit 2
fi

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
for _hp in "$SCRIPT_DIR/lib/harness-path.sh" "$SCRIPT_DIR/../../adapters/shared/harness-path.sh" "$SCRIPT_DIR/../shared/harness-path.sh"; do
  [ -f "$_hp" ] && { . "$_hp"; break; }
done
unset _hp
DIST_ABS="$TARGET/$DIST_DIR"
if command -v recipe_runtime_dir >/dev/null 2>&1; then
  RUNTIME_DIR="$(recipe_runtime_dir)"
else
  RUNTIME_DIR="${RECIPE_RUNTIME_DIR:-temp/recipe/runtime}"
fi
RUNTIME_DIST_DIR="${RECIPE_RUNTIME_DIST_DIR:-runtime-dist}"
# The running Chrome loads this unpacked extension directory. Do not recreate or
# rsync-delete it while Chrome is live: Chrome can keep the intended
# chrome-extension:// target URL but serve chrome-error://chromewebdata after its
# loaded directory is replaced underneath it. Fresh snapshots belong to the clean
# relaunch path; quick reattach only reloads the already-loaded runtime.
RUNTIME_DIST_ABS="$TARGET/$RUNTIME_DIR/$RUNTIME_DIST_DIR"
echo "[reattach] reusing loaded runtime-dist: $RUNTIME_DIST_ABS" >&2
if [ -d "$DIST_ABS" ] && [ -d "$RUNTIME_DIST_ABS" ]; then
  echo "[reattach] refreshing loaded runtime-dist in place" >&2
  rsync -a --exclude _metadata "$DIST_ABS/" "$RUNTIME_DIST_ABS/" >&2
  node "$SCRIPT_DIR/configure-runtime-manifest.cjs" --target "$TARGET" --manifest "$RUNTIME_DIST_ABS/manifest.json" >&2
  node "$SCRIPT_DIR/stamp-runtime-title.cjs" --target "$TARGET" --runtime-dist "$RUNTIME_DIST_ABS" --runtime-dir "$RUNTIME_DIR" >&2
fi

# Resolve the extension id: explicit flag, then the recorded id file, else CDP.
if [ -z "$EXT_ID" ]; then
  for idf in "$TARGET/$RUNTIME_DIR/extension.id" "$SCRIPT_DIR/extension.id"; do
    if [ -f "$idf" ]; then
      EXT_ID="$(tr -d '[:space:]' < "$idf")"
      [ -n "$EXT_ID" ] && break
    fi
  done
fi

cd "$TARGET"
echo "[reattach] reloading extension in place over CDP :${CDP_PORT}" >&2

CDP_PORT="$CDP_PORT" EXT_ID="$EXT_ID" TARGET="$TARGET" SCRIPT_DIR="$SCRIPT_DIR" RUNTIME_DIR="$RUNTIME_DIR" RUNTIME_DIST_DIR="$RUNTIME_DIST_DIR" START_URL="$START_URL" SETTLE_MS="$SETTLE_MS" DISPLAY_MODE="$DISPLAY_MODE" node <<'NODE'
let chromium; try { chromium = require('@playwright/test').chromium; } catch { chromium = require('playwright').chromium; }
const fs = require('node:fs');
const path = require('node:path');
const { extensionIdFromManifestFile } = require(path.join(process.env.SCRIPT_DIR, 'lib/extension-id.cjs'));
const { readSlotId, applyPersistentSlotTitle } = require(path.join(process.env.SCRIPT_DIR, 'lib/slot-title.cjs'));
const { evaluatePageViaCdp } = require(path.join(process.env.SCRIPT_DIR, 'lib/playwright-cdp.cjs'));

const port = process.env.CDP_PORT;
const target = process.env.TARGET;
const runtimeDir = process.env.RUNTIME_DIR;
const runtimeDistDir = process.env.RUNTIME_DIST_DIR;
const startUrl = process.env.START_URL || '';
const settleMs = Number(process.env.SETTLE_MS || '8000') || 8000;
const displayMode = process.env.DISPLAY_MODE || 'fullscreen';
const slotId = readSlotId(target, runtimeDir);

if (!runtimeDir || !runtimeDistDir) {
  console.error('reattach: resolved runtime paths were not provided.');
  process.exit(2);
}

function extensionIdFromRuntimeDist() {
  return extensionIdFromManifestFile(path.join(target, runtimeDir, runtimeDistDir, 'manifest.json'));
}

function validExtensionId(value) {
  return /^[a-p]{32}$/u.test(String(value || '')) ? String(value) : '';
}

function writeExtensionId(id) {
  if (!validExtensionId(id)) return;
  try {
    const idPath = path.join(target, runtimeDir, 'extension.id');
    fs.mkdirSync(path.dirname(idPath), { recursive: true });
    fs.writeFileSync(idPath, `${id}\n`);
  } catch {
    // Best-effort marker repair; the live CDP reload is the source of truth.
  }
}

function extensionIdFrom(context, provided) {
  const urls = [];
  const ids = [];
  for (const page of allPages(context)) urls.push(page.url());
  for (const worker of allServiceWorkers(context)) urls.push(worker.url());
  for (const url of urls) {
    const match = /^chrome-extension:\/\/([^/]+)\//u.exec(url);
    if (match && !ids.includes(match[1])) ids.push(match[1]);
  }
  if (provided) {
    if (ids.length === 0 || ids.includes(provided)) return provided;
    return '';
  }
  return ids[0] || '';
}

function allContexts(context) {
  const browser = context.browser?.();
  const contexts = browser?.contexts?.();
  return contexts && contexts.length ? contexts : [context];
}

function allPages(context) {
  return allContexts(context).flatMap((candidate) => candidate.pages());
}

function allServiceWorkers(context) {
  return allContexts(context).flatMap((candidate) => candidate.serviceWorkers());
}

async function connect() {
  const browser = await chromium.connectOverCDP(`http://127.0.0.1:${port}`);
  const context = browser.contexts()[0];
  return { browser, context };
}

async function stampSlotTitle(page) {
  if (!slotId || !page) return;
  await evaluatePageViaCdp(page, applyPersistentSlotTitle, slotId).catch(() => {
    // Best-effort operator affordance; reload success is verified by CDP state.
  });
}

async function closeDisposableChromeTabs(context) {
  for (const page of allPages(context)) {
    const url = page.url();
    if (url === 'chrome://newtab/' || url === 'chrome://new-tab-page/' || url === 'chrome://extensions/') {
      await page.close().catch(() => {
        // Best-effort tab hygiene; runtime health is verified separately.
      });
    }
  }
}

async function closePages(context, predicate) {
  for (const page of allPages(context)) {
    if (!predicate(page)) continue;
    await page.close().catch(() => {
      // Best-effort display-mode cleanup; final health checks own correctness.
    });
  }
}

(async () => {
  // Phase 1: attach to the already-loaded extension page. Do not call
  // chrome.runtime.reload() here: on the slot Chrome version it can leave the
  // target URL as chrome-extension://.../home.html while the actual document is
  // chrome-error://chromewebdata. Clean relaunch owns loading a new unpacked
  // runtime-dist; quick reattach only refreshes/foregrounds the live UI.
  let { browser, context } = await connect();
  const providedExtId = extensionIdFromRuntimeDist() || validExtensionId(process.env.EXT_ID);
  const extId = extensionIdFrom(context, providedExtId);
  if (!extId) {
    if (typeof browser.disconnect === 'function') await browser.disconnect();
    else await browser.close();
    console.error('reattach: could not resolve the extension id from runtime-dist or live CDP targets.');
    console.error('  Next: mm-harness launch --build   (clean build + relaunch)');
    process.exit(1);
  }
  writeExtensionId(extId);

  let page = allPages(context)
    .find((candidate) => candidate.url().startsWith(`chrome-extension://${extId}/`));
  if (!page) {
    page = await context.newPage();
    await page.goto(`chrome-extension://${extId}/home.html`, {
      waitUntil: 'domcontentloaded',
      timeout: 15000,
    });
  }
  await page.reload({ waitUntil: 'domcontentloaded', timeout: 15000 }).catch(async () => {
    await page.goto(`chrome-extension://${extId}/home.html`, {
      waitUntil: 'domcontentloaded',
      timeout: 15000,
    });
  });
  if (typeof browser.disconnect === 'function') await browser.disconnect();
  else await browser.close();

  // Phase 2: re-open/retitle the wallet and, when requested, a dapp tab. Chrome
  // itself never restarted, so CDP stays up.
  ({ browser, context } = await connect());
  let home = allPages(context)
    .find(
      (candidate) =>
        candidate.url().startsWith(`chrome-extension://${extId}/`) &&
        !candidate.url().includes('/sidepanel.html'),
    );
  if (!home) {
    home = await context.newPage();
    await home
      .goto(`chrome-extension://${extId}/home.html`, { waitUntil: 'domcontentloaded', timeout: 15000 })
      .catch(() => { /* wallet home is best-effort; the reload already applied */ });
  }
  await stampSlotTitle(home);
  for (const page of allPages(context)) {
    const url = page.url();
    if (
      page !== home &&
      url.startsWith(`chrome-extension://${extId}/`) &&
      url.includes('/home.html')
    ) {
      await stampSlotTitle(page);
    }
  }
  await closeDisposableChromeTabs(context);
  if (startUrl) {
    const dapp = allPages(context)
      .find((candidate) => candidate.url().startsWith('http://') || candidate.url().startsWith('https://'));
    const tab = dapp || (await context.newPage());
    await tab
      .goto(startUrl, { waitUntil: 'domcontentloaded', timeout: 20000 })
      .catch(() => { /* dapp navigation is best-effort */ });
  }
  if (displayMode === 'fullscreen') {
    await closePages(context, (candidate) => {
      const url = candidate.url();
      if (url.startsWith(`chrome-extension://${extId}/`) && url.includes('/sidepanel.html')) return true;
      if (!startUrl && (url.startsWith('http://') || url.startsWith('https://'))) return true;
      return false;
    });
    if (process.env.MM_HARNESS_FOCUS_BROWSER === '1') {
      await home.bringToFront().catch(() => {});
    }
  }
  if (typeof browser.disconnect === 'function') await browser.disconnect();
  else await browser.close();
})().catch((error) => {
  console.error(`reattach: ${error && error.message ? error.message : error}`);
  console.error('  Next: mm-harness launch --build   (clean build + relaunch)');
  process.exit(1);
});
NODE

echo "[reattach] extension reloaded in place — no rebuild, no relaunch" >&2
echo "  Next: mm-harness verify   (confirm the reloaded runtime)" >&2
