import type { RecordedAction, FrameChainEntry } from '../engine/browser-recorder.js';
import type { ParamSpec } from './frontmatter.js';
import { serializeScript } from './frontmatter.js';
import type { ScriptMeta } from './frontmatter.js';

// ── Public API ────────────────────────────────────────────────────────────────

export interface CompileScriptOptions {
  recording: RecordedAction[];
  description: string;
  sessionProfileId?: number;
  paramHints?: { name: string; valueToReplace: string; type: 'string' | 'number' | 'boolean' }[];
  recordingSource?: string;
}

export interface CompiledScript {
  /** Frontmatter + body, ready to write to scripts/{name}.js */
  source: string;
  /** Frontmatter only (parsed back-shape for previewing). */
  meta: ScriptMeta;
}

export interface CompiledScriptBody {
  /** JS body only (no frontmatter). Pass to serializeScript({ frontmatter, body }). */
  body: string;
  /** Param specs that were actually used from paramHints (unused hints are dropped). */
  paramSpecs: ParamSpec[];
}

// ── Quoting helpers ───────────────────────────────────────────────────────────

/** Single-quoted JS string literal (selector / simple URLs). */
function quoteSingle(s: string): string {
  return "'" + s.replace(/\\/g, '\\\\').replace(/'/g, "\\'") + "'";
}

/** Double-quoted JS string literal via JSON.stringify — safe for arbitrary strings. */
function quoteDouble(s: string): string {
  return JSON.stringify(s);
}

/**
 * Normalize a frameChain entry to the structured form.
 * Legacy recordings may store `string[]`; we accept both shapes.
 */
function normalizeFrameEntry(e: FrameChainEntry | string): FrameChainEntry {
  if (typeof e === 'string') return { selector: e };
  return e;
}

/**
 * Compose a Playwright FrameLocator / Page chain expression for `frameChain`.
 *
 * Returns 'page' when chain is empty. Each entry maps to either:
 *   - `.frameLocator(sel)` when only `selector` is set (unique attr selector)
 *   - `.locator(sel).nth(N).contentFrame()` when `index` is set (positional fallback)
 *
 * Both forms return a FrameLocator and compose, so a mixed chain works:
 *   page.frameLocator('iframe[name="x"]').locator('iframe').nth(0).contentFrame()
 */
function targetExpr(rawChain?: (FrameChainEntry | string)[]): string {
  if (!rawChain || rawChain.length === 0) return 'page';
  let expr = 'page';
  for (const raw of rawChain) {
    const entry = normalizeFrameEntry(raw);
    if (entry.index !== undefined) {
      expr = `${expr}.locator(${quoteSingle(entry.selector)}).nth(${entry.index}).contentFrame()`;
    } else {
      expr = `${expr}.frameLocator(${quoteSingle(entry.selector)})`;
    }
  }
  return expr;
}

// ── Core compiler ─────────────────────────────────────────────────────────────

/**
 * Compile just the JS body from a recording, returning the body string and the
 * param specs that were actually used. Callers that need to control frontmatter
 * timestamps (e.g. flushAndStageRecording) should use this instead of
 * compileScript, then call serializeScript themselves.
 */
export function compileScriptBody(opts: CompileScriptOptions): CompiledScriptBody {
  const { recording, paramHints = [] } = opts;

  // Build a value→paramHint lookup (first-wins for duplicate valueToReplace)
  const hintByValue = new Map<string, typeof paramHints[0]>();
  for (const hint of paramHints) {
    if (!hintByValue.has(hint.valueToReplace)) {
      hintByValue.set(hint.valueToReplace, hint);
    }
  }

  // Track which param names actually appear in the recording (preserving order of first use)
  const usedParamNames = new Map<string, ParamSpec>(); // name → spec

  // Generate action lines
  const lines: string[] = [];
  const lastIdx = recording.length - 1;
  let returnVar: string | undefined;

  for (let i = 0; i < recording.length; i++) {
    const action = recording[i];
    const isLast = i === lastIdx;

    switch (action.type) {
      case 'goto': {
        const url = action.url ?? '';
        lines.push(`    await page.goto(${quoteSingle(url)});`);
        break;
      }

      case 'click': {
        const selector = action.selector ?? '';
        const target = targetExpr(action.frameChain);
        lines.push(`    await ${target}.locator(${quoteSingle(selector)}).click();`);
        break;
      }

      case 'fill': {
        const selector = action.selector ?? '';
        const value = action.value ?? '';
        const hint = hintByValue.get(value);
        let valueExpr: string;
        if (hint) {
          valueExpr = `params.${hint.name}`;
          if (!usedParamNames.has(hint.name)) {
            usedParamNames.set(hint.name, { name: hint.name, type: hint.type });
          }
        } else {
          valueExpr = quoteDouble(value);
        }
        const target = targetExpr(action.frameChain);
        lines.push(`    await ${target}.locator(${quoteSingle(selector)}).fill(${valueExpr});`);
        break;
      }

      case 'wait': {
        const ms = action.ms ?? 0;
        lines.push(`    await page.waitForTimeout(${ms});`);
        break;
      }

      case 'screenshot': {
        const value = action.value ?? '';
        lines.push(`    await page.screenshot({ path: ${quoteDouble(value)} });`);
        break;
      }

      case 'getText': {
        const selector = action.selector ?? '';
        const target = targetExpr(action.frameChain);
        lines.push(`    const __text = await ${target}.locator(${quoteSingle(selector)}).textContent();`);
        if (isLast) returnVar = '__text';
        break;
      }

      case 'dumpHtml': {
        const selector = action.selector ?? '';
        const target = targetExpr(action.frameChain);
        lines.push(
          `    const __html = await ${target}.locator(${quoteSingle(selector)}).evaluate(el => el.outerHTML);`
        );
        if (isLast) returnVar = '__html';
        break;
      }
    }
  }

  // Build body
  const bodyLines: string[] = [
    'async function main({ context, params }) {',
    '  const page = await context.newPage();',
    '  try {',
    ...lines,
  ];

  if (returnVar) {
    bodyLines.push(`    return ${returnVar};`);
  }

  bodyLines.push('  } finally {', '    await page.close();', '  }', '}', 'module.exports = main;', '');

  const body = bodyLines.join('\n');
  const paramSpecs: ParamSpec[] = Array.from(usedParamNames.values());

  return { body, paramSpecs };
}

export function compileScript(opts: CompileScriptOptions): CompiledScript {
  const { description, sessionProfileId, recordingSource } = opts;

  const { body, paramSpecs } = compileScriptBody(opts);

  // Build frontmatter meta
  const meta: ScriptMeta = { description, params: paramSpecs };
  if (sessionProfileId !== undefined) meta.sessionProfileId = sessionProfileId;
  if (recordingSource !== undefined) meta.recordingSource = recordingSource;

  // Serialize using Task 2.1's serializeScript
  const source = serializeScript({ frontmatter: meta, body });

  return { source, meta };
}
