/**
 * World progress computation — maps world state onto the 8-phase ladder
 * shown in the inbox row progress bar.
 *
 * @module world-progress
 */

import path from 'node:path';
import { homedir } from 'node:os';
import { createRequire } from 'node:module';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { readPlanProgress } from './plan-progress.mjs';

const execFileAsync = promisify(execFile);

// Mirror of @olam/core/src/world-paths.mjs. Inlined deliberately: host-cp's
// slim Docker image does NOT bundle @olam/core (see server.mjs ~L560 for the
// architectural decision). Keep these two definitions in sync until the
// host-cp image build is taught to vendor workspace deps.
const WORLD_DB_FILENAME = 'world.db';
function getWorldDbPath(workspacePath) {
  return path.join(workspacePath, WORLD_DB_FILENAME);
}

/**
 * Phase ladder definition.
 * @type {Array<{name: string, index: number}>}
 */
const PHASES = [
  { name: 'starting', index: 1 },
  { name: 'implementing', index: 2 },
  { name: 'committing', index: 3 },
  { name: 'pushing', index: 4 },
  { name: 'in_review', index: 5 },
  { name: 'ci_failed', index: 6 },
  { name: 'ready', index: 7 },
  { name: 'merged', index: 8 },
];

const PHASE_TOTAL = PHASES.length;
const IDLE_THRESHOLD_MS = 5 * 60 * 1000; // 5 minutes

/**
 * Determine the current phase from observable state.
 *
 * @param {{
 *   thoughts: number,
 *   commitsAhead: number,
 *   pushed: boolean,
 *   prUrl: string|null,
 *   prChecks: 'pending'|'passing'|'failing'|null,
 *   prState: 'open'|'merged'|'closed'|null,
 * }} state
 * @returns {string} phase name
 */
export function determinePhase({ thoughts, commitsAhead, pushed, prUrl, prChecks, prState }) {
  // merged
  if (prState === 'merged') return 'merged';

  // prUrl exists
  if (prUrl) {
    if (prChecks === 'failing') return 'ci_failed';
    if (prChecks === 'passing' && prState === 'open') return 'ready';
    // prChecks is null or pending
    return 'in_review';
  }

  // No PR yet
  if (pushed) return 'pushing';
  if (commitsAhead >= 1) return 'committing';
  if (thoughts >= 30) return 'implementing';
  return 'starting';
}

/**
 * Build the safe/default response for a world.
 *
 * @param {string} worldId
 * @returns {object}
 */
export function makeSafeResponse(worldId) {
  return {
    worldId,
    phase: 'starting',
    phaseIndex: 1,
    phaseTotal: PHASE_TOTAL,
    isIdle: false,
    thoughts: 0,
    lastActivityAt: null,
    runtimeMs: 0,
    commitsAhead: 0,
    pushed: false,
    prUrl: null,
    prNumber: null,
    prChecks: null,
    prState: null,
    plan: null,
  };
}

/**
 * Read a world row from worlds.db.
 *
 * @param {string} dbPath
 * @param {string} worldId
 * @returns {{ branch: string, repos: string[], workspacePath: string, createdAt: string } | null}
 */
function defaultReadWorldRow(dbPath, worldId) {
  try {
    const Database = createRequire(import.meta.url)('better-sqlite3');
    const db = new Database(dbPath, { readonly: true });
    db.pragma('journal_mode = WAL');
    const row = db.prepare(
      'SELECT branch, repos, workspace_path, created_at FROM worlds WHERE id = ?',
    ).get(worldId);
    db.close();
    if (!row) return null;
    let repos = [];
    try {
      repos = typeof row.repos === 'string' ? JSON.parse(row.repos) : (row.repos ?? []);
    } catch {
      repos = [];
    }
    return {
      branch: row.branch ?? 'main',
      repos,
      workspacePath: row.workspace_path ?? '',
      createdAt: row.created_at ?? null,
    };
  } catch {
    return null;
  }
}

/**
 * Read thought count and last activity from a world.db.
 *
 * @param {string} dbPath
 * @returns {{ count: number, lastAt: string|null }}
 */
function defaultReadThoughts(dbPath) {
  try {
    const Database = createRequire(import.meta.url)('better-sqlite3');
    const db = new Database(dbPath, { readonly: true });
    db.pragma('journal_mode = WAL');
    const row = db
      .prepare('SELECT COUNT(*) AS cnt, MAX(created_at) AS last_at FROM thought_nodes')
      .get();
    db.close();
    return {
      count: Number(row?.cnt ?? 0),
      lastAt: row?.last_at ?? null,
    };
  } catch {
    return { count: 0, lastAt: null };
  }
}

/**
 * Count commits ahead of origin/main for a git worktree.
 *
 * @param {string} worktreePath
 * @returns {Promise<number>}
 */
async function defaultGitCommitsAhead(worktreePath) {
  try {
    const { stdout } = await execFileAsync(
      'git',
      ['-C', worktreePath, 'rev-list', 'origin/main..HEAD', '--count'],
      { timeout: 5000 },
    );
    const n = parseInt(stdout.trim(), 10);
    return Number.isFinite(n) ? n : 0;
  } catch {
    return 0;
  }
}

