import { readFileSync, existsSync } from 'fs';

const ACTIONABLE_EVENTS = ['navigate', 'type', 'fill', 'click', 'select_option', 'select'];

// ARIA roles that accept text input via Playwright .fill()
const FILLABLE_ROLES = new Set(['textbox', 'combobox', 'spinbutton', 'searchbox']);

/**
 * Load recorded browser events from a session's events.json,
 * filtering to only actionable events (navigate, fill, click, etc.)
 */
export function loadRecordedActions(sessionPath, nodeName = 'execute_live') {
  const eventsPath = `${sessionPath}/${nodeName}/events.json`;
  if (!existsSync(eventsPath)) return [];
  try {
    return JSON.parse(readFileSync(eventsPath, 'utf-8'))
      .filter(e => ACTIONABLE_EVENTS.includes(e.type))
      .map(e => ({
        type: e.type,
        stableId: e.stableId || e.data?.stableId,
        value: e.data?.text || e.data?.params?.text || e.data?.url || e.data?.params?.url || e.data?.values?.[0] || e.data?.params?.values?.[0],
        element: e.data?.element || e.data?.params?.element,
        reasoning: e.reasoning || e.description
      }));
  } catch (e) {
    console.error('Failed to read events:', e.message);
    return [];
  }
}

/**
 * Index AI-captured selectors by element description (lowercase)
 * for fuzzy matching when building fallback selectors.
 */
export function buildSelectorMap(actions) {
  const map = {};
  for (const a of actions || []) {
    if (a.selectors?.role) {
      map[(a.description || '').toLowerCase()] = a.selectors;
    }
  }
  return map;
}

/** Extract meaningful tokens from a description string for fuzzy matching. */
function extractTokens(str) {
  return str.replace(/[-–—]/g, ' ').split(/\s+/).filter(t => t.length > 1);
}

/** Score how well two descriptions match by counting shared tokens. */
function tokenOverlapScore(a, b) {
  const tokensA = extractTokens(a.toLowerCase());
  const tokensB = extractTokens(b.toLowerCase());
  return tokensA.filter(t => tokensB.some(tb => tb.includes(t) || t.includes(tb))).length;
}

/**
 * Check whether an ARIA role is compatible with an action type.
 * Prevents click actions from getting textbox fallbacks and vice versa.
 */
function isRoleCompatible(role, actionType) {
  if (!role || role === 'generic' || role === 'none') return true;
  if (actionType === 'click' && FILLABLE_ROLES.has(role)) return false;
  if ((actionType === 'fill' || actionType === 'type') && !FILLABLE_ROLES.has(role)) return false;
  return true;
}

function formatRoleSelector(roleObj) {
  if (!roleObj) return '';
  const { role, name } = roleObj;
  if (role === 'generic' || role === 'none') {
    return name ? ` | fallback: getByText('${name}')` : '';
  }
  return ` | fallback: getByRole('${role}'${name ? `, { name: '${name}' }` : ''})`;
}

/**
 * Find a fallback selector for an element by matching against the AI selector map.
 * 1. Try exact description match first.
 * 2. Try role.name containment — prefer the longest (most specific) role name found in the element description.
 * 3. Fall back to fuzzy token matching with ≥2 token overlap threshold.
 * 4. Validate ARIA role compatibility with the action type.
 */
function findFallbackSelector(elementKey, selectorMap, actionType) {
  if (selectorMap[elementKey]?.role && isRoleCompatible(selectorMap[elementKey].role.role, actionType)) {
    return formatRoleSelector(selectorMap[elementKey].role);
  }

  let bestNameMatch = null;
  let bestNameLen = 0;
  for (const [, selectors] of Object.entries(selectorMap)) {
    if (!isRoleCompatible(selectors.role?.role, actionType)) continue;
    const roleName = selectors.role?.name;
    if (!roleName) continue;
    if (elementKey.includes(roleName.toLowerCase()) && roleName.length > bestNameLen) {
      bestNameLen = roleName.length;
      bestNameMatch = selectors;
    }
  }
  if (bestNameMatch?.role) return formatRoleSelector(bestNameMatch.role);

  let bestMatch = null;
  let bestScore = 0;
  for (const [k, selectors] of Object.entries(selectorMap)) {
    if (!isRoleCompatible(selectors.role?.role, actionType)) continue;
    const score = tokenOverlapScore(elementKey, k);
    if (score > bestScore) {
      bestScore = score;
      bestMatch = selectors;
    }
  }
  if (!bestMatch?.role || bestScore < 2) return '';
  return formatRoleSelector(bestMatch.role);
}

