import { Logger } from '../shared/logger';
import { base64ToBlob, blobToBase64 } from '../shared/binary';
import type {
  Message,
  TransferredVideoSegment,
  VideoSegment,
  SessionMetadata,
  VideoBufferResponse
} from '../shared/types';
import JSZip from 'jszip';

// Default MIME applied when a wire chunk somehow lacks a type
// field (defense-in-depth: in normal operation the offscreen recorder
// always populates it from chunk.data.type). Matches D-20 strict codec.
const VIDEO_MIME_FALLBACK = 'video/webm;codecs=vp9';

const logger = new Logger('Main');

// Состояние
// Видеобуфер живёт в offscreen-документе (D-16). SW не хранит чанки локально:
// при экспорте он спрашивает буфер у offscreen через long-lived port (D-17).
let isRecording = false;
let offscreenCreated = false;
let lastScreenshotTime = 0;
let cachedScreenshot: Blob | null = null;
// Port from offscreen (D-17). Re-assigned on every (re)connect.
let videoPort: chrome.runtime.Port | null = null;
// Offscreen readiness Promise — set up at module load, resolved on first
// OFFSCREEN_READY message (Pattern 4). startVideoCapture awaits this before
// sending START_RECORDING, so we never lose the popup's transient activation
// to a race with the offscreen bootstrap.
let offscreenReadyResolve: (() => void) | null = null;
let offscreenReady: Promise<void> = new Promise((res) => {
  offscreenReadyResolve = res;
});
// userEvents хранится только в content script
// Для архивации получаем его оттуда

// Ring-buffer helpers (header-pin + age-trim) and the buffer duration
// constant were removed in Plan 01-03 — the buffer now lives in
// src/offscreen/recorder.ts per D-16. Plan 05 completes the SW shrink:
// see deletions below.

// Создание offscreen документа
async function ensureOffscreen() {
  if (offscreenCreated) {
    logger.log('Offscreen already created');
    return;
  }

  try {
    const url = chrome.runtime.getURL('src/offscreen/index.html');
    logger.log('Creating offscreen document at:', url);

    await chrome.offscreen.createDocument({
      url: url,
      reasons: [chrome.offscreen.Reason.DISPLAY_MEDIA],
      justification: 'Continuous screen recording for operator session diagnostics'
    });
    offscreenCreated = true;
    logger.log('Offscreen document created successfully');
  } catch (error) {
    const msg = error instanceof Error ? error.message : String(error);
    if (msg.includes('already exists')) {
      offscreenCreated = true;
      logger.log('Offscreen document already exists');
    } else {
      logger.error('Failed to create offscreen document:', error);
      throw error;
    }
  }
}

// SW-side port host (D-17, RESEARCH.md Pattern 5). The offscreen opens this
// port on bootstrap and reconnects on disconnect. We use it for: (a)
// keepalive traffic (PING) — Chrome 110+ resets the SW idle timer on every
// port message; (b) on-demand REQUEST_BUFFER round-trip during SAVE_ARCHIVE.
chrome.runtime.onConnect.addListener((port) => {
  // T-1-04 mitigation: only accept ports from this extension
  if (port.name !== 'video-keepalive') {
    return;
  }
  if (port.sender?.id !== chrome.runtime.id) {
    logger.warn('Rejecting port with mismatched sender:', port.sender?.id);
    port.disconnect();
    return;
  }
  logger.log('Offscreen port connected');
  videoPort = port;
  // CR-02 fix: install a permanent onMessage sink on every accepted port.
  // Chrome 110+ resets the SW idle-timer on any inbound port message, BUT
  // in the field, behaviour has been observed to differ subtly when no
  // listener is attached at all — Chrome may skip the idle-timer reset
  // path entirely on unrouted messages. A no-op listener guarantees the
  // PING traffic is consumed and the timer reset is unconditional. The
  // per-request listener installed by `getVideoBufferFromOffscreen` still
  // handles BUFFER routing; this sink only drains PING and any unknown
  // traffic so it doesn't accumulate or surprise us later.
  port.onMessage.addListener((msg) => {
    if (
      typeof msg === 'object' &&
      msg !== null &&
      (msg as { type?: unknown }).type === 'PING'
    ) {
      // Explicit drain — silences "no listener" semantics in Chrome's
      // port-message dispatch and keeps the SW idle-timer reset reliable.
      return;
    }
    // Unknown traffic — drop silently (T-1-04 defense-in-depth).
    // BUFFER is routed by the per-request listener in
    // getVideoBufferFromOffscreen; that listener fires first when
    // attached, so this branch never observes BUFFER in practice.
  });
  port.onDisconnect.addListener(() => {
    logger.log('Offscreen port disconnected; offscreen will reconnect');
    videoPort = null;
  });
});

