// crystallize-planning — atomic-or-compensating chunk-copy from a planning
// session (_planning world) into a freshly provisioned real world.
//
// APPEND-ONLY CONSTRAINT: The chunks table has a NO_DELETE + NO_UPDATE
// trigger (chunks_append_only_trigger). If chunk-copy fails mid-batch,
// any chunks already INSERTed under the new worldId STAY in the database.
// Compensating cleanup only calls destroyWorld (world container teardown) —
// it CANNOT delete the orphaned chunks. Those orphan chunks are harmless:
//   • idx_chunks_planning only covers world_id='_planning' rows.
//   • The destroyed world container no longer exists, so no subscriber
//     will ever observe those orphans through the normal shape proxy.
//   • Any future re-crystallize creates a fresh worldId, fresh session_id.
//
// IDEMPOTENCY:
//   • If crystallize_status is 'crystallized' (with a stored worldId),
//     return immediately — the work is already done.
//   • If crystallize_status is 'in_progress', we cannot safely resume
//     (we don't know how far the previous copy got, and the chunk INSERT
//     is not idempotent by worldId+sessionId alone — the PRIMARY KEY is
//     (message_id, seq), so the same chunk could be re-inserted into a
//     different new session without collision). Safe behavior: return
//     the current status so the UI can display "in progress" and the
//     operator can force-retry after manual inspection.
//
// SLUG RULE: lowercased, non-alphanum → hyphens, max 40 chars.
// Matches the dev-substrate stub in plan-chat-spa/src/server/index.ts
// (confirmed by reading that file's crystallize stub, around line 983).

import { randomUUID } from 'node:crypto';
import { PLANNING_WORLD_ID } from '@olam/chunks/schema';
import { setCrystallizeStatus } from './planning-sessions.mjs';

/**
 * Slug a plan title into a world-name-safe string.
 * Lowercased, non-alphanum → hyphens, max 40 chars, leading/trailing
 * hyphens removed. Falls back to 'plan' if result is empty.
 *
 * @param {string} title
 * @returns {string}
 */
function slugTitle(title) {
  const base = title
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-+|-+$/g, '')
    .slice(0, 40);
  return base || 'plan';
}

/**
 * Read the current crystallize_status + crystallized_world_id for a session.
 *
 * @param {object} pool
 * @param {string} sessionId
 * @returns {Promise<{crystallize_status: string, crystallized_world_id: string | null}>}
 */
async function readCrystallizeState(pool, sessionId) {
  const result = await pool.query(
    `SELECT crystallize_status, crystallized_world_id
     FROM planning_sessions
     WHERE session_id = $1`,
    [sessionId],
  );
  if (result.rows.length === 0) {
    return { crystallize_status: 'open', crystallized_world_id: null };
  }
  const row = result.rows[0];
  return {
    crystallize_status: row.crystallize_status,
    crystallized_world_id: row.crystallized_world_id ?? null,
  };
}

/**
 * SELECT all planning chunks for a session, ordered by seq.
 *
 * @param {object} pool
 * @param {string} sessionId
 * @returns {Promise<Array<{world_id, session_id, message_id, seq, actor_id, actor_type, role, chunk, chunk_type}>>}
 */
async function selectPlanningChunks(pool, sessionId) {
  const result = await pool.query(
    `SELECT world_id, session_id, message_id, seq, actor_id, actor_type, role, chunk, chunk_type
     FROM chunks
     WHERE world_id = $1 AND session_id = $2
     ORDER BY seq ASC`,
    [PLANNING_WORLD_ID, sessionId],
  );
  return result.rows;
}

/**
 * INSERT a single chunk into the new world's session.
 * Uses the original message_id + seq verbatim; only world_id and
 * session_id change to point at the new world's session.
 *
 * @param {object} pool
 * @param {object} chunk   — row from the planning session
 * @param {string} newWorldId
 * @param {string} newSessionId
 * @returns {Promise<void>}
 */
