#!/usr/bin/env npx tsx

import parseArgs from 'minimist';
import type { Page } from 'playwright-core';
import { launchBrowser } from './browse';

type Step =
  | { action: 'goto'; url: string }
  | { action: 'click'; selector?: string; text?: string; role?: string; name?: string }
  | { action: 'type'; selector?: string; text: string }
  | { action: 'press'; key: string; selector?: string }
  | { action: 'wait'; ms: number }
  | { action: 'screenshot'; path: string }
  | { action: 'extract'; selector: string; count?: number };

function normalizeKey(k: string): string {
  if (!k) return 'Enter';
  const lower = k.toLowerCase();
  if (lower === 'enter' || lower === 'return') return 'Enter';
  if (lower === 'tab') return 'Tab';
  if (lower === 'escape' || lower === 'esc') return 'Escape';
  return k;
}

function splitInstructions(instruction: string): string[] {
  return instruction
    .split(/\bthen\b|;/gi)
    .map((s) => s.trim())
    .filter(Boolean);
}

function parseInstruction(instruction: string): Step[] {
  const parts = splitInstructions(instruction);
  const steps: Step[] = [];

  for (const p of parts) {
    // go to https://...
    const goto = p.match(/^(?:go to|open|navigate to)\s+(https?:\/\/\S+)/i);
    if (goto) {
      steps.push({ action: 'goto', url: goto[1] });
      continue;
    }

    // click on "text" or click #selector or click button "name"
    const clickRole = p.match(/^click\s+(button|link|textbox|img|image|tab)\s+"([^"]+)"$/i);
    if (clickRole) {
      const role = clickRole[1].toLowerCase() === 'image' ? 'img' : clickRole[1].toLowerCase();
      steps.push({ action: 'click', role, name: clickRole[2] });
      continue;
    }
    const clickText = p.match(/^click(?: on)?\s+"([^"]+)"/i);
    if (clickText) {
      steps.push({ action: 'click', text: clickText[1] });
      continue;
    }
    const clickSelector = p.match(/^click(?: on)?\s+(#[\w-]+|\.[\w-]+|[a-z]+\[[^\]]+\])/i);
    if (clickSelector) {
      steps.push({ action: 'click', selector: clickSelector[1] });
      continue;
    }

    // type "text" [in selector]
    const typeInto = p.match(/^type\s+"([^"]+)"\s+in\s+(.+)$/i);
    if (typeInto) {
      steps.push({ action: 'type', text: typeInto[1], selector: typeInto[2].trim() });
      continue;
    }
    const typeOnly = p.match(/^type\s+"([^"]+)"$/i);
    if (typeOnly) {
      steps.push({ action: 'type', text: typeOnly[1] });
      continue;
    }

    // press enter [in selector]
    const pressIn = p.match(/^press\s+(\w+)\s+in\s+(.+)$/i);
    if (pressIn) {
      steps.push({ action: 'press', key: normalizeKey(pressIn[1]), selector: pressIn[2].trim() });
      continue;
    }
    const pressOnly = p.match(/^press\s+(\w+)$/i);
    if (pressOnly) {
      steps.push({ action: 'press', key: normalizeKey(pressOnly[1]) });
      continue;
    }

    // wait 2s / wait 500ms
    const waitS = p.match(/^wait\s+(\d+)\s*s(?:ec(?:onds?)?)?$/i);
    if (waitS) {
      steps.push({ action: 'wait', ms: parseInt(waitS[1], 10) * 1000 });
      continue;
    }
    const waitMs = p.match(/^wait\s+(\d+)\s*ms$/i);
    if (waitMs) {
      steps.push({ action: 'wait', ms: parseInt(waitMs[1], 10) });
      continue;
    }

    // screenshot path
    const shot = p.match(/^screenshot(?: to)?\s+(.+)$/i);
    if (shot) {
      steps.push({ action: 'screenshot', path: shot[1].trim() });
      continue;
    }

    throw new Error(`Could not parse step: "${p}"`);
  }

  return steps;
}

