/**
 * Meta commands — tabs, server control, screenshots, chain, diff, snapshot
 */

import type { BrowserManager } from './browser-manager';
import { handleSnapshot } from './snapshot';
import { getCleanText } from './read-commands';
import { READ_COMMANDS, WRITE_COMMANDS, META_COMMANDS, PAGE_CONTENT_COMMANDS, wrapUntrustedContent } from './commands';
import { validateNavigationUrl } from './url-validation';
import * as Diff from 'diff';
import * as fs from 'fs';
import * as path from 'path';
import { TEMP_DIR, isPathWithin } from './platform';
import { resolveConfig } from './config';
import type { Frame } from 'playwright';

// Security: Path validation to prevent path traversal attacks
const SAFE_DIRECTORIES = [TEMP_DIR, process.cwd()];

export function validateOutputPath(filePath: string): void {
  const resolved = path.resolve(filePath);
  const isSafe = SAFE_DIRECTORIES.some(dir => isPathWithin(resolved, dir));
  if (!isSafe) {
    throw new Error(`Path must be within: ${SAFE_DIRECTORIES.join(', ')}`);
  }
}

/** Tokenize a pipe segment respecting double-quoted strings. */
function tokenizePipeSegment(segment: string): string[] {
  const tokens: string[] = [];
  let current = '';
  let inQuote = false;
  for (let i = 0; i < segment.length; i++) {
    const ch = segment[i];
    if (ch === '"') {
      inQuote = !inQuote;
    } else if (ch === ' ' && !inQuote) {
      if (current) { tokens.push(current); current = ''; }
    } else {
      current += ch;
    }
  }
  if (current) tokens.push(current);
  return tokens;
}