// 2 s budget covers the worst-case round-trip: offscreen base64-encodes
// up to ~15 chunks of ~100 KB each (~1.5 MB raw → ~2 MB base64) in
// well under 100 ms, post-message + JSON parse adds < 50 ms, leaving
// plenty of headroom. Bumping later is cheap if real-world recordings
// produce significantly larger buffers; today this is sufficient.
const BUFFER_FETCH_TIMEOUT_MS = 2_000;

async function getVideoBufferFromOffscreen(): Promise<VideoBufferResponse> {
  if (videoPort === null) {
    logger.warn('No offscreen port available; returning empty buffer');
    return { segments: [] };
  }
  const port = videoPort;
  return new Promise<VideoBufferResponse>((resolve) => {
    const timer = setTimeout(() => {
      port.onMessage.removeListener(handler);
      // Sweep #5 fix: surface the diagnostic when the timeout fires
      // because the port was replaced by a reconnect mid-request.
      // The OLD port (captured as `port`) has a dead listener; the
      // offscreen will encode-and-send on the NEW port but the
      // listener installed there belongs to a different
      // getVideoBufferFromOffscreen call (if any). Without this
      // diagnostic the operator sees a silent timeout that masquerades
      // as an offscreen-side problem. With it, the SW log shows the
      // reconnect timing was the proximate cause.
      const portReplaced = videoPort !== port;
      logger.warn(
        `Buffer fetch timed out after ${BUFFER_FETCH_TIMEOUT_MS} ms`,
        'port_replaced_during_fetch:', portReplaced,
      );
      resolve({ segments: [] });
    }, BUFFER_FETCH_TIMEOUT_MS);
    const handler = (msg: unknown) => {
      if (
        typeof msg === 'object' &&
        msg !== null &&
        (msg as { type?: unknown }).type === 'BUFFER'
      ) {
        clearTimeout(timer);
        port.onMessage.removeListener(handler);
        // D-12 wire format + D-13 segment lifecycle: payload arrives
        // as TransferredVideoSegment[] (base64 string + MIME). Decode
        // each entry back into a VideoSegment — each is a
        // self-contained ~10 s WebM (EBML header + seed keyframe).
        // Concatenating them sequentially produces a multi-EBML-header
        // file Chrome plays natively. See src/shared/binary.ts +
        // RESEARCH.md Pattern 3.
        const wireSegments =
          (msg as { segments?: TransferredVideoSegment[] }).segments ?? [];
        // WR-07 fix: filter empty wire segments BEFORE base64 decode.
        // An empty wire.data would decode to a zero-byte Blob; the
        // SW-side mergeVideoSegments would then concat it into the
        // output WebM, producing a stray empty EBML segment that
        // breaks Chrome playback. We split into two passes (filter →
        // decode → filter-non-empty) so the iteration semantics stay
        // declarative (no early-return in the loop body).
        const nonEmptyWires = wireSegments.filter((wire) => {
          const isEmpty = !wire.data || wire.data.length === 0;
          if (isEmpty) {
            logger.warn(
              'Skipping empty wire segment (zero-length base64)',
              'timestamp:', wire.timestamp,
            );
          }
          return !isEmpty;
        });
        const segments: VideoSegment[] = nonEmptyWires.flatMap((wire) => {
          try {
            const blob = base64ToBlob(wire.data, wire.type || VIDEO_MIME_FALLBACK);
            if (blob.size === 0) {
              logger.warn(
                'Skipping segment that decoded to zero bytes',
                'timestamp:', wire.timestamp,
              );
              return [];
            }
            return [{ data: blob, timestamp: wire.timestamp }];
          } catch (err) {
            logger.warn(
              'base64ToBlob failed; skipping segment',
              'timestamp:', wire.timestamp,
              'error:', err,
            );
            return [];
          }
        });
        resolve({ segments });
      }
    };
    port.onMessage.addListener(handler);
    port.postMessage({ type: 'REQUEST_BUFFER' });
  });
}

