// rr-graph.ts — shared DAG helpers for Record & Replay
// Note: keep types lightweight to avoid cross-package coupling
// Centralize step type strings and tiny helpers here to avoid magic literals.

import { EDGE_LABELS, type EdgeLabel } from './labels';

export interface RRNode {
  id: string;
  type: string;
  config?: Record<string, unknown>;
}
export interface RREdge {
  id: string;
  from: string;
  to: string;
  label?: EdgeLabel;
}

// Centralized step type strings (kept in shared to avoid duplication)
export const RR_STEP_TYPES = {
  CLICK: 'click',
  DBLCLICK: 'dblclick',
  FILL: 'fill',
  DRAG: 'drag',
  KEY: 'key',
  WAIT: 'wait',
  ASSERT: 'assert',
  IF: 'if',
  FOREACH: 'foreach',
  WHILE: 'while',
  NAVIGATE: 'navigate',
  SCRIPT: 'script',
  HTTP: 'http',
  EXTRACT: 'extract',
  SCREENSHOT: 'screenshot',
  SCROLL: 'scroll',
  TRIGGER_EVENT: 'triggerEvent',
  SET_ATTRIBUTE: 'setAttribute',
  LOOP_ELEMENTS: 'loopElements',
  SWITCH_FRAME: 'switchFrame',
  OPEN_TAB: 'openTab',
  SWITCH_TAB: 'switchTab',
  CLOSE_TAB: 'closeTab',
  EXECUTE_FLOW: 'executeFlow',
  HANDLE_DOWNLOAD: 'handleDownload',
  // UI-only, mapped to WAIT
  DELAY: 'delay',
} as const;
export type RRStepType = (typeof RR_STEP_TYPES)[keyof typeof RR_STEP_TYPES];

function ensureTarget(t: any) {
  return t && typeof t === 'object' ? t : { candidates: [] };
}

function backgroundConfig(c: any): Record<string, boolean> {
  return typeof c?.background === 'boolean' ? { background: c.background } : {};
}

// Topological order using Kahn's algorithm; edges considered as-is (caller may pre-filter labels)
export function topoOrder<T extends RRNode>(nodes: T[], edges: RREdge[]): T[] {
  const id2n = new Map(nodes.map((n) => [n.id, n] as const));
  const indeg = new Map<string, number>(nodes.map((n) => [n.id, 0] as const));
  for (const e of edges) indeg.set(e.to, (indeg.get(e.to) || 0) + 1);
  const nexts = new Map<string, string[]>(nodes.map((n) => [n.id, [] as string[]] as const));
  for (const e of edges) nexts.get(e.from)!.push(e.to);
  const q: string[] = nodes.filter((n) => (indeg.get(n.id) || 0) === 0).map((n) => n.id);
  const out: T[] = [];
  while (q.length) {
    const id = q.shift()!;
    const n = id2n.get(id);
    if (!n) continue;
    out.push(n as T);
    for (const v of nexts.get(id)!) {
      indeg.set(v, (indeg.get(v) || 0) - 1);
      if ((indeg.get(v) || 0) === 0) q.push(v);
    }
  }
  return out.length === nodes.length ? out : nodes.slice();
}

