/**
 * Browser test automation — run-index.jsonl rows (execute_live / generate_script layout).
 * Used by CLI runTest; generic JSONL I/O is in @zibby/core/utils/run-registry.js.
 */

import { existsSync, readdirSync, statSync } from 'fs';
import { join, relative, sep, resolve as pathResolve } from 'path';
import {
  appendRunIndexRecord,
  readRunIndexRecordsFromFile,
  resolveRunIndexPath,
} from '../../dist/utils/run-registry.js';
import { mergeSessionRunState, readSessionRunState } from '../../dist/utils/run-state-session.js';
import { partitionRunIndexBySession, runIndexSessionEntryIsLive } from '../../dist/utils/run-index-merge.js';
import { DEFAULT_OUTPUT_BASE, SESSIONS_DIR } from '../../dist/framework/constants.js';
import { BROWSER_TEST_PIPELINE_NODE_IDS } from './pipeline-ids.js';

export { BROWSER_TEST_PIPELINE_NODE_IDS };

/** Stable id for Studio + run-index: Studio env, else session folder id (CLI/chat). */
function runIndexCorrelationId(sessionId) {
  const env = process.env.ZIBBY_STUDIO_TEST_CASE_ID;
  if (env != null && String(env).trim() !== '') return String(env).trim();
  return sessionId != null ? String(sessionId) : '';
}

const SCRIPT_CANDIDATES = [
  join('generate_script', 'generated-test.spec.js'),
  join('generate_script', 'generated-test.spec.ts'),
  join('generate_script', 'playwright.spec.ts'),
  join('generate_script', 'test.spec.ts'),
];

function firstExistingVideo(sessionAbs) {
  const dirs = [
    join(sessionAbs, 'execute_live', 'videos'),
    join(sessionAbs, 'execute_live'),
    sessionAbs,
  ];
  for (const d of dirs) {
    if (!existsSync(d)) continue;
    let names;
    try {
      names = readdirSync(d);
    } catch {
      continue;
    }
    const webm = names.find((f) => f.endsWith('.webm'));
    if (webm) return join(d, webm);
  }
  return null;
}

function firstExistingEvents(sessionAbs) {
  const c = [join(sessionAbs, 'execute_live', 'events.json'), join(sessionAbs, 'events.json')];
  for (const p of c) {
    if (existsSync(p)) return p;
  }
  return null;
}

function firstExistingScript(sessionAbs) {
  for (const rel of SCRIPT_CANDIDATES) {
    const p = join(sessionAbs, rel);
    if (existsSync(p)) return p;
  }
  return null;
}

/**
 * Discover absolute artifact paths for a browser-test session directory.
 * @param {string} sessionPathAbs
 */
export function discoverBrowserTestSessionArtifacts(sessionPathAbs) {
  if (!sessionPathAbs || !existsSync(sessionPathAbs)) {
    return { videoPathAbs: null, eventsPathAbs: null, scriptPathAbs: null };
  }
  return {
    videoPathAbs: firstExistingVideo(sessionPathAbs),
    eventsPathAbs: firstExistingEvents(sessionPathAbs),
    scriptPathAbs: firstExistingScript(sessionPathAbs),
  };
}

/**
 * @param {object} opts
 * @param {string} opts.cwd
 * @param {object} opts.result - return value from agent.run / runSingleNode
 * @param {boolean} opts.success
 * @param {string} [opts.outputBase]
 * @param {string} [opts.specPath]
 * @param {string} [opts.errorMessage]
 * @param {string} [opts.status] — override default completed/failed (e.g. `interrupted`)
 */
export function buildBrowserTestRunIndexRecord(opts) {
  const cwd = opts.cwd || process.cwd();
  const outputBase = opts.outputBase || DEFAULT_OUTPUT_BASE;
  const result = opts.result || {};
  const state = result.state || {};
  const sessionPathAbs = state.sessionPath;
  if (!sessionPathAbs || typeof sessionPathAbs !== 'string') {
    return null;
  }

  const sessionId = sessionPathAbs.split(/[/\\]/).filter(Boolean).pop();
  if (!sessionId) return null;

  const { videoPathAbs, eventsPathAbs, scriptPathAbs } =
    discoverBrowserTestSessionArtifacts(sessionPathAbs);

  const toRel = (abs) => {
    if (!abs) return null;
    try {
      return relative(cwd, abs).split(sep).join('/');
    } catch {
      return null;
    }
  };

  let specRel = null;
  if (opts.specPath) {
    try {
      const absSpec = pathResolve(cwd, opts.specPath);
      specRel = relative(cwd, absSpec).split(sep).join('/');
    } catch {
      specRel = String(opts.specPath).split(sep).join('/');
    }
  }

  return {
    v: 1,
    recordKind: 'summary',
    ts: Date.now(),
    sessionId,
    status: opts.status ?? (opts.success ? 'completed' : 'failed'),
    cwd,
    outputBase,
    sessionPathAbs,
    sessionDirRel: toRel(sessionPathAbs),
    videoPathAbs: videoPathAbs || null,
    eventsPathAbs: eventsPathAbs || null,
    scriptPathAbs: scriptPathAbs || null,
    videoRel: toRel(videoPathAbs),
    eventsRel: toRel(eventsPathAbs),
    scriptRel: toRel(scriptPathAbs),
    specRel,
    source: process.env.ZIBBY_RUN_SOURCE || 'cli',
    studioTestCaseId: runIndexCorrelationId(sessionId) || null,
    errorMessage: opts.errorMessage || null,
  };
}