// Начало записи видео
async function startVideoCapture() {
  if (isRecording) {
    logger.log('Video recording already active');
    return;
  }

  try {
    // Получаем активную вкладку
    const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });

    if (!tab.id || !tab.url) {
      throw new Error('No active tab found');
    }

    logger.log(`Starting video capture for tab ${tab.id}: ${tab.url}`);

    // Создаём offscreen документ (с reason из D-02)
    await ensureOffscreen();
    // Ждём, пока offscreen зарегистрирует свой onMessage listener
    // (RESEARCH.md Pattern 4). Иначе гонка: START_RECORDING летит раньше,
    // чем offscreen готов его принять, и Chrome бросает "Receiving end
    // does not exist".
    await offscreenReady;

    // Просим offscreen запустить запись — getDisplayMedia вызывается там
    // (D-01: больше нет SW-side stream-id юзаства).
    logger.log('Sending START_RECORDING to offscreen...');
    try {
      await chrome.runtime.sendMessage({
        type: 'START_RECORDING'
      });
      logger.log('START_RECORDING sent successfully');
    } catch (msgError) {
      logger.error('Failed to send START_RECORDING:', msgError);
      throw msgError;
    }

    isRecording = true;
    logger.log('Video recording started successfully');

  } catch (error) {
    logger.error('Failed to start video capture:', error);
    isRecording = false;
    throw error;
  }
}

// Keepalive теперь обеспечивается long-lived портом offscreen→SW (D-17/D-18).
// Старая alarms-based реализация удалена: alarm callbacks не сбрасывали SW idle
// timer (audit P1 #8), а порт сбрасывает таймер на каждое сообщение.

// Получение видеобуфера — port-based (getVideoBufferFromOffscreen объявлен
// выше). Старый синхронный SW-локальный буфер удалён в Task 1 этого плана.

// Получение скриншота активной вкладки
async function captureScreenshot(): Promise<Blob> {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });

  if (!tab.id) {
    throw new Error('No active tab');
  }

  // Проверяем кэш и лимит частоты (максимум 1 скриншот в 2 секунды)
  const now = Date.now();
  if (cachedScreenshot && (now - lastScreenshotTime) < 2000) {
    logger.log('Using cached screenshot');
    return cachedScreenshot;
  }

  // Делаем новый скриншот
  const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, { format: 'png' });

  // Конвертируем data URL в Blob
  const response = await fetch(dataUrl);
  cachedScreenshot = await response.blob();
  lastScreenshotTime = now;

  return cachedScreenshot;
}