export async function handleMetaCommand(
  command: string,
  args: string[],
  bm: BrowserManager,
  shutdown: () => Promise<void> | void
): Promise<string> {
  switch (command) {
    // ─── Tabs ──────────────────────────────────────────
    case 'tabs': {
      const tabs = await bm.getTabListWithTitles();
      return tabs.map(t =>
        `${t.active ? '→ ' : '  '}[${t.id}] ${t.title || '(untitled)'} — ${t.url}`
      ).join('\n');
    }

    case 'tab': {
      const id = parseInt(args[0], 10);
      if (isNaN(id)) throw new Error('Usage: browse tab <id>');
      bm.switchTab(id);
      return `Switched to tab ${id}`;
    }

    case 'newtab': {
      const url = args[0];
      const id = await bm.newTab(url);
      return `Opened tab ${id}${url ? ` → ${url}` : ''}`;
    }

    case 'closetab': {
      const id = args[0] ? parseInt(args[0], 10) : undefined;
      await bm.closeTab(id);
      return `Closed tab${id ? ` ${id}` : ''}`;
    }

    // ─── Server Control ────────────────────────────────
    case 'status': {
      const page = bm.getPage();
      const tabs = bm.getTabCount();
      const mode = bm.getConnectionMode();
      return [
        `Status: healthy`,
        `Mode: ${mode}`,
        `URL: ${page.url()}`,
        `Tabs: ${tabs}`,
        `PID: ${process.pid}`,
      ].join('\n');
    }

    case 'url': {
      return bm.getCurrentUrl();
    }

    case 'stop': {
      await shutdown();
      return 'Server stopped';
    }

    case 'restart': {
      // Signal that we want a restart — the CLI will detect exit and restart
      console.log('[browse] Restart requested. Exiting for CLI to restart.');
      await shutdown();
      return 'Restarting...';
    }

    // ─── Visual ────────────────────────────────────────
    case 'screenshot': {
      // Parse priority: flags (--viewport, --clip) → selector (@ref, CSS) → output path
      const page = bm.getPage();
      let outputPath = `${TEMP_DIR}/browse-screenshot.png`;
      let clipRect: { x: number; y: number; width: number; height: number } | undefined;
      let targetSelector: string | undefined;
      let viewportOnly = false;

      const remaining: string[] = [];
      for (let i = 0; i < args.length; i++) {
        if (args[i] === '--viewport') {
          viewportOnly = true;
        } else if (args[i] === '--clip') {
          const coords = args[++i];
          if (!coords) throw new Error('Usage: screenshot --clip x,y,w,h [path]');
          const parts = coords.split(',').map(Number);
          if (parts.length !== 4 || parts.some(isNaN))
            throw new Error('Usage: screenshot --clip x,y,width,height — all must be numbers');
          clipRect = { x: parts[0], y: parts[1], width: parts[2], height: parts[3] };
        } else if (args[i].startsWith('--')) {
          throw new Error(`Unknown screenshot flag: ${args[i]}`);
        } else {
          remaining.push(args[i]);
        }
      }

      // Separate target (selector/@ref) from output path
      for (const arg of remaining) {
        // File paths containing / and ending with an image/pdf extension are never CSS selectors
        const isFilePath = arg.includes('/') && /\.(png|jpe?g|webp|pdf)$/i.test(arg);
        if (isFilePath) {
          outputPath = arg;
        } else if (arg.startsWith('@e') || arg.startsWith('@c') || arg.startsWith('.') || arg.startsWith('#') || arg.includes('[')) {
          targetSelector = arg;
        } else {
          outputPath = arg;
        }
      }

      validateOutputPath(outputPath);

      if (clipRect && targetSelector) {
        throw new Error('Cannot use --clip with a selector/ref — choose one');
      }
      if (viewportOnly && clipRect) {
        throw new Error('Cannot use --viewport with --clip — choose one');
      }

      if (targetSelector) {
        const resolved = await bm.resolveRef(targetSelector);
        const locator = 'locator' in resolved ? resolved.locator : page.locator(resolved.selector);
        await locator.screenshot({ path: outputPath, timeout: 5000 });
        return `Screenshot saved (element): ${outputPath}`;
      }

      if (clipRect) {
        await page.screenshot({ path: outputPath, clip: clipRect });
        return `Screenshot saved (clip ${clipRect.x},${clipRect.y},${clipRect.width},${clipRect.height}): ${outputPath}`;
      }

      await page.screenshot({ path: outputPath, fullPage: !viewportOnly });
      return `Screenshot saved${viewportOnly ? ' (viewport)' : ''}: ${outputPath}`;
    }

    case 'pdf': {
      const page = bm.getPage();
      const pdfPath = args[0] || `${TEMP_DIR}/browse-page.pdf`;
      validateOutputPath(pdfPath);
      await page.pdf({ path: pdfPath, format: 'A4' });
      return `PDF saved: ${pdfPath}`;
    }

    case 'responsive': {
      const page = bm.getPage();
      const prefix = args[0] || `${TEMP_DIR}/browse-responsive`;
      validateOutputPath(prefix);
      const viewports = [
        { name: 'mobile', width: 375, height: 812 },
        { name: 'tablet', width: 768, height: 1024 },
        { name: 'desktop', width: 1280, height: 720 },
      ];
      const originalViewport = page.viewportSize();
      const results: string[] = [];

      for (const vp of viewports) {
        await page.setViewportSize({ width: vp.width, height: vp.height });
        const path = `${prefix}-${vp.name}.png`;
        await page.screenshot({ path, fullPage: true });
        results.push(`${vp.name} (${vp.width}x${vp.height}): ${path}`);
      }

      // Restore original viewport
      if (originalViewport) {
        await page.setViewportSize(originalViewport);
      }

      return results.join('\n');
    }

    // ─── Chain ─────────────────────────────────────────
    case 'chain': {
      // Read JSON array from args[0] (if provided) or expect it was passed as body
      const jsonStr = args[0];
      if (!jsonStr) throw new Error(
        'Usage: echo \'[["goto","url"],["text"]]\' | browse chain\n' +
        '   or: browse chain \'goto url | click @e5 | snapshot -ic\''
      );

      let commands: string[][];
      try {
        commands = JSON.parse(jsonStr);
        if (!Array.isArray(commands)) throw new Error('not array');
      } catch {
        // Fallback: pipe-delimited format "goto url | click @e5 | snapshot -ic"
        commands = jsonStr.split(' | ')
          .filter(seg => seg.trim().length > 0)
          .map(seg => tokenizePipeSegment(seg.trim()));
      }

      const results: string[] = [];
      const { handleReadCommand } = await import('./read-commands');
      const { handleWriteCommand } = await import('./write-commands');

      let lastWasWrite = false;
      for (const cmd of commands) {
        const [name, ...cmdArgs] = cmd;
        try {
          let result: string;
          if (WRITE_COMMANDS.has(name)) {
            result = await handleWriteCommand(name, cmdArgs, bm);
            lastWasWrite = true;
          } else if (READ_COMMANDS.has(name)) {
            result = await handleReadCommand(name, cmdArgs, bm);
            if (PAGE_CONTENT_COMMANDS.has(name)) {
              result = wrapUntrustedContent(result, bm.getCurrentUrl());
            }
            lastWasWrite = false;
          } else if (META_COMMANDS.has(name)) {
            result = await handleMetaCommand(name, cmdArgs, bm, shutdown);
            lastWasWrite = false;
          } else {
            throw new Error(`Unknown command: ${name}`);
          }
          results.push(`[${name}] ${result}`);
        } catch (err: any) {
          results.push(`[${name}] ERROR: ${err.message}`);
        }
      }

      // Wait for network to settle after write commands before returning
      if (lastWasWrite) {
        await bm.getPage().waitForLoadState('networkidle', { timeout: 2000 }).catch(() => {});
      }

      return results.join('\n\n');
    }

    // ─── Diff ──────────────────────────────────────────
    case 'diff': {
      const [url1, url2] = args;
      if (!url1 || !url2) throw new Error('Usage: browse diff <url1> <url2>');

      const page = bm.getPage();
      await validateNavigationUrl(url1);
      await page.goto(url1, { waitUntil: 'domcontentloaded', timeout: 15000 });
      const text1 = await getCleanText(page);

      await validateNavigationUrl(url2);
      await page.goto(url2, { waitUntil: 'domcontentloaded', timeout: 15000 });
      const text2 = await getCleanText(page);

      const changes = Diff.diffLines(text1, text2);
      const output: string[] = [`--- ${url1}`, `+++ ${url2}`, ''];

      for (const part of changes) {
        const prefix = part.added ? '+' : part.removed ? '-' : ' ';
        const lines = part.value.split('\n').filter(l => l.length > 0);
        for (const line of lines) {
          output.push(`${prefix} ${line}`);
        }
      }

      return wrapUntrustedContent(output.join('\n'), `diff: ${url1} vs ${url2}`);
    }

    // ─── Snapshot ─────────────────────────────────────
    case 'snapshot': {
      const snapshotResult = await handleSnapshot(args, bm);
      return wrapUntrustedContent(snapshotResult, bm.getCurrentUrl());
    }

    // ─── Handoff ────────────────────────────────────
    case 'handoff': {
      const message = args.join(' ') || 'User takeover requested';
      return await bm.handoff(message);
    }

    case 'resume': {
      bm.resume();
      // Re-snapshot to capture current page state after human interaction
      const snapshot = await handleSnapshot(['-i'], bm);
      return `RESUMED\n${wrapUntrustedContent(snapshot, bm.getCurrentUrl())}`;
    }

    // ─── Headed Mode ──────────────────────────────────────
    case 'connect': {
      // connect is handled as a pre-server command in cli.ts
      // If we get here, server is already running — tell the user
      if (bm.getConnectionMode() === 'headed') {
        return 'Already in headed mode with extension.';
      }
      return 'The connect command must be run from the CLI (not sent to a running server). Run: $B connect';
    }

    case 'disconnect': {
      if (bm.getConnectionMode() !== 'headed') {
        return 'Not in headed mode — nothing to disconnect.';
      }
      // Signal that we want a restart in headless mode
      console.log('[browse] Disconnecting headed browser. Restarting in headless mode.');
      await shutdown();
      return 'Disconnected. Server will restart in headless mode on next command.';
    }

    case 'focus': {
      if (bm.getConnectionMode() !== 'headed') {
        return 'focus requires headed mode. Run `$B connect` first.';
      }
      try {
        const { execSync } = await import('child_process');
        // Try common Chromium-based browser app names to bring to foreground
        const appNames = ['Comet', 'Google Chrome', 'Arc', 'Brave Browser', 'Microsoft Edge'];
        let activated = false;
        for (const appName of appNames) {
          try {
            execSync(`osascript -e 'tell application "${appName}" to activate'`, { stdio: 'pipe', timeout: 3000 });
            activated = true;
            break;
          } catch {
            // Try next browser
          }
        }

        if (!activated) {
          return 'Could not bring browser to foreground. macOS only.';
        }

        // If a ref was passed, scroll it into view
        if (args.length > 0 && args[0].startsWith('@')) {
          try {
            const resolved = await bm.resolveRef(args[0]);
            if ('locator' in resolved) {
              await resolved.locator.scrollIntoViewIfNeeded({ timeout: 5000 });
              return `Browser activated. Scrolled ${args[0]} into view.`;
            }
          } catch {
            // Ref not found — still activated the browser
          }
        }

        return 'Browser window activated.';
      } catch (err: any) {
        return `focus failed: ${err.message}. macOS only.`;
      }
    }

    // ─── Watch ──────────────────────────────────────────
    case 'watch': {
      if (args[0] === 'stop') {
        if (!bm.isWatching()) return 'Not currently watching.';
        const result = bm.stopWatch();
        const durationSec = Math.round(result.duration / 1000);
        const lastSnapshot = result.snapshots.length > 0
          ? wrapUntrustedContent(result.snapshots[result.snapshots.length - 1], bm.getCurrentUrl())
          : '(none)';
        return [
          `WATCH STOPPED (${durationSec}s, ${result.snapshots.length} snapshots)`,
          '',
          'Last snapshot:',
          lastSnapshot,
        ].join('\n');
      }

      if (bm.isWatching()) return 'Already watching. Run `$B watch stop` to stop.';
      if (bm.getConnectionMode() !== 'headed') {
        return 'watch requires headed mode. Run `$B connect` first.';
      }

      bm.startWatch();
      return 'WATCHING — observing user browsing. Periodic snapshots every 5s.\nRun `$B watch stop` to stop and get summary.';
    }

    // ─── Inbox ──────────────────────────────────────────
    case 'inbox': {
      const { execSync } = await import('child_process');
      let gitRoot: string;
      try {
        gitRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
      } catch {
        return 'Not in a git repository — cannot locate inbox.';
      }

      const inboxDir = path.join(gitRoot, '.context', 'sidebar-inbox');
      if (!fs.existsSync(inboxDir)) return 'Inbox empty.';

      const files = fs.readdirSync(inboxDir)
        .filter(f => f.endsWith('.json') && !f.startsWith('.'))
        .sort()
        .reverse(); // newest first

      if (files.length === 0) return 'Inbox empty.';

      const messages: { timestamp: string; url: string; userMessage: string }[] = [];
      for (const file of files) {
        try {
          const data = JSON.parse(fs.readFileSync(path.join(inboxDir, file), 'utf-8'));
          messages.push({
            timestamp: data.timestamp || '',
            url: data.page?.url || 'unknown',
            userMessage: data.userMessage || '',
          });
        } catch {
          // Skip malformed files
        }
      }

      if (messages.length === 0) return 'Inbox empty.';

      const lines: string[] = [];
      lines.push(`SIDEBAR INBOX (${messages.length} message${messages.length === 1 ? '' : 's'})`);
      lines.push('────────────────────────────────');

      for (const msg of messages) {
        const ts = msg.timestamp ? `[${msg.timestamp}]` : '[unknown]';
        lines.push(`${ts} ${msg.url}`);
        lines.push(`  "${msg.userMessage}"`);
        lines.push('');
      }

      lines.push('────────────────────────────────');

      // Handle --clear flag
      if (args.includes('--clear')) {
        for (const file of files) {
          try { fs.unlinkSync(path.join(inboxDir, file)); } catch {}
        }
        lines.push(`Cleared ${files.length} message${files.length === 1 ? '' : 's'}.`);
      }

      return lines.join('\n');
    }

    // ─── State ────────────────────────────────────────
    case 'state': {
      const [action, name] = args;
      if (!action || !name) throw new Error('Usage: state save|load <name>');

      // Sanitize name: alphanumeric + hyphens + underscores only
      if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
        throw new Error('State name must be alphanumeric (a-z, 0-9, _, -)');
      }

      const config = resolveConfig();
      const stateDir = path.join(config.stateDir, 'browse-states');
      fs.mkdirSync(stateDir, { recursive: true });
      const statePath = path.join(stateDir, `${name}.json`);

      if (action === 'save') {
        const state = await bm.saveState();
        // V1: cookies + URLs only (not localStorage — breaks on load-before-navigate)
        const saveData = {
          version: 1,
          savedAt: new Date().toISOString(),
          cookies: state.cookies,
          pages: state.pages.map(p => ({ url: p.url, isActive: p.isActive })),
        };
        fs.writeFileSync(statePath, JSON.stringify(saveData, null, 2), { mode: 0o600 });
        return `State saved: ${statePath} (${state.cookies.length} cookies, ${state.pages.length} pages)\n⚠️  Cookies stored in plaintext. Delete when no longer needed.`;
      }

      if (action === 'load') {
        if (!fs.existsSync(statePath)) throw new Error(`State not found: ${statePath}`);
        const data = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
        if (!Array.isArray(data.cookies) || !Array.isArray(data.pages)) {
          throw new Error('Invalid state file: expected cookies and pages arrays');
        }
        // Warn on state files older than 7 days
        if (data.savedAt) {
          const ageMs = Date.now() - new Date(data.savedAt).getTime();
          const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000;
          if (ageMs > SEVEN_DAYS) {
            console.warn(`[browse] Warning: State file is ${Math.round(ageMs / 86400000)} days old. Consider re-saving.`);
          }
        }
        // Close existing pages, then restore (replace, not merge)
        bm.setFrame(null);
        await bm.closeAllPages();
        await bm.restoreState({
          cookies: data.cookies,
          pages: data.pages.map((p: any) => ({ ...p, storage: null })),
        });
        return `State loaded: ${data.cookies.length} cookies, ${data.pages.length} pages`;
      }

      throw new Error('Usage: state save|load <name>');
    }

    // ─── Frame ───────────────────────────────────────
    case 'frame': {
      const target = args[0];
      if (!target) throw new Error('Usage: frame <selector|@ref|--name name|--url pattern|main>');

      if (target === 'main') {
        bm.setFrame(null);
        bm.clearRefs();
        return 'Switched to main frame';
      }

      const page = bm.getPage();
      let frame: Frame | null = null;

      if (target === '--name') {
        if (!args[1]) throw new Error('Usage: frame --name <name>');
        frame = page.frame({ name: args[1] });
      } else if (target === '--url') {
        if (!args[1]) throw new Error('Usage: frame --url <pattern>');
        frame = page.frame({ url: new RegExp(args[1]) });
      } else {
        // CSS selector or @ref for the iframe element
        const resolved = await bm.resolveRef(target);
        const locator = 'locator' in resolved ? resolved.locator : page.locator(resolved.selector);
        const elementHandle = await locator.elementHandle({ timeout: 5000 });
        frame = await elementHandle?.contentFrame() ?? null;
        await elementHandle?.dispose();
      }

      if (!frame) throw new Error(`Frame not found: ${target}`);
      bm.setFrame(frame);
      bm.clearRefs();
      return `Switched to frame: ${frame.url()}`;
    }

    default:
      throw new Error(`Unknown meta command: ${command}`);
  }
}