export function tryAppendBrowserTestRunIndex({
  cwd,
  config,
  result,
  success,
  specPath,
  errorMessage,
}) {
  try {
    const record = buildBrowserTestRunIndexRecord({
      cwd: cwd || process.cwd(),
      result,
      success,
      outputBase: config?.paths?.output || DEFAULT_OUTPUT_BASE,
      specPath,
      errorMessage,
    });
    if (record) {
      appendRunIndexRecord(record);
      if (record.sessionPathAbs) {
        mergeSessionRunState(record.sessionPathAbs, {
          sessionId: record.sessionId,
          studioTestCaseId: record.studioTestCaseId || record.sessionId,
          status: record.status,
          activeNode: null,
          activeStageIndex: null,
          errorMessage: record.errorMessage || null,
          runSource: record.source || 'cli',
          cwd: record.cwd,
          outputBase: record.outputBase,
          sessionPathAbs: record.sessionPathAbs,
        });
      }
    }
  } catch (e) {
    console.warn(`[zibby browser-test run-index] ${e.message}`);
  }
}

/**
 * Append one JSONL row when a pipeline node starts (live Mission Control).
 * @param {object} payload
 * @param {string} payload.currentNode — graph node id
 * @param {string} payload.sessionPath — absolute session dir
 * @param {string} [payload.sessionId]
 * @param {string} [payload.cwd]
 * @param {string} [payload.outputBase]
 */
/**
 * Single absolute session directory for pipeline progress + zibby-run-state.json.
 * Studio spawns the CLI with ZIBBY_SESSION_PATH; cwd may still be wrong (home, app bundle, etc.).
 * Never merge under join(cwd, …/sessions/id) when studio env points elsewhere — that caused duplicate dirs.
 */
export function resolveBrowserTestPipelineSessionPathAbs({
  sessionPath,
  sessionId,
  cwd,
  outputBase = DEFAULT_OUTPUT_BASE,
} = {}) {
  const cwd0 = cwd || process.cwd();
  const ob = outputBase || DEFAULT_OUTPUT_BASE;
  const sid =
    sessionId != null && String(sessionId).trim() !== ''
      ? String(sessionId).trim()
      : null;

  const studio = process.env.ZIBBY_RUN_SOURCE === 'studio';
  const envSession = process.env.ZIBBY_SESSION_PATH && String(process.env.ZIBBY_SESSION_PATH).trim();
  if (studio && envSession) {
    return pathResolve(envSession);
  }

  const trimmed = sessionPath && String(sessionPath).trim();
  if (trimmed) {
    return pathResolve(trimmed);
  }

  const sessionsRoot = process.env.ZIBBY_SESSIONS_ROOT && String(process.env.ZIBBY_SESSIONS_ROOT).trim();
  if (sessionsRoot && sid) {
    return pathResolve(join(sessionsRoot, sid));
  }

  if (process.env.ZIBBY_SESSION_PATH && String(process.env.ZIBBY_SESSION_PATH).trim()) {
    return pathResolve(String(process.env.ZIBBY_SESSION_PATH).trim());
  }

  return pathResolve(join(cwd0, ob, SESSIONS_DIR, sid || 'invalid'));
}

export function tryAppendBrowserTestPipelineProgress(payload) {
  try {
    const currentNode = payload?.currentNode;
    if (!currentNode || !BROWSER_TEST_PIPELINE_NODE_IDS.includes(currentNode)) return;

    const sessionPath = payload.sessionPath;
    const sessionId =
      payload.sessionId ||
      (sessionPath && String(sessionPath).split(/[/\\]/).filter(Boolean).pop()) ||
      null;
    if (!sessionId) return;

    const cwd = payload.cwd || process.cwd();
    const outputBase = payload.outputBase || DEFAULT_OUTPUT_BASE;
    const activeStageIndex = BROWSER_TEST_PIPELINE_NODE_IDS.indexOf(currentNode);
    const specPathRaw = payload?.specPath != null ? String(payload.specPath).trim() : '';
    const taskDescription =
      payload?.taskDescription != null ? String(payload.taskDescription) : '';
    let specRel = null;
    if (specPathRaw) {
      try {
        const absSpec = pathResolve(cwd, specPathRaw);
        specRel = relative(cwd, absSpec).split(sep).join('/');
      } catch {
        specRel = specPathRaw.split(sep).join('/');
      }
    }

    const sessionPathAbs = resolveBrowserTestPipelineSessionPathAbs({
      sessionPath,
      sessionId,
      cwd,
      outputBase,
    });

    appendRunIndexRecord({
      v: 1,
      recordKind: 'progress',
      ts: Date.now(),
      sessionId,
      cwd,
      outputBase,
      sessionPathAbs,
      activeNode: currentNode,
      activeStageIndex,
      specRel,
      taskDescription: taskDescription || null,
      studioTestCaseId: runIndexCorrelationId(sessionId) || null,
      source: process.env.ZIBBY_RUN_SOURCE || 'cli',
    });

    mergeSessionRunState(sessionPathAbs, {
      sessionId,
      studioTestCaseId: runIndexCorrelationId(sessionId) || sessionId,
      status: 'running',
      activeNode: currentNode,
      activeStageIndex,
      sessionPathAbs,
      cwd,
      outputBase,
      specPath: specRel || null,
      task: taskDescription || null,
      taskDescription: taskDescription || null,
      runSource: process.env.ZIBBY_RUN_SOURCE || 'cli',
      pid: typeof process.pid === 'number' ? process.pid : null,
    });
  } catch (e) {
    console.warn(`[zibby browser-test run-index progress] ${e.message}`);
  }
}