// Склейка сегментов в один WebM-файл.
//
// Под D-13 каждый VideoSegment — это самодостаточный ~10-секундный WebM
// (собственный EBML-заголовок + seed keyframe). Сортируем по timestamp
// и склеиваем подряд: получаем multi-EBML-header файл, который Chrome
// проигрывает последовательно (см. SPEC §10 #7 — требуется проигрывание
// в Chrome, кросс-плейер совместимость вне scope Phase 1).
//
// Никакой логики «pin первого чанка» или header-retention больше нет
// — это снято вместе с D-09..D-11 и активацией D-13 в recorder.ts.
function mergeVideoSegments(segments: VideoSegment[]): Blob {
  logger.log(`Merging ${segments.length} segments`);

  // Сортируем по времени, чтобы сохранить правильный порядок (старшие
  // сегменты — раньше). Под D-13 порядок уже задаётся offscreen-стороной,
  // но сортируем оборонительно — стоимость copy+sort ничтожна.
  const sortedSegments = [...segments].sort((a, b) => a.timestamp - b.timestamp);

  logger.log(
    `Segments sorted, first timestamp: ${sortedSegments[0]?.timestamp}, ` +
      `last: ${sortedSegments[sortedSegments.length - 1]?.timestamp}`,
  );

  // Каждый сегмент уже валидный WebM — конкатенация безопасна.
  const blobs: Blob[] = sortedSegments.map((segment, index) => {
    logger.log(`Adding segment ${index}, size: ${segment.data.size} bytes`);
    return segment.data;
  });

  const finalBlob = new Blob(blobs, { type: 'video/webm' });
  logger.log(
    `Final video blob size: ${finalBlob.size} bytes, ` +
      `total segments merged: ${blobs.length}`,
  );

  return finalBlob;
}

// Создание архива
async function createArchive(
  videoBufferResponse: VideoBufferResponse,
  rrwebEvents: unknown[],
  userEvents: unknown[],
  screenshot: Blob
): Promise<Blob> {
  logger.log('=== Creating archive ===');
  logger.log(`Events to include: ${rrwebEvents.length} rrweb, ${userEvents.length} user`);

  const zip = new JSZip();

  // Добавляем видео (D-13: каждая запись — самодостаточный WebM-сегмент)
  if (videoBufferResponse.segments.length > 0) {
    const videoBlob = mergeVideoSegments(videoBufferResponse.segments);
    zip.file('video/last_30sec.webm', videoBlob);
    logger.log(`✓ Added video: ${videoBlob.size} bytes`);
  } else {
    logger.warn('✗ No video segments to add');
  }

  // Добавляем rrweb события
  const rrwebJson = JSON.stringify(rrwebEvents, null, 2);
  zip.file('rrweb/session.json', rrwebJson);
  if (rrwebEvents.length > 0) {
    logger.log(`✓ Added rrweb events: ${rrwebEvents.length} events, ${rrwebJson.length} bytes`);
  } else {
    logger.warn('✗ rrweb session.json created but EMPTY (content script may not be running)');
  }

  // Добавляем пользовательские события
  const eventsJson = JSON.stringify(userEvents, null, 2);
  zip.file('logs/events.json', eventsJson);
  if (userEvents.length > 0) {
    logger.log(`✓ Added user events: ${userEvents.length} events, ${eventsJson.length} bytes`);
  } else {
    logger.warn('✗ logs/events.json created but EMPTY (no user interactions detected)');
  }

  // Добавляем скриншот
  zip.file('screenshot.png', screenshot);
  logger.log('✓ Added screenshot');

  // Добавляем метаданные
  // IN-01 fix: read version from manifest at runtime instead of the
  // hardcoded '1.0.0'. Previously the metadata would silently lie about
  // the running version once manifest.json bumps to 1.0.1+. The Chrome
  // runtime API is always available in the SW context, so no fallback
  // is needed.
  const manifest = chrome.runtime.getManifest();
  const metadata: SessionMetadata = {
    timestamp: new Date().toISOString(),
    url: new URL(chrome.runtime.getURL('')).origin,
    userAgent: navigator.userAgent,
    extensionVersion: manifest.version,
    videoBufferSeconds: 30,
    logDurationMinutes: 10,
    totalEvents: rrwebEvents.length + userEvents.length
  };

  zip.file('meta.json', JSON.stringify(metadata, null, 2));
  logger.log('✓ Added metadata');

  // Генерируем архив
  const archiveBlob = await zip.generateAsync({ type: 'blob' });
  logger.log(`✓ Archive created: ${archiveBlob.size} bytes`);

  return archiveBlob;
}