function escapeRegExp(value: string): string {
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

function isLikelyLoginText(text: string): boolean {
  return /(login|accedi|sign\s*in|entra)/i.test(text);
}

async function clickByText(page: Page, text: string): Promise<boolean> {
  const patterns = [new RegExp(`^${escapeRegExp(text)}$`, 'i'), new RegExp(escapeRegExp(text), 'i')];

  for (const pattern of patterns) {
    const targets = [
      page.getByRole('button', { name: pattern }).first(),
      page.getByRole('link', { name: pattern }).first(),
      page.getByText(pattern).first(),
    ];

    for (const target of targets) {
      if (await target.count()) {
        try {
          await target.click({ timeout: 8000 });
          return true;
        } catch {
          // keep trying next candidate
        }
      }
    }
  }

  return false;
}

async function fallbackLoginNavigation(page: Page, requestedText: string): Promise<boolean> {
  if (!isLikelyLoginText(requestedText)) return false;

  const current = new URL(page.url());

  const candidateLinks = await page.evaluate(() => {
    const loginTerms = ['login', 'accedi', 'sign in', 'entra'];
    const anchors = Array.from(document.querySelectorAll('a[href], a[onclick], button[onclick]')) as Array<HTMLAnchorElement | HTMLButtonElement>;

    return anchors
      .map((el) => {
        const text = (el.textContent || '').trim().toLowerCase();
        const href = (el as HTMLAnchorElement).getAttribute('href') || '';
        return { text, href };
      })
      .filter((x) => x.text && loginTerms.some((t) => x.text.includes(t)))
      .map((x) => x.href)
      .filter(Boolean);
  });

  // Prefer real URLs (not javascript:)
  const realCandidate = candidateLinks.find((h) => /login|account\/login/i.test(h) && !h.startsWith('javascript:'));
  if (realCandidate) {
    const target = new URL(realCandidate, page.url()).toString();
    await page.goto(target, { waitUntil: 'domcontentloaded', timeout: 60000 });
    return true;
  }

  // Site-specific fallback for Corriere
  if (/corriere\.it$/i.test(current.hostname) || /\.corriere\.it$/i.test(current.hostname)) {
    await page.goto('https://www.corriere.it/account/login', {
      waitUntil: 'domcontentloaded',
      timeout: 60000,
    });
    return true;
  }

  return false;
}

async function typeInBestTarget(page: Page, text: string, selector?: string) {
  if (selector) {
    await page.locator(selector).first().click({ timeout: 10000 });
    await page.locator(selector).first().fill(text);
    return;
  }
  const loc = page.locator('input[name="q"], input[type="search"], input[type="text"], textarea').first();
  await loc.click({ timeout: 10000 });
  await loc.fill(text);
}

async function pressOnTarget(page: Page, key: string, selector?: string) {
  if (selector) {
    await page.locator(selector).first().press(key);
    return;
  }
  await page.keyboard.press(key);
}

async function runSteps(page: Page, steps: Step[]) {
  for (const step of steps) {
    switch (step.action) {
      case 'goto':
        await page.goto(step.url, { waitUntil: 'domcontentloaded', timeout: 60000 });
        break;
      case 'click':
        if (step.selector) {
          await page.locator(step.selector).first().click({ timeout: 15000 });
        } else if (step.role && step.name) {
          await page.getByRole(step.role as any, { name: new RegExp(escapeRegExp(step.name), 'i') }).first().click({ timeout: 15000 });
        } else if (step.text) {
          const clicked = await clickByText(page, step.text);
          if (!clicked) {
            const recovered = await fallbackLoginNavigation(page, step.text);
            if (!recovered) {
              throw new Error(`Could not click target text: ${step.text}`);
            }
          }
        } else {
          throw new Error('click step missing selector/text/role');
        }
        try {
          await page.waitForLoadState('domcontentloaded', { timeout: 10000 });
        } catch {
          // no navigation is fine
        }
        break;
      case 'type':
        await typeInBestTarget(page, step.text, step.selector);
        break;
      case 'press':
        await pressOnTarget(page, step.key, step.selector);
        break;
      case 'wait':
        await page.waitForTimeout(step.ms);
        break;
      case 'screenshot':
        await page.screenshot({ path: step.path, fullPage: true });
        break;
      case 'extract': {
        const items = await page.locator(step.selector).allTextContents();
        const out = items.slice(0, step.count ?? items.length).map((t) => t.trim()).filter(Boolean);
        console.log(JSON.stringify(out, null, 2));
        break;
      }
      default:
        throw new Error('Unknown step');
    }
  }
}

async function main() {
  const args = parseArgs(process.argv.slice(2), {
    string: ['instruction', 'steps'],
    boolean: ['headless', 'help'],
    default: { headless: true },
    alias: { i: 'instruction', s: 'steps', h: 'help' },
  });

  if (args.help || (!args.instruction && !args.steps)) {
    console.log(`
General Web Flow Runner (CloakBrowser)

Usage:
  npx tsx flow.ts --instruction "go to https://example.com then type \"hello\" then press enter"
  npx tsx flow.ts --steps '[{"action":"goto","url":"https://example.com"}]'

Supported natural steps:
  - go to/open/navigate to <url>
  - click on "Text"
  - click <css-selector>
  - type "text"
  - type "text" in <css-selector>
  - press <key>
  - press <key> in <css-selector>
  - wait <N>s | wait <N>ms
  - screenshot <path>
`);
    process.exit(args.help ? 0 : 1);
  }

  const browser = await launchBrowser({ headless: args.headless });
  const page = await browser.newPage();

  try {
    const steps: Step[] = args.steps ? JSON.parse(args.steps) : parseInstruction(args.instruction);
    await runSteps(page, steps);
    console.log('Flow complete. Final URL:', page.url());
  } finally {
    await browser.close();
  }
}

main().catch((e) => {
  console.error('Error:', e instanceof Error ? e.message : e);
  process.exit(1);
});
