/**
 * Boot-time reconciler — sync worlds.db with live docker state.
 *
 * Problem (issue #963): after Colima / userspace restart, host-cp can
 * start with worlds.db rows that no longer reflect docker reality. The
 * existing `worlds-db-source.mjs` reconciler runs DB→registry (reads
 * 'running' rows and adds them to in-memory WORLDS). It does NOT heal
 * the inverse case: a container is alive on docker but worlds.db has
 * no row (Hazel coral-sky-2478 scenario), or worlds.db says a world is
 * running but the container is gone (orphaned row).
 *
 * This module fills both gaps with a one-shot pass at boot:
 *
 *   1. List `olam-*-devbox` containers via the docker API.
 *   2. For each container, derive the worldId (strip prefix + suffix).
 *   3. Cross-check against worlds.db rows:
 *        - container alive, row exists                 → no-op
 *        - container alive, row missing                → INSERT (status=reconciled)
 *        - row says running/active, container missing  → UPDATE status=orphaned
 *
 * Fail-soft: if the docker daemon is unreachable OR better-sqlite3 is
 * not available, the function logs a warning and returns without
 * throwing. Server boot continues.
 *
 * Idempotent: a second invocation against the same docker + DB state
 * produces no further changes (existing rows are skipped at step 3a,
 * already-orphaned rows are skipped at step 3c).
 *
 * Coordination with issue #962: the dedup logic in `olam create` handles
 * per-call deduplication; this reconciler handles boot-time cleanup.
 * They don't conflict — both operate on the worlds.db source-of-truth.
 */

import { createRequire } from 'node:module';

const require = createRequire(import.meta.url);

const CONTAINER_NAME_PATTERN = /^\/?(olam-(.+)-devbox)$/;

/**
 * @typedef {object} ReconcileDeps
 * @property {string} dbPath                                  Path to worlds.db
 * @property {() => Promise<string[] | null>} listContainerNames  Returns null when docker is unreachable
 * @property {(msg: string) => void} [log]                    Defaults to console.log
 * @property {() => string} [now]                             ISO timestamp generator (overridable for tests)
 * @property {(path: string) => unknown | null} [openDb]      Overridable DB opener (tests inject fakes)
 */

/**
 * @typedef {object} ReconcileSummary
 * @property {number} inserted   Number of new rows inserted (reconciled containers)
 * @property {number} orphaned   Number of rows transitioned to status='orphaned'
 * @property {number} skipped    Containers/rows where no change was needed
 * @property {boolean} dockerUnreachable
 * @property {boolean} dbUnavailable
 */

/**
 * Extract a worldId from a docker container name.
 * Accepts either `olam-foo-bar-1234-devbox` or `/olam-foo-bar-1234-devbox`
 * (the docker API prefixes container names with a slash).
 *
 * @param {string} name
 * @returns {string | null}
 */
export function extractWorldIdFromContainerName(name) {
  if (typeof name !== 'string') return null;
  const match = CONTAINER_NAME_PATTERN.exec(name);
  if (!match) return null;
  const worldId = match[2];
  if (!worldId || worldId.length === 0) return null;
  return worldId;
}

/**
 * Default docker container lister. Hits the Docker Engine API.
 * Returns null on any failure (fail-soft).
 *
 * @param {string} dockerApiBase   e.g. 'http://docker-socket-proxy:2375'
 * @param {(msg: string) => void} log
 * @returns {Promise<string[] | null>}
 */
export async function defaultListContainerNames(dockerApiBase, log) {
  if (!dockerApiBase || dockerApiBase === 'http://localhost:2375') {
    // 'docker-cli' sentinel; no API available in this deployment mode.
    log('[boot-reconciler] docker API unavailable (bare-node mode); skipping');
    return null;
  }
  try {
    const filters = encodeURIComponent(JSON.stringify({ name: ['olam-'] }));
    const url = `${dockerApiBase}/containers/json?filters=${filters}`;
    const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
    if (!res.ok) {
      log(`[boot-reconciler] docker /containers/json returned ${res.status}; skipping`);
      return null;
    }
    const data = await res.json();
    if (!Array.isArray(data)) return [];
    const names = [];
    for (const container of data) {
      const list = container?.Names;
      if (!Array.isArray(list)) continue;
      for (const n of list) {
        if (typeof n === 'string') names.push(n);
      }
    }
    return names;
  } catch (err) {
    log(`[boot-reconciler] docker query failed: ${err.message}; skipping`);
    return null;
  }
}