// Скачивание архива
async function downloadArchive(archiveBlob: Blob) {
  const now = new Date();
  const dateStr = now.toISOString().replace(/[:.]/g, '-').split('T')[0];
  const timeStr = now.toTimeString().split(' ')[0].replace(/:/g, '-');

  const filename = `session_report_${dateStr}_${timeStr}.zip`;

  logger.log(`Downloading archive: ${filename} (${archiveBlob.size} bytes)`);

  // WR-08 fix: delegate to the shared `blobToBase64` helper instead of
  // re-implementing the same per-byte concat + btoa inline. Keeps the
  // wire-format encoding single-source-of-truth (also used by the
  // offscreen↔SW port; see src/shared/binary.ts) and ensures any future
  // performance work (chunked apply, etc.) propagates to both call sites.
  const base64 = await blobToBase64(archiveBlob);
  const url = `data:application/zip;base64,${base64}`;

  await chrome.downloads.download({
    url: url,
    filename: filename,
    saveAs: false
  });
  logger.log('Archive download started');
}

// Сохранение архива (полный процесс)
async function saveArchive() {
  try {
    logger.log('Starting archive save process');

    // Получаем текущую активную вкладку
    const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });

    if (!tab.id) {
      throw new Error('No active tab found');
    }

    logger.log(`Using tab ${tab.id} for archive, url: ${tab.url}`);

    // Получаем скриншот
    logger.log('Capturing screenshot...');
    const screenshot = await captureScreenshot();

    // Получаем видео буфер из offscreen через long-lived port (D-17)
    const videoBufferResp = await getVideoBufferFromOffscreen();
    logger.log(`Video buffer: ${videoBufferResp.segments.length} segments`);

    // Получаем rrweb события от content script
    logger.log(`Requesting rrweb events from content script on tab ${tab.id} (${tab.url})...`);

    let rrwebEvents: unknown[] = [];
    let userEvents: unknown[] = [];

    try {
      logger.log(`Sending GET_RRWEB_EVENTS message to tab ${tab.id}...`);
      const response: { events?: unknown[]; userEvents?: unknown[] } | undefined =
        await chrome.tabs.sendMessage(tab.id, { type: 'GET_RRWEB_EVENTS' });

      logger.log(`Got response from tab ${tab.id}:`, response);

      rrwebEvents = response?.events ?? [];
      userEvents = response?.userEvents ?? [];
      
      logger.log(`✓ Received ${rrwebEvents.length} rrweb events, ${userEvents.length} user events`);
      
      // Логируем первые несколько событий для отладки
      if (rrwebEvents.length > 0) {
        logger.log('First rrweb event sample:', JSON.stringify(rrwebEvents[0]).substring(0, 200) + '...');
      }
      if (userEvents.length > 0) {
        logger.log('First user event:', userEvents[0]);
      }
      
    } catch (messageError) {
      logger.warn('✗ Failed to get events from content script:', messageError);
      logger.warn('This may happen on special pages (chrome://, about:blank) or if content script is not injected');
      logger.warn('Check if content script is running on the active tab (open DevTools Console on the page)');
      // Продолжаем без событий, если контент-скрипт недоступен
    }

    // Создаем архив
    const archiveBlob = await createArchive(
      videoBufferResp,
      rrwebEvents,
      userEvents,
      screenshot
    );

    // Скачиваем
    await downloadArchive(archiveBlob);

    logger.log('Archive save completed');
    return { success: true };

  } catch (error) {
    logger.error('Failed to save archive:', error);
    return { success: false, error };
  }
}

// checkPermissions / requestPermissions удалены: старая permission
// больше не нужна (D-A6 — заменена на desktopCapture в manifest), а
// getDisplayMedia не требует runtime-разрешения — нужен только user gesture.
// REQUEST_PERMISSIONS теперь просто запускает запись и возвращает granted=true.