async function insertChunkIntoNewWorld(pool, chunk, newWorldId, newSessionId) {
  await pool.query(
    `INSERT INTO chunks
       (world_id, session_id, message_id, seq, actor_id, actor_type, role, chunk, chunk_type)
     VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
    [
      newWorldId,
      newSessionId,
      chunk.message_id,
      chunk.seq,
      chunk.actor_id,
      chunk.actor_type,
      chunk.role,
      chunk.chunk,
      chunk.chunk_type,
    ],
  );
}

/**
 * INSERT a system marker chunk into the ORIGINAL planning session to
 * leave an audit trail of crystallization. The marker lands at
 * world_id='_planning' + the original sessionId.
 *
 * @param {object} pool
 * @param {string} sessionId   — original planning session id
 * @param {string} worldId     — newly created world id
 * @param {number} phaseCount  — number of phases in the plan
 * @returns {Promise<void>}
 */
async function insertMarkerChunk(pool, sessionId, worldId, phaseCount) {
  const messageId = randomUUID();
  // Find the current max seq so the marker doesn't collide.
  const seqResult = await pool.query(
    `SELECT COALESCE(MAX(seq), -1) AS max_seq
     FROM chunks
     WHERE world_id = $1 AND session_id = $2`,
    [PLANNING_WORLD_ID, sessionId],
  );
  const nextSeq = Number(seqResult.rows[0].max_seq) + 1;
  await pool.query(
    `INSERT INTO chunks
       (world_id, session_id, message_id, seq, actor_id, actor_type, role, chunk, chunk_type)
     VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
    [
      PLANNING_WORLD_ID,
      sessionId,
      messageId,
      nextSeq,
      'system',
      'system',
      'system',
      `Plan crystallized into world "${worldId}" (${phaseCount} phase${phaseCount === 1 ? '' : 's'}).`,
      'text',
    ],
  );
}

/**
 * crystallizePlanningSession
 *
 * 4-phase atomic-or-compensating process:
 *   1. Set crystallize_status='in_progress'
 *   2. Call createWorld({ name: slugged-title }) → { id: worldId }
 *   3. SELECT all chunks in _planning/sessionId; INSERT each into new world
 *   4. Set crystallize_status='crystallized' (with worldId); INSERT marker chunk
 *
 * Compensating pattern on partial failure:
 *   - If createWorld throws: set status='failed', rethrow. destroyWorld NOT called.
 *   - If chunk-copy throws mid-batch: set status='failed', call destroyWorld(worldId),
 *     rethrow. Orphan chunks already INSERTed stay (append-only; see file header).
 *
 * Idempotency:
 *   - Already 'crystallized': return immediately without re-running.
 *   - Already 'in_progress': return current status (safe short-circuit; see header).
 *
 * @param {object} opts
 * @param {object}   opts.pool           — pg.Pool-compatible with .query()
 * @param {string}   opts.sessionId      — planning session to crystallize
 * @param {string}   opts.planTitle      — plan title (used for world name slug)
 * @param {Array}    opts.planPhases     — array of phase objects (name, acceptance, risks?)
 * @param {Function} opts.createWorld    — async ({ name }) => { id: string, ... }
 * @param {Function} opts.destroyWorld   — async (worldId) => void
 *
 * @returns {Promise<{worldId: string, status: string, new_session_id: string}>}
 * @throws  on failure (crystallize_status already set to 'failed' when thrown)
 */
export async function crystallizePlanningSession({
  pool,
  sessionId,
  planTitle,
  planPhases,
  createWorld,
  destroyWorld,
}) {
  // ── Idempotency guard ────────────────────────────────────────────────────
  const currentState = await readCrystallizeState(pool, sessionId);

  if (currentState.crystallize_status === 'crystallized') {
    return {
      worldId: currentState.crystallized_world_id,
      status: `crystallized:${currentState.crystallized_world_id}`,
      new_session_id: null,
    };
  }

  if (currentState.crystallize_status === 'in_progress') {
    // Cannot safely resume without knowing how far the copy got.
    // Return current status so the UI shows 'in_progress'.
    return {
      worldId: currentState.crystallized_world_id,
      status: 'in_progress',
      new_session_id: null,
    };
  }

  // ── Phase 1: mark in_progress ────────────────────────────────────────────
  await setCrystallizeStatus({ pool, sessionId, status: 'in_progress', worldId: null });

  // ── Phase 2: create world ────────────────────────────────────────────────
  let worldId;
  try {
    const worldName = slugTitle(planTitle);
    const world = await createWorld({ name: worldName });
    worldId = world.id;
  } catch (err) {
    await setCrystallizeStatus({ pool, sessionId, status: 'failed', worldId: null });
    throw err;
  }

  // ── Phase 3: copy chunks into new world ──────────────────────────────────
  const newSessionId = randomUUID();
  try {
    const chunks = await selectPlanningChunks(pool, sessionId);
    for (const chunk of chunks) {
      await insertChunkIntoNewWorld(pool, chunk, worldId, newSessionId);
    }
  } catch (err) {
    await setCrystallizeStatus({ pool, sessionId, status: 'failed', worldId: null });
    try {
      await destroyWorld(worldId);
    } catch {
      // Compensating destroy failure is non-fatal — the world may already
      // be partially torn down or the destroy operation may not be
      // reversible. Log is left to the caller's context.
    }
    throw err;
  }

  // ── Phase 4: mark crystallized + insert marker ───────────────────────────
  await setCrystallizeStatus({ pool, sessionId, status: 'crystallized', worldId });
  await insertMarkerChunk(pool, sessionId, worldId, planPhases.length);

  return {
    worldId,
    status: `crystallized:${worldId}`,
    new_session_id: newSessionId,
  };
}