/** Extract visible text hint from element description for use as a getByText fallback. */
function extractTextHint(elementDesc) {
  if (!elementDesc) return '';
  const cnMatch = elementDesc.match(/[\u4e00-\u9fff\u3000-\u303f]+/g);
  if (cnMatch) return cnMatch.join('');
  const afterDash = elementDesc.split(/[-–—]\s*/).pop()?.trim();
  if (afterDash && afterDash !== elementDesc) return afterDash;
  return '';
}

/**
 * Mark duplicate stableIds across actions. When the same stableId appears
 * on different steps, the second+ occurrence gets nullified with a marker
 * so the LLM knows to use the fallback selector instead.
 */
export function deduplicateStableIds(actions) {
  const seen = new Map();
  return actions.map((a, i) => {
    if (!a.stableId || a.stableId === 'undefined') return a;
    if (seen.has(a.stableId)) {
      return { ...a, _dupOf: seen.get(a.stableId), stableId: null };
    }
    seen.set(a.stableId, i);
    return a;
  });
}

/**
 * Detect a form-submission setup sequence at the start of recorded actions.
 * Purely structural — no hardcoded keywords. Finds a block of fill/type/click
 * actions near the start containing ≥2 fills, ending with a click (submit).
 * The LLM decides what it is (login, registration, etc.) from context.
 */
export function detectLoginPattern(actions) {
  if (!actions || actions.length < 3) return null;

  let start = 0;
  while (start < actions.length && actions[start].type === 'navigate') start++;
  if (start >= actions.length) return null;

  let lastFillIdx = -1;
  let fillCount = 0;
  let i = start;
  while (i < actions.length) {
    const t = actions[i].type;
    if (t === 'fill' || t === 'type') {
      fillCount++;
      lastFillIdx = i;
    } else if (t !== 'click') {
      break;
    }
    i++;
  }

  if (fillCount < 2 || lastFillIdx < 0) return null;

  let submitIdx = -1;
  for (let j = lastFillIdx + 1; j < actions.length; j++) {
    if (actions[j].type === 'click') { submitIdx = j; break; }
    if (actions[j].type !== 'fill' && actions[j].type !== 'type') break;
  }
  if (submitIdx < 0) return null;

  const remaining = actions.length - submitIdx - 1;
  if (remaining < 1) return null;

  return {
    startIndex: 0,
    endIndex: submitIdx,
    fillCount,
    actions: actions.slice(0, submitIdx + 1),
  };
}

/** Format a single recorded event into a human-readable action line with stableId and fallback selector. */
export function formatAction(event, index, selectorMap) {
  const num = index + 1;
  const reason = event.reasoning ? ` | reason: "${event.reasoning}"` : '';
  const hasStableId = event.stableId && event.stableId !== 'undefined';

  if (event._dupOf !== undefined) {
    const dupHint = `[DUPLICATE_STABLE_ID — same as step ${event._dupOf + 1}, use fallback]`;
    const fallback = findFallbackSelector((event.element || '').toLowerCase(), selectorMap, event.type);
    const textFb = !fallback ? (() => { const h = extractTextHint(event.element); return h ? ` | fallback: getByText('${h}')` : ''; })() : '';
    switch (event.type) {
      case 'click':
        return `${num}. CLICK ${dupHint} - ${event.element || 'element'}${fallback || textFb}${reason}`;
      case 'fill': case 'type':
        return `${num}. FILL ${dupHint} with "${event.value}" - ${event.element || 'field'}${fallback || textFb}${reason}`;
      default:
        return `${num}. ${event.type} ${dupHint} ${event.value || ''}${fallback || textFb}${reason}`;
    }
  }

  const fallback = findFallbackSelector((event.element || '').toLowerCase(), selectorMap, event.type);
  const idPart = hasStableId ? `[stableId="${event.stableId}"]` : '[NO_STABLE_ID]';

  let textFallback = '';
  if (!hasStableId && !fallback) {
    const hint = extractTextHint(event.element);
    if (hint) textFallback = ` | fallback: getByText('${hint}')`;
  }

  switch (event.type) {
    case 'navigate':
      return `${num}. NAVIGATE to: ${event.value}${reason}`;
    case 'click':
      return `${num}. CLICK ${idPart} - ${event.element || 'element'}${fallback || textFallback}${reason}`;
    case 'fill': case 'type':
      return `${num}. FILL ${idPart} with "${event.value}" - ${event.element || 'field'}${fallback || textFallback}${reason}`;
    case 'select': case 'select_option':
      return `${num}. SELECT ${idPart} option "${event.value}" - ${event.element || 'dropdown'}${fallback || textFallback}${reason}`;
    default:
      return `${num}. ${event.type} ${hasStableId ? idPart : ''} ${event.value || ''}${fallback || textFallback}${reason}`;
  }
}