// Обработка сообщений
chrome.runtime.onMessage.addListener((message: Message, sender, sendResponse) => {
  // T-1-NEW-05-01 mitigation: only accept onMessage traffic from this
  // extension (popup, content script, offscreen). External callers (other
  // extensions, web pages) are silently dropped.
  if (sender.id !== chrome.runtime.id) {
    logger.warn('Rejecting message with mismatched sender:', sender.id);
    return false;
  }
  logger.log('Received message:', message.type, message);

  switch (message.type) {
    case 'REQUEST_PERMISSIONS':
      // Под getDisplayMedia (D-01) runtime-permission проверять нечего —
      // браузер сам покажет picker по user gesture из popup. Просто
      // запускаем запись и подтверждаем popup-у.
      (async () => {
        try {
          if (!isRecording) {
            await startVideoCapture();
          }
          sendResponse({ granted: true });
        } catch (error) {
          logger.error('startVideoCapture failed:', error);
          sendResponse({ granted: false });
        }
      })();
      return true;

    case 'GET_VIDEO_BUFFER':
      getVideoBufferFromOffscreen().then((resp) => sendResponse(resp));
      return true;

    case 'SAVE_ARCHIVE':
      saveArchive().then(result => {
        sendResponse(result);
      });
      return true;

    case 'OFFSCREEN_READY':
      logger.log('OFFSCREEN_READY received');
      offscreenReadyResolve?.();
      offscreenReadyResolve = null;
      return false;

    // Legacy chunk-streaming and IndexedDB save/load handlers were removed
    // in Plan 01-03:
    //   - the offscreen recorder now owns the buffer (D-16);
    //   - chunks no longer travel via chrome.runtime.sendMessage (D-19);
    //   - SW-side IDB plumbing was the audit P0 #2 broken path.
    // The IDB helpers were only reachable from those deleted cases.
    // Plan 05 finishes the SW shrink (see deletions above).

    default:
      logger.warn('Unknown message type:', message.type);
      return false;
  }
});

// Инициализация
async function initialize() {
  logger.log('Service Worker initializing');
  // Audit P1 #8: after the SW respawns (e.g. after Chrome wakes it from idle),
  // the offscreen document may still exist while our offscreenCreated flag
  // resets to false. Ask Chrome the ground truth so we don't end up trying
  // to createDocument over an existing one. Cheap and idempotent.
  try {
    if (typeof chrome.offscreen?.hasDocument === 'function') {
      const exists = await chrome.offscreen.hasDocument();
      if (exists) {
        offscreenCreated = true;
        logger.log('Existing offscreen document detected on SW init');
        // CR-03 fix: handshake deadlock after SW respawn.
        // OFFSCREEN_READY is fired by the offscreen exactly once at its
        // bootstrap (recorder.ts line ~452). If the SW is evicted (~30 s
        // idle) while the offscreen document persists, the next SW lifetime
        // creates a fresh `offscreenReady` Promise (line 33) and waits on
        // it forever — the offscreen has no signal to re-emit, and
        // startVideoCapture() hangs at `await offscreenReady`.
        // Resolve readiness immediately when we detect a pre-existing
        // offscreen: it MUST have completed its bootstrap before being
        // observable via hasDocument().
        offscreenReadyResolve?.();
        offscreenReadyResolve = null;
        logger.log('Resolving offscreenReady immediately — offscreen survived a prior SW lifetime');
      }
    }
  } catch (err) {
    logger.warn('chrome.offscreen.hasDocument check failed:', err);
  }
  logger.log('Service Worker initialized');
}

// Запуск при установке
chrome.runtime.onInstalled.addListener((details) => {
  logger.log('Extension installed/updated:', details.reason);
  // RESEARCH.md Runtime State Inventory — clean up orphaned IndexedDB from
  // pre-Phase-01 builds. Idempotent: no-op if DB never existed.
  // T-1-NEW-05-02 mitigation.
  try {
    indexedDB.deleteDatabase('VideoRecorderDB');
    logger.log('Cleaned up orphaned VideoRecorderDB (if present)');
  } catch (e) {
    logger.warn('IDB cleanup failed:', e);
  }
  initialize();
});

// Запуск при старте Service Worker
initialize();