// Map a Node (Flow V2) to a linear Step (Flow V1)
export function mapNodeToStep(node: RRNode): any {
  const c: any = node.config || {};
  const base = { id: node.id } as any;
  // Config-driven generic mapping (prefer this path)
  try {
    const type = String(node.type);
    // UI-only helper: delay -> wait.sleep
    if (type === 'delay') {
      const sleep = Number((c as any).sleep ?? (c as any).ms ?? 1000);
      return { ...base, type: 'wait', condition: { sleep: Math.max(0, sleep) } };
    }
    const step: any = { ...base, type, ...c };
    if (step.target) step.target = ensureTarget(step.target);
    if (step.start) step.start = ensureTarget(step.start);
    if (step.end) step.end = ensureTarget(step.end);
    return step;
  } catch {}
  switch (node.type) {
    case RR_STEP_TYPES.CLICK:
    case RR_STEP_TYPES.DBLCLICK:
      return {
        ...base,
        type: node.type,
        target: ensureTarget(c.target),
        before: c.before,
        after: c.after,
      };
    case RR_STEP_TYPES.FILL:
      return {
        ...base,
        type: RR_STEP_TYPES.FILL,
        target: ensureTarget(c.target),
        value: c.value || '',
      };
    case RR_STEP_TYPES.DRAG:
      return {
        ...base,
        type: RR_STEP_TYPES.DRAG,
        start: ensureTarget(c.start),
        end: ensureTarget(c.end),
        path: Array.isArray(c.path) ? c.path : undefined,
      };
    case RR_STEP_TYPES.KEY:
      return { ...base, type: RR_STEP_TYPES.KEY, keys: c.keys || '' };
    case RR_STEP_TYPES.WAIT:
      return {
        ...base,
        type: RR_STEP_TYPES.WAIT,
        condition: c.condition || { text: '', appear: true },
      };
    case RR_STEP_TYPES.ASSERT:
      return {
        ...base,
        type: RR_STEP_TYPES.ASSERT,
        assert: c.assert || { exists: '' },
        failStrategy: c.failStrategy,
      };
    case RR_STEP_TYPES.IF:
      return { ...base, type: RR_STEP_TYPES.IF, condition: c.condition || {} };
    case RR_STEP_TYPES.FOREACH:
      return {
        ...base,
        type: RR_STEP_TYPES.FOREACH,
        listVar: c.listVar || '',
        itemVar: c.itemVar || 'item',
        subflowId: c.subflowId || '',
      };
    case RR_STEP_TYPES.WHILE:
      return {
        ...base,
        type: RR_STEP_TYPES.WHILE,
        condition: c.condition || {},
        subflowId: c.subflowId || '',
        maxIterations: Math.max(0, Number(c.maxIterations ?? 100)),
      };
    case RR_STEP_TYPES.NAVIGATE:
      return {
        ...base,
        type: RR_STEP_TYPES.NAVIGATE,
        url: c.url || '',
        ...backgroundConfig(c),
      };
    case RR_STEP_TYPES.SCRIPT:
      return {
        ...base,
        type: RR_STEP_TYPES.SCRIPT,
        world: c.world || 'ISOLATED',
        code: c.code || '',
        when: c.when,
      };
    case RR_STEP_TYPES.DELAY: // map to wait.sleep to avoid navigation confusion
      return {
        ...base,
        type: RR_STEP_TYPES.WAIT,
        condition: { sleep: Math.max(0, Number(c.ms ?? 1000)) },
      };
    case RR_STEP_TYPES.HTTP:
      return {
        ...base,
        type: RR_STEP_TYPES.HTTP,
        method: c.method || 'GET',
        url: c.url || '',
        headers: c.headers || {},
        body: c.body,
        formData: c.formData,
        saveAs: c.saveAs || '',
      };
    case RR_STEP_TYPES.EXTRACT:
      return {
        ...base,
        type: RR_STEP_TYPES.EXTRACT,
        selector: c.selector || '',
        attr: c.attr || 'text',
        js: c.js || '',
        saveAs: c.saveAs || '',
      };
    case RR_STEP_TYPES.SCREENSHOT:
      return {
        ...base,
        type: RR_STEP_TYPES.SCREENSHOT,
        selector: c.selector || '',
        fullPage: !!c.fullPage,
        ...backgroundConfig(c),
        saveAs: c.saveAs || '',
      };
    case RR_STEP_TYPES.SCROLL:
      return {
        ...base,
        type: RR_STEP_TYPES.SCROLL,
        mode: c.mode || 'offset',
        target: ensureTarget(c.target),
        offset: c.offset || { x: 0, y: 300 },
      };
    case RR_STEP_TYPES.TRIGGER_EVENT:
      return {
        ...base,
        type: RR_STEP_TYPES.TRIGGER_EVENT,
        target: ensureTarget(c.target),
        event: c.event || 'input',
        bubbles: c.bubbles !== false,
        cancelable: !!c.cancelable,
      };
    case RR_STEP_TYPES.SET_ATTRIBUTE:
      return {
        ...base,
        type: RR_STEP_TYPES.SET_ATTRIBUTE,
        target: ensureTarget(c.target),
        name: c.name || '',
        value: c.value,
        remove: !!c.remove,
      };
    case RR_STEP_TYPES.LOOP_ELEMENTS:
      return {
        ...base,
        type: RR_STEP_TYPES.LOOP_ELEMENTS,
        selector: c.selector || '',
        saveAs: c.saveAs || 'elements',
        itemVar: c.itemVar || 'item',
        subflowId: c.subflowId || '',
      };
    case RR_STEP_TYPES.SWITCH_FRAME:
      return {
        ...base,
        type: RR_STEP_TYPES.SWITCH_FRAME,
        frame: {
          index: c.frame && c.frame.index != null ? Number(c.frame.index) : undefined,
          urlContains: c.frame?.urlContains || '',
        },
      };
    case RR_STEP_TYPES.OPEN_TAB:
      return {
        ...base,
        type: RR_STEP_TYPES.OPEN_TAB,
        url: c.url || '',
        newWindow: !!c.newWindow,
        ...backgroundConfig(c),
      };
    case RR_STEP_TYPES.SWITCH_TAB:
      return {
        ...base,
        type: RR_STEP_TYPES.SWITCH_TAB,
        tabId: c.tabId || undefined,
        urlContains: c.urlContains || '',
        titleContains: c.titleContains || '',
        ...backgroundConfig(c),
      };
    case RR_STEP_TYPES.CLOSE_TAB:
      return {
        ...base,
        type: RR_STEP_TYPES.CLOSE_TAB,
        tabIds: Array.isArray(c.tabIds) ? c.tabIds : undefined,
        url: c.url || '',
      };
    case RR_STEP_TYPES.EXECUTE_FLOW:
      return {
        ...base,
        type: RR_STEP_TYPES.EXECUTE_FLOW,
        flowId: c.flowId || '',
        inline: c.inline !== false,
        args: c.args || {},
      };
    case RR_STEP_TYPES.HANDLE_DOWNLOAD:
      return {
        ...base,
        type: RR_STEP_TYPES.HANDLE_DOWNLOAD,
        filenameContains: c.filenameContains || '',
        waitForComplete: c.waitForComplete !== false,
        timeoutMs: Math.max(0, Number(c.timeoutMs ?? 60000)),
        saveAs: c.saveAs || '',
      };
    default:
      return { ...base, type: RR_STEP_TYPES.SCRIPT, world: 'ISOLATED', code: '' };
  }
}