/**
 * Default DB opener. Loads better-sqlite3 dynamically so a missing
 * native build degrades gracefully instead of crashing host-cp boot.
 *
 * @param {string} dbPath
 * @param {(msg: string) => void} log
 * @returns {unknown | null}
 */
export function defaultOpenDb(dbPath, log) {
  try {
    const Database = require('better-sqlite3');
    return new Database(dbPath, { fileMustExist: true });
  } catch (err) {
    if (err && err.code === 'MODULE_NOT_FOUND') {
      log('[boot-reconciler] better-sqlite3 not available; skipping');
    } else if (err && err.code === 'SQLITE_CANTOPEN') {
      log(`[boot-reconciler] ${dbPath} not found; nothing to reconcile`);
    } else {
      log(`[boot-reconciler] failed to open ${dbPath}: ${err.message}`);
    }
    return null;
  }
}

/**
 * Run a single boot-time reconciliation pass. Pure and dep-injected
 * for testability.
 *
 * @param {ReconcileDeps} deps
 * @returns {Promise<ReconcileSummary>}
 */
export async function reconcileWorldsWithDocker(deps) {
  const log = deps.log ?? console.log;
  const now = deps.now ?? (() => new Date().toISOString());
  const openDb = deps.openDb ?? ((p) => defaultOpenDb(p, log));

  const summary = {
    inserted: 0,
    orphaned: 0,
    skipped: 0,
    dockerUnreachable: false,
    dbUnavailable: false,
  };

  const containerNames = await deps.listContainerNames();
  if (containerNames === null) {
    summary.dockerUnreachable = true;
    return summary;
  }

  const liveWorldIds = new Set();
  for (const name of containerNames) {
    const worldId = extractWorldIdFromContainerName(name);
    if (worldId) liveWorldIds.add(worldId);
  }

  const db = openDb(deps.dbPath);
  if (!db) {
    summary.dbUnavailable = true;
    return summary;
  }

  try {
    /** @type {Array<{ id: string, status: string }>} */
    let rows;
    try {
      rows = db.prepare('SELECT id, status FROM worlds').all();
    } catch (err) {
      log(`[boot-reconciler] query failed: ${err.message}; skipping`);
      summary.dbUnavailable = true;
      return summary;
    }

    const dbWorlds = new Map(rows.map((r) => [r.id, r.status]));

    // Pass 1: containers alive but missing from DB → insert.
    const insertStmt = db.prepare(
      `INSERT INTO worlds
       (id, name, status, repos, branch, port_offset, workspace_path,
        compute_provider, total_cost_usd, thought_count, created_at, updated_at)
       VALUES (?, ?, 'reconciled', '[]', 'main', 0, ?, 'docker', 0, 0, ?, ?)`,
    );
    for (const worldId of liveWorldIds) {
      if (dbWorlds.has(worldId)) {
        summary.skipped += 1;
        continue;
      }
      const ts = now();
      const workspacePath = `~/.olam/worlds/${worldId}`;
      try {
        insertStmt.run(worldId, worldId, workspacePath, ts, ts);
        summary.inserted += 1;
        log(`[boot-reconciler] inserted reconciled row for ${worldId} (container alive, no DB row)`);
      } catch (err) {
        log(`[boot-reconciler] failed to insert ${worldId}: ${err.message}`);
      }
    }

    // Pass 2: DB says alive but container missing → mark orphaned.
    const orphanStmt = db.prepare(
      `UPDATE worlds SET status = 'orphaned', updated_at = ? WHERE id = ?`,
    );
    const aliveStatuses = new Set(['running', 'active', 'creating']);
    for (const [worldId, status] of dbWorlds) {
      if (liveWorldIds.has(worldId)) continue;
      if (!aliveStatuses.has(status)) continue;
      try {
        orphanStmt.run(now(), worldId);
        summary.orphaned += 1;
        log(`[boot-reconciler] marked ${worldId} as orphaned (was '${status}', container missing)`);
      } catch (err) {
        log(`[boot-reconciler] failed to mark ${worldId} orphaned: ${err.message}`);
      }
    }

    log(
      `[boot-reconciler] complete: inserted=${summary.inserted} orphaned=${summary.orphaned} ` +
        `skipped=${summary.skipped} live-containers=${liveWorldIds.size}`,
    );
  } finally {
    try { db.close?.(); } catch { /* ignore */ }
  }

  return summary;
}
