/**
 * BrowserTestResultHandler - Browser test automation post-processing.
 * Extends core ResultHandler with event enrichment, video handling,
 * and assertion → event ID resolution.
 */

import {
  writeFileSync,
  existsSync,
  readFileSync,
  statSync,
  copyFileSync,
  mkdirSync,
} from 'fs';
import { join, isAbsolute, normalize } from 'path';
import { spawnSync } from 'child_process';
import { ResultHandler, logger } from '@zibby/core';

export class BrowserTestResultHandler extends ResultHandler {

  static async onNodeSaved(nodeFolder, executionData) {
    this.enrichEvents(nodeFolder, executionData);
    const videoPath = join(nodeFolder, 'recording.webm');
    if (existsSync(videoPath)) {
      this.recalculateEventTimestamps(nodeFolder, videoPath);
    }
  }

  // ── Video ──────────────────────────────────────────────────

  static getVideoDurationMs(videoPath) {
    try {
      const { stdout: result } = spawnSync('ffprobe', [
        '-v', 'error', '-show_entries', 'format=duration',
        '-of', 'default=noprint_wrappers=1:nokey=1', videoPath
      ], { encoding: 'utf-8', timeout: 5000 });
      const durationSec = parseFloat(result.trim());
      if (!isNaN(durationSec)) {
        const durationMs = Math.round(durationSec * 1000);
        logger.debug(`Video duration (ffprobe): ${durationMs}ms`);
        return durationMs;
      }
    } catch (_e) { /* ffprobe not available */ }

    try {
      const stats = statSync(videoPath);
      const durationMs = Math.floor(stats.mtimeMs - stats.birthtimeMs);
      if (durationMs > 0 && durationMs < 600000) {
        logger.debug(`Video duration (file stats): ${durationMs}ms`);
        return durationMs;
      }
    } catch (e) {
      console.warn(`⚠️  Could not determine video duration: ${e.message}`);
    }

    return null;
  }

  static recalculateEventTimestamps(folder, videoPath) {
    try {
      const eventsPath = join(folder, 'events.json');
      if (!existsSync(eventsPath)) return;

      const events = JSON.parse(readFileSync(eventsPath, 'utf-8'));
      if (events.length === 0) return;

      const videoDurationMs = this.getVideoDurationMs(videoPath);
      if (!videoDurationMs) {
        console.warn('⚠️  Could not get video duration');
        return;
      }

      const closeEvent = events.find(e => e.type === 'close');
      if (!closeEvent) {
        console.warn('⚠️  No close event found');
        return;
      }

      const closeMs = new Date(closeEvent.timestamp).getTime();
      const videoStartMs = closeMs - videoDurationMs;
      let realVideoStartMs = videoStartMs;

      try {
        const videoStats = statSync(videoPath);
        const fileBirthMs = videoStats.birthtimeMs;

        logger.info('Video file timing:');
        logger.info(`  File created: ${new Date(fileBirthMs).toISOString()}`);
        logger.info(`  Close event: ${new Date(closeMs).toISOString()}`);
        logger.info(`  Duration: ${videoDurationMs}ms`);
        logger.info(`  Calculated start: ${new Date(videoStartMs).toISOString()}`);

        const birthToClose = closeMs - fileBirthMs;
        if (birthToClose > 0 && birthToClose <= videoDurationMs + 5000) {
          realVideoStartMs = fileBirthMs;
        } else {
          logger.debug('File birth unreliable, using calculated start');
        }
      } catch (e) {
        logger.debug(`Using calculated video start: ${new Date(videoStartMs).toISOString()}`);
        logger.debug(`Error: ${e.message}`);
      }

      events.forEach(event => {
        const eventMs = new Date(event.timestamp).getTime();
        const offsetMs = eventMs - realVideoStartMs;
        event.videoOffsetMs = Math.max(0, Math.round(offsetMs));
        event.videoOffsetFormatted = this._formatTime(event.videoOffsetMs);
      });

      writeFileSync(eventsPath, JSON.stringify(events, null, 2));
      logger.info(`Recalculated ${events.length} events`);
    } catch (err) {
      console.warn(`[INFO] Could not recalculate timestamps: ${err.message}`);
    }
  }