export function nodesToSteps(nodes: RRNode[], edges: RREdge[]): any[] {
  const order = edges && edges.length ? topoOrder(nodes, edges) : nodes.slice();
  return order.map((n) => mapNodeToStep(n));
}

// Reverse mapping (Step -> Node config)
export function mapStepToNodeConfig(step: unknown): Record<string, unknown> {
  if (!step || typeof step !== 'object') return {};
  const src = step as Record<string, unknown>;
  const out: Record<string, unknown> = {};
  for (const [k, v] of Object.entries(src)) {
    if (k === 'id' || k === 'type') continue;
    out[k] = v;
  }
  const target = out['target'];
  if (target) out['target'] = ensureTarget(target);
  const start = out['start'];
  if (start) out['start'] = ensureTarget(start);
  const end = out['end'];
  if (end) out['end'] = ensureTarget(end);
  return out;
}

export function stepsToNodes(steps: ReadonlyArray<unknown>): RRNode[] {
  const arr: RRNode[] = [];
  steps.forEach((step, i) => {
    const obj: Record<string, unknown> =
      step && typeof step === 'object' ? (step as Record<string, unknown>) : {};
    const idValue = obj['id'];
    const typeValue = obj['type'];
    const id = typeof idValue === 'string' && idValue ? idValue : `n_${i}`;
    const type = typeof typeValue === 'string' && typeValue ? typeValue : RR_STEP_TYPES.SCRIPT;
    arr.push({ id, type, config: mapStepToNodeConfig(step) });
  });
  return arr;
}

/**
 * Convert linear steps array to DAG format (nodes + edges).
 * Generates sequential edges connecting nodes in order.
 */
export function stepsToDAG(steps: ReadonlyArray<unknown>): { nodes: RRNode[]; edges: RREdge[] } {
  const nodes = stepsToNodes(steps);
  const edges: RREdge[] = [];
  for (let i = 0; i < nodes.length - 1; i++) {
    const from = nodes[i].id;
    const to = nodes[i + 1].id;
    // Include index in edge id to avoid collision when step ids repeat
    edges.push({
      id: `e_${i}_${from}_${to}`,
      from,
      to,
      label: EDGE_LABELS.DEFAULT,
    });
  }
  return { nodes, edges };
}