/** Factory for graph.run `initialState.onPipelineProgress`. */
export function createBrowserTestPipelineProgressAppender({ cwd, config } = {}) {
  const cwd0 = cwd || process.cwd();
  const ob0 = config?.paths?.output || DEFAULT_OUTPUT_BASE;
  return (payload) => {
    tryAppendBrowserTestPipelineProgress({
      cwd: payload?.cwd || cwd0,
      outputBase: payload?.outputBase || ob0,
      sessionPath: payload?.sessionPath,
      sessionId: payload?.sessionId,
      currentNode: payload?.currentNode,
      specPath: payload?.specPath,
      taskDescription: payload?.taskDescription,
    });
  };
}

/**
 * Append a terminal `summary` row for every session that still looks “live” in run-index (progress newer than summary).
 * Call from SIGINT/SIGTERM so Mission Control clears after Ctrl+C in the terminal.
 * @param {object} [opts]
 * @param {string} [opts.cwd]
 * @param {object} [opts.config] — uses `config.paths.output`
 */
export function tryAppendBrowserTestInterruptedRunIndex(opts = {}) {
  try {
    const cwd0 = opts.cwd || process.cwd();
    const outputBase = opts.config?.paths?.output || opts.outputBase || DEFAULT_OUTPUT_BASE;
    const indexPath = resolveRunIndexPath(cwd0, outputBase);
    const records = readRunIndexRecordsFromFile(indexPath);
    const partitioned = partitionRunIndexBySession(records);
    const handled = new Set();

    const errMsg =
      opts.errorMessage ||
      'Run stopped (SIGINT/SIGTERM) before a normal summary was written.';

    const flushInterrupted = (sessionId, sessionPathAbs) => {
      if (!sessionId || !sessionPathAbs) return;
      if (handled.has(sessionId)) return;
      handled.add(sessionId);
      const record = buildBrowserTestRunIndexRecord({
        cwd: cwd0,
        outputBase,
        result: { state: { sessionPath: sessionPathAbs } },
        success: false,
        specPath: null,
        status: 'interrupted',
        errorMessage: errMsg,
      });
      if (record) {
        appendRunIndexRecord(record);
        mergeSessionRunState(sessionPathAbs, {
          sessionId,
          studioTestCaseId: record.studioTestCaseId || sessionId,
          status: 'interrupted',
          activeNode: null,
          activeStageIndex: null,
          errorMessage: record.errorMessage || null,
          runSource: record.source || 'cli',
          cwd: cwd0,
          outputBase,
          sessionPathAbs,
        });
      }
    };

    // 1) JSONL “live” sessions (progress newer than summary)
    for (const [folderSessionId, entry] of partitioned) {
      if (!runIndexSessionEntryIsLive(entry)) continue;
      const prog = entry.progress;
      if (!prog) continue;
      const sessionId = String(folderSessionId);
      const sessionPathAbs =
        (prog.sessionPathAbs && String(prog.sessionPathAbs)) ||
        join(cwd0, outputBase, SESSIONS_DIR, sessionId);
      flushInterrupted(sessionId, sessionPathAbs);
    }

    // 2) Disk-only live: Studio (or early CLI) wrote zibby-run-state.json as running before any
    //    run-index progress line — JSONL partition omits them, so Ctrl+C must clear by scanning sessions/.
    const sessionsRoot = join(cwd0, outputBase, SESSIONS_DIR);
    if (!existsSync(sessionsRoot)) return;
    let names;
    try {
      names = readdirSync(sessionsRoot);
    } catch {
      return;
    }
    for (const name of names) {
      const sessionPathAbs = join(sessionsRoot, name);
      let st;
      try {
        st = statSync(sessionPathAbs);
      } catch {
        continue;
      }
      if (!st.isDirectory()) continue;
      const doc = readSessionRunState(sessionPathAbs);
      if (!doc || doc.status !== 'running') continue;
      flushInterrupted(String(name), sessionPathAbs);
    }
  } catch (e) {
    console.warn(`[zibby browser-test run-index interrupt] ${e.message}`);
  }
}