/**
 * Build the full RECORDED ACTIONS prompt block by loading events.json
 * and merging with AI-captured selectors from execution output.
 * Auto-deduplicates stableIds before formatting.
 */
export function formatRecordedActions(sessionPath, executionActions) {
  let recorded = loadRecordedActions(sessionPath);
  if (recorded.length === 0) return '';
  recorded = deduplicateStableIds(recorded);
  const selectorMap = buildSelectorMap(executionActions);
  return `RECORDED ACTIONS:\n${recorded.map((e, i) => formatAction(e, i, selectorMap)).join('\n')}\n\nGenerate a CLEAN script from these — not a 1:1 replay. If stableId fails, use the fallback selector.`;
}

/**
 * Format a prompt hint when a setup sequence (form submission) is detected
 * at the start of the recorded actions. Tells the LLM to check for or
 * create a shared fixture — no prescriptive file names or code patterns.
 */
export function formatSetupHint(setup) {
  if (!setup) return '';
  return `
SETUP SEQUENCE DETECTED (steps ${setup.startIndex + 1}–${setup.endIndex + 1}, ${setup.fillCount} form fields):
The first ${setup.endIndex + 1} actions are a form-submission sequence that will repeat across tests.
BEFORE writing ANY new files, you MUST search the codebase:
1. Search tests/ for existing fixture, helper, or setup files (e.g. grep for "export.*function" in tests/)
2. Read any found files to see what they export
3. If an existing function already handles this setup → import and reuse it, skip steps ${setup.startIndex + 1}–${setup.endIndex + 1}
4. ONLY create a new fixture if nothing existing covers these steps
DO NOT create a duplicate fixture. Reuse what exists.
`;
}

/** Format preflight assertions into a simple numbered checklist for the execute_live prompt. */
export function formatAssertionChecklist(assertions) {
  if (!assertions?.length) return '';
  return assertions.map((a, i) =>
    `${i + 1}. ${a.description} → expected: ${a.expected}`
  ).join('\n');
}

/**
 * Merge preflight assertion definitions with execution results (pass/fail/evidence)
 * into an ASSERTIONS TO IMPLEMENT block for the generate_script prompt.
 */
export function formatAssertionsWithResults(preflightAssertions, executionAssertions, notes, finalUrl) {
  if (!preflightAssertions?.length && !executionAssertions?.length) return '';

  const lines = preflightAssertions?.length > 0
    ? preflightAssertions.map((a, i) => {
        const exec = executionAssertions?.[i];
        const status = exec ? (exec.passed ? '✅ PASSED' : '❌ FAILED') : '⚠️ NOT CHECKED';
        const evidence = exec?.evidence ? ` | evidence: "${exec.evidence}"` : '';
        return `${i + 1}. [${status}] ${a.description} → expected: ${a.expected}${evidence}`;
      })
    : executionAssertions.map((a, i) =>
        typeof a === 'object'
          ? `${i + 1}. ${a.description || a.type}: expected "${a.expected}", actual "${a.actual}"`
          : `${i + 1}. ${a}`
      );

  return `ASSERTIONS TO IMPLEMENT:
${lines.join('\n')}

Final URL: ${finalUrl || 'unknown'}${notes ? `\nAI OBSERVATION: ${notes}` : ''}`;
}