/**
 * Check whether the branch has been pushed to origin.
 *
 * @param {string} worktreePath
 * @param {string} branch
 * @returns {Promise<boolean>}
 */
async function defaultGitIsPushed(worktreePath, branch) {
  try {
    await execFileAsync(
      'git',
      ['-C', worktreePath, 'rev-parse', '--quiet', '--verify', `origin/${branch}`],
      { timeout: 5000 },
    );
    return true;
  } catch {
    return false;
  }
}

/**
 * Compute the current progress state for a world.
 *
 * @param {string} worldId
 * @param {{
 *   worldsDbPath?: string,
 *   prCache?: { getPr: (prUrl: string, getToken: () => Promise<string|null>) => Promise<{state:string|null,number:number|null,checks:string|null}|null> },
 *   prStateStore?: { get: (worldId: string) => object|null },
 *   getGhToken?: () => Promise<string|null>,
 *   _readWorldRow?: (dbPath: string, worldId: string) => object|null,
 *   _readThoughts?: (dbPath: string) => { count: number, lastAt: string|null },
 *   _gitCommitsAhead?: (worktreePath: string) => Promise<number>,
 *   _gitIsPushed?: (worktreePath: string, branch: string) => Promise<boolean>,
 * }} [deps]
 * @returns {Promise<object>}
 */
export async function computeProgress(worldId, deps = {}) {
  const safe = makeSafeResponse(worldId);

  try {
    const {
      worldsDbPath = process.env.OLAM_WORLDS_DB ?? path.join(homedir(), '.olam/worlds.db'),
      prCache = null,
      prStateStore = null,
      getGhToken = async () => null,
      _readWorldRow = defaultReadWorldRow,
      _readThoughts = defaultReadThoughts,
      _gitCommitsAhead = defaultGitCommitsAhead,
      _gitIsPushed = defaultGitIsPushed,
    } = deps;

    // Read world row
    const worldRow = _readWorldRow(worldsDbPath, worldId);
    if (!worldRow) return safe;

    const { branch, repos, workspacePath, createdAt } = worldRow;
    const worktreePath = repos.length > 0 ? path.join(workspacePath, repos[0]) : workspacePath;

    // Compute runtimeMs
    const runtimeMs = createdAt ? Date.now() - new Date(createdAt).getTime() : 0;

    // Read thoughts
    const thoughtsDbPath = getWorldDbPath(workspacePath);
    const { count: thoughts, lastAt: thoughtsLastAt } = _readThoughts(thoughtsDbPath);

    // Git state
    const [commitsAhead, pushed] = await Promise.all([
      _gitCommitsAhead(worktreePath),
      _gitIsPushed(worktreePath, branch),
    ]);

    // PR state — check prStateStore first
    let prUrl = null;
    let prNumber = null;
    let prState = null;
    let prChecks = null;

    if (prStateStore) {
      const prEntry = prStateStore.get(worldId);
      if (prEntry) {
        prUrl = prEntry.pr_url ?? null;
        prNumber = prEntry.pr_number ?? null;
        // Normalize merged_destroyed → merged
        const rawState = prEntry.pr_state ?? null;
        prState = rawState === 'merged_destroyed' ? 'merged' : (rawState === 'none' ? null : rawState);
      }
    }

    // Live PR data from cache
    if (prUrl && prCache) {
      try {
        const livePr = await prCache.getPr(prUrl, getGhToken);
        if (livePr) {
          prChecks = livePr.checks;
          // Update state if live data shows merged
          if (livePr.state === 'merged') prState = 'merged';
          if (livePr.number != null) prNumber = livePr.number;
        }
      } catch {
        // Non-fatal
      }
    }

    // Determine phase
    const phase = determinePhase({ thoughts, commitsAhead, pushed, prUrl, prChecks, prState });
    const phaseEntry = PHASES.find((p) => p.name === phase) ?? PHASES[0];

    // Idle overlay — only for implementing or committing phases
    let isIdle = false;
    if (phase === 'implementing' || phase === 'committing') {
      if (thoughtsLastAt) {
        const lastActivityMs = new Date(thoughtsLastAt).getTime();
        if (!isNaN(lastActivityMs) && Date.now() - lastActivityMs > IDLE_THRESHOLD_MS) {
          isIdle = true;
        }
      }
    }

    // Plan progress — additive; null when no tracker found
    const lastActivityAtMs = thoughtsLastAt ? new Date(thoughtsLastAt).getTime() : null;
    const plan = readPlanProgress(worktreePath, branch, { lastActivityAtMs });

    return {
      worldId,
      phase,
      phaseIndex: phaseEntry.index,
      phaseTotal: PHASE_TOTAL,
      isIdle,
      thoughts,
      lastActivityAt: thoughtsLastAt ?? null,
      runtimeMs: Math.max(0, runtimeMs),
      commitsAhead,
      pushed,
      prUrl,
      prNumber,
      prChecks,
      prState,
      plan,
    };
  } catch {
    return safe;
  }
}