  static _formatTime(ms) {
    const totalMs = Math.round(ms);
    const totalSeconds = Math.floor(totalMs / 1000);
    const hours = Math.floor(totalSeconds / 3600);
    const minutes = Math.floor((totalSeconds % 3600) / 60);
    const seconds = totalSeconds % 60;
    const milliseconds = totalMs % 1000;
    return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}.${String(milliseconds).padStart(3, '0')}`;
  }

  // ── Event enrichment + assertion resolution ────────────────

  static enrichEvents(executeLiveFolder, executionData) {
    try {
      const eventsPath = join(executeLiveFolder, 'events.json');
      if (!existsSync(eventsPath)) return;

      let events = JSON.parse(readFileSync(eventsPath, 'utf-8'));
      const aiActions = executionData.actions || [];
      const aiSteps = executionData.steps || [];

      logger.info(`Event Enrichment: ${events.length} recorded events, ${aiActions.length} AI actions`);
      logger.info(`Recorded event types: ${events.map(e => e.type).join(', ')}`);
      logger.info(`AI action types: ${aiActions.map(a => a.type).join(', ')}`);

      const usedActionIndices = new Set();

      events = events.map((event) => {
        const actionableTypes = ['navigate', 'click', 'fill', 'type', 'select', 'keypress', 'hover', 'drag'];

        if (actionableTypes.includes(event.type)) {
          let bestMatch = null;
          let bestScore = 0;

          aiActions.forEach((action, actionIndex) => {
            if (usedActionIndices.has(actionIndex)) return;

            let score = 0;
            const typeMatches =
              action.type === event.type ||
              (action.type === 'fill' && (event.type === 'fill' || event.type === 'type')) ||
              (action.type === 'type' && (event.type === 'fill' || event.type === 'type'));

            if (typeMatches) {
              score += 10;
              if (actionIndex === usedActionIndices.size) score += 5;

              if (event.data?.params?.element && action.description) {
                const elementStr = String(event.data.params.element);
                if (action.description.toLowerCase().includes(elementStr.toLowerCase().slice(0, 10))) {
                  score += 3;
                }
              }

              if (score > bestScore) {
                bestScore = score;
                bestMatch = { action, actionIndex };
              }
            }
          });

          if (bestMatch) {
            event.description = bestMatch.action.description;
            event.reasoning = bestMatch.action.reasoning;
            event.matchedActionIndex = bestMatch.actionIndex;
            usedActionIndices.add(bestMatch.actionIndex);

            if (aiSteps.length > 0) {
              const matchingStep = aiSteps.find(s => {
                if (typeof s !== 'string') return false;
                const sLower = s.toLowerCase();
                return sLower.includes(event.type) ||
                  (event.data?.params?.element && sLower.includes(String(event.data.params.element).toLowerCase()));
              });
              if (matchingStep) event.step = matchingStep;
            }
          } else {
            const toolName = event.data?.tool || 'action';
            event.description = `${toolName} action`;
            event.reasoning = 'Browser interaction';
          }
        } else if (event.type === 'screenshot') {
          const evidenceScreenshot = executionData.evidenceScreenshot;
          if (evidenceScreenshot) {
            event.description = evidenceScreenshot.description;
            event.verdict = evidenceScreenshot.verdict;
            event.isEvidence = true;
            event.reasoning = `Evidence of test ${evidenceScreenshot.verdict === 'pass' ? 'passing' : 'failing'}`;
            logger.debug(`Enriched evidence screenshot: ${evidenceScreenshot.verdict.toUpperCase()}`);
          }
        }

        return event;
      });

      logger.info(`Matching results: ${usedActionIndices.size}/${aiActions.length} AI actions used`);

      writeFileSync(eventsPath, JSON.stringify(events, null, 2), 'utf-8');
      logger.info(`Enriched ${events.length} events with AI descriptions and reasoning`);

      // Resolve assertion verifiedAfterAction (action index) -> verifiedAtEventId (event id)
      const assertions = executionData.assertions || [];
      if (assertions.length > 0) {
        const actionToEvent = new Map();
        for (const event of events) {
          if (event.matchedActionIndex !== undefined) {
            actionToEvent.set(event.matchedActionIndex, event.id);
          }
        }

        for (const assertion of assertions) {
          if (assertion.verifiedAfterAction === undefined) continue;

          const exactEvent = actionToEvent.get(assertion.verifiedAfterAction);
          if (exactEvent !== undefined) {
            assertion.verifiedAtEventId = exactEvent;
            continue;
          }

          let bestActionIdx = -1;
          for (const [actionIdx] of actionToEvent) {
            if (actionIdx <= assertion.verifiedAfterAction && actionIdx > bestActionIdx) {
              bestActionIdx = actionIdx;
            }
          }
          if (bestActionIdx >= 0) {
            assertion.verifiedAtEventId = actionToEvent.get(bestActionIdx);
          }
        }

        const resultPath = join(executeLiveFolder, 'result.json');
        if (existsSync(resultPath)) {
          const resultData = JSON.parse(readFileSync(resultPath, 'utf-8'));
          resultData.assertions = assertions;
          writeFileSync(resultPath, JSON.stringify(resultData, null, 2), 'utf-8');
          logger.info(`Resolved verifiedAtEventId for ${assertions.filter(a => a.verifiedAtEventId !== undefined).length}/${assertions.length} assertions`);
        }
      }
    } catch (err) {
      console.warn('⚠️  Could not enrich events:', err.message);
    }
  }

  /**
   * Copy the final Playwright/Selenium file into `session/generate_script/` under stable names
   * so Studio (Electron + web bridge) can load scripts without resolving arbitrary `scriptPath`.
   * Safe to run even when the agent already wrote into that folder.
   */
  static ensureStudioCodegenMirror(sessionRoot, cwd) {
    if (!sessionRoot || typeof sessionRoot !== 'string') return;
    const gsDir = join(sessionRoot, 'generate_script');
    const resultPath = join(gsDir, 'result.json');
    if (!existsSync(resultPath)) return;

    let data;
    try {
      data = JSON.parse(readFileSync(resultPath, 'utf-8'));
    } catch {
      return;
    }
    const raw = data?.scriptPath;
    if (!raw || typeof raw !== 'string' || !raw.trim()) return;

    const t = raw.trim();
    const candidates = [];
    if (isAbsolute(t)) candidates.push(normalize(t));
    if (cwd && typeof cwd === 'string') candidates.push(join(cwd, t));
    candidates.push(join(sessionRoot, t));
    candidates.push(join(gsDir, t));

    let src = null;
    for (const c of candidates) {
      try {
        if (existsSync(c) && statSync(c).isFile()) {
          src = c;
          break;
        }
      } catch {
        /* ignore */
      }
    }
    if (!src) {
      logger.debug(`[Studio] Codegen mirror: could not resolve scriptPath "${t}"`);
      return;
    }

    const lower = src.toLowerCase();
    const destName = lower.endsWith('.py')
      ? 'test.selenium.py'
      : lower.endsWith('.ts') || lower.endsWith('.tsx')
        ? 'playwright.spec.ts'
        : 'generated-test.spec.js';
    const dest = join(gsDir, destName);

    try {
      if (existsSync(dest)) {
        try {
          if (statSync(dest).mtimeMs >= statSync(src).mtimeMs) return;
        } catch {
          /* replace if unsure */
        }
      }
      mkdirSync(gsDir, { recursive: true });
      copyFileSync(src, dest);
      logger.info(`[Studio] Mirrored script for discovery: ${dest}`);
    } catch (e) {
      logger.debug(`[Studio] Codegen mirror failed: ${e.message}`);
    }
  }
}
