/**
 * Stage 3 (F4): generate a Page Object class from a flow shared across specs.
 *
 * Given the captured steps of a shared prefix (from detectSharedFlows), emit a
 * `pages/<Name>.ts` class whose single method replays the flow. Per D4,
 * structure becomes the method (selectors centralized here) and data values
 * become method parameters. Naming follows D7: the entry navigation's last path
 * segment (`/login` -> `LoginPage.login`), falling back to `FlowPage.run`.
 *
 * Deterministic, no LLM. Reuses writeSpec's selector + visibility-prelude
 * emitters with a `this.page` page variable, so a Page Object's selectors match
 * the crystallized specs exactly.
 */
import type { SkillStep } from '../skills/writeSkill.js';
import {
  selectorFromDescription,
  selectorForFormField,
  emitInteraction,
  blockScope,
} from './writeSpec.js';

const PAGE_VAR = 'this.page';

export interface PageObjectResult {
  /** PascalCase class name, e.g. `LoginPage`. */
  className: string;
  /** camelCase method name, e.g. `login`. */
  methodName: string;
  /** File to write under `pages/`, e.g. `LoginPage.ts`. */
  fileName: string;
  /** Generated TypeScript source. */
  source: string;
}

export function generatePageObject(
  steps: SkillStep[],
  override: { className?: string; methodName?: string } = {},
): PageObjectResult {
  const derived = deriveNames(steps);
  const className = override.className ?? derived.className;
  const methodName = override.methodName ?? derived.methodName;

  const params: string[] = [];
  const used = new Set<string>();
  const body: string[] = [];

  for (const s of steps) {
    if (s.kind !== 'step' || !s.tool) continue;
    const i = (s.input ?? {}) as Record<string, unknown>;
    switch (s.tool) {
      case 'browser_navigate':
        body.push(`await ${PAGE_VAR}.goto(${JSON.stringify(navPath(String(i.url ?? '')))});`);
        break;
      case 'browser_click':
        body.push(...blockScope(emitInteraction(
          selectorFromDescription(String(i.element ?? ''), PAGE_VAR), 'click()')));
        break;
      case 'browser_double_click':
        body.push(...blockScope(emitInteraction(
          selectorFromDescription(String(i.element ?? ''), PAGE_VAR), 'dblclick()')));
        break;
      case 'browser_hover':
        body.push(...blockScope(emitInteraction(
          selectorFromDescription(String(i.element ?? ''), PAGE_VAR), 'hover()')));
        break;
      case 'browser_type': {
        const p = uniqueParam(paramName(String(i.element ?? '')), used);
        params.push(p);
        body.push(...blockScope(emitInteraction(
          selectorFromDescription(String(i.element ?? ''), PAGE_VAR), `fill(${p})`)));
        break;
      }
      case 'browser_select_option': {
        const p = uniqueParam(paramName(String(i.element ?? '')), used);
        params.push(p);
        body.push(...blockScope(emitInteraction(
          selectorFromDescription(String(i.element ?? ''), PAGE_VAR), `selectOption(${p})`)));
        break;
      }
      case 'browser_fill_form': {
        const fields = (i.fields as Array<Record<string, unknown>> | undefined) ?? [];
        for (const f of fields) {
          const target = String(f.name ?? f.element ?? '');
          const p = uniqueParam(paramName(target), used);
          params.push(p);
          body.push(...blockScope(emitInteraction(
            selectorForFormField(target, f.type as string | undefined, PAGE_VAR), `fill(${p})`)));
        }
        break;
      }
      case 'browser_press_key':
        body.push(`await ${PAGE_VAR}.keyboard.press(${JSON.stringify(String(i.key ?? ''))});`);
        break;
      default:
        break; // diagnostics / non-replayable — skipped
    }
  }

  const paramList = params.map(p => `${p}: string`).join(', ');
  return {
    className,
    methodName,
    fileName: `${className}.ts`,
    source: renderClass(className, methodName, paramList, body),
  };
}

function renderClass(className: string, methodName: string, paramList: string, body: string[]): string {
  const out = body.length > 0 ? body : ['// (no replayable steps)'];
  const lines: string[] = [];
  lines.push(`import { expect, type Page } from '@playwright/test';`);
  lines.push('');
  lines.push('/**');
  lines.push(' * Generated by Hover from a flow shared across saved specs.');
  lines.push(' * Selectors live here so a UI change is a one-file edit; data values are');
  lines.push(' * method parameters. Plain Playwright — no Hover runtime.');
  lines.push(' */');
  lines.push(`export class ${className} {`);
  lines.push(`  constructor(private readonly page: Page) {}`);
  lines.push('');
  lines.push(`  async ${methodName}(${paramList}): Promise<void> {`);
  for (const b of out) lines.push(`    ${b}`);
  lines.push(`  }`);
  lines.push(`}`);
  lines.push('');
  return lines.join('\n');
}

function deriveNames(steps: SkillStep[]): { className: string; methodName: string } {
  for (const s of steps) {
    if (s.kind === 'step' && s.tool === 'browser_navigate') {
      const seg = lastSegment(String(((s.input ?? {}) as Record<string, unknown>).url ?? ''));
      if (seg) return { className: pascal(seg) + 'Page', methodName: camel(seg) };
    }
  }
  return { className: 'FlowPage', methodName: 'run' };
}

function lastSegment(url: string): string {
  let path = url;
  try {
    if (/^https?:\/\//.test(url)) path = new URL(url).pathname;
  } catch {
    /* keep raw */
  }
  const segs = path.split('/').filter(Boolean);
  return segs.length ? segs[segs.length - 1].replace(/[^a-zA-Z0-9]+/g, ' ').trim() : '';
}

/** Tokenize a label into words, dropping a trailing MCP role keyword. */
function words(raw: string): string[] {
  return raw
    .replace(/\s+(button|link|textbox|checkbox|radio|combobox|switch|field|input)$/i, '')
    .replace(/[^a-zA-Z0-9]+/g, ' ')
    .trim()
    .split(/\s+/)
    .filter(Boolean);
}

function camel(raw: string): string {
  const w = words(raw);
  if (w.length === 0) return 'value';
  const c =
    w[0].toLowerCase() +
    w.slice(1).map(x => x[0].toUpperCase() + x.slice(1).toLowerCase()).join('');
  return /^[a-zA-Z_]/.test(c) ? c : `v${c}`;
}

function pascal(raw: string): string {
  const c = camel(raw);
  return c.charAt(0).toUpperCase() + c.slice(1);
}

/** Parameter name derived from a field/element label. */
const paramName = camel;

function uniqueParam(base: string, used: Set<string>): string {
  const root = base || 'value';
  let name = root;
  let n = 2;
  while (used.has(name)) name = `${root}${n++}`;
  used.add(name);
  return name;
}

function navPath(url: string): string {
  if (!/^https?:\/\//.test(url)) return url;
  try {
    const u = new URL(url);
    return u.pathname + u.search + u.hash || '/';
  } catch {
    return url;
  }
}
