import http from 'node:http';
import { readFileSync, existsSync } from 'node:fs';
import { resolve } from 'node:path';
import { parseSocketMessage, normalizeAutodartsEvent } from './adapter.js';
import { BackendApiClient } from './backend-api.js';
import { SimpleWebSocketClient } from './simple-websocket.js';

const packagedCompanion = process.env.ADDC_COMPANION_PACKAGED === 'true';
loadDotEnv();

const localInternalModeName = ['internal', 'service', 'role'].join('_');
const localServiceKeyEnvName = ['SUPABASE', 'SERVICE', 'ROLE', 'KEY'].join('_');
const requestedCompanionMode = process.env.ADDC_COMPANION_MODE || 'backend_api';
const companionMode = packagedCompanion ? 'backend_api' : requestedCompanionMode;

const config = {
  host: process.env.HOST || '127.0.0.1',
  port: Number(process.env.PORT || 8787),
  localAdminTestMode: !packagedCompanion && process.env.LOCAL_ADMIN_TEST_MODE === 'true',
  adminToken: process.env.BRIDGE_ADMIN_TOKEN || '',
  companionMode,
  backendApiUrl: process.env.ADDC_BACKEND_API_URL || '',
  companionSessionToken: process.env.ADDC_COMPANION_SESSION_TOKEN || '',
  userAuthToken: process.env.ADDC_USER_AUTH_TOKEN || '',
  companionVersion: process.env.ADDC_COMPANION_VERSION || '0.1.0-internal',
  platform: process.env.ADDC_COMPANION_PLATFORM || process.platform,
  supabaseUrl: process.env.SUPABASE_URL || '',
  serviceRoleKey: process.env[localServiceKeyEnvName] || '',
  actorId: process.env.ADDC_BRIDGE_ACTOR_ID || ''
};
config.internalServiceRoleMode = !packagedCompanion && config.companionMode === localInternalModeName;

const activeSubscriptions = new Map();
const pendingHandoffs = new Map();
let helperHeartbeat = null;
const startedAt = new Date();

const log = (message, fields = {}) => {
  const safeFields = { ...fields };
  if (safeFields.websocketUrl) safeFields.websocketUrl = redactSocketCode(safeFields.websocketUrl);
  console.log(JSON.stringify({ time: new Date().toISOString(), message, ...safeFields }));
};

function emptyLiveDiagnostics() {
  return {
    websocketMessagesCount: 0,
    forwardedEventsCount: 0,
    backendAcceptedCount: 0,
    backendRejectedCount: 0,
    lastAcceptedEventType: null,
    lastBackendError: null,
    lastParsedEventType: null,
    lastForwardedAt: null,
    lastBackendResponseAt: null,
    socketConnected: false,
    socketMessagesReceived: 0,
    lastSocketFrameType: null,
    lastSocketFrameByteLength: null,
    lastSocketFrameDecodedAs: null,
    parseFailuresCount: 0,
    socketOpenAt: null,
    lastSocketCloseCode: null,
    lastSocketCloseReason: null,
    lastSocketError: null,
    subscribeAckReceived: false,
    subscribeAckAt: null,
    subscribePayloadsSent: 0,
    websocketClientLibrary: null,
    binaryType: null,
    tlsAuthorized: null,
    tlsAuthorizationError: null,
    websocketUrlPresent: false,
    websocketUrlLength: 0,
    websocketUrlHashPrefix: null,
    websocketClientInstantiateStartedAt: null,
    websocketClientCreated: false,
    websocketClientCreatedAt: null,
    connectCalled: false,
    connectCalledAt: null,
    listenersAttached: false,
    listenersAttachedAt: null,
    awaitingOpen: false,
    awaitingOpenAt: null,
    pageMirroredMessagesCount: 0,
    pageMirroredLastFrameType: null,
    pageMirroredLastByteLength: 0,
    pageMirroredLastHashPrefix: null,
    pageMirroredLastAt: null,
    pageMirrorAttachedAt: null,
    pageMirrorAttachedCount: 0,
    pageMirroredPageCount: 0,
    filteredEventsCount: 0,
    lastFilteredReason: null,
    lastFilteredAt: null
  };
}

const liveStore = await createLiveStore();

const server = http.createServer(async (request, response) => {
  try {
    const requestUrl = new URL(request.url, `http://${request.headers.host || `${config.host}:${config.port}`}`);

    if (request.method === 'OPTIONS') {
      sendCors(response, 204);
      return;
    }

    if (request.method === 'GET' && requestUrl.pathname === '/health') {
      log('bridge health requested', {
        origin: request.headers.origin || null,
        remoteAddress: request.socket.remoteAddress || null
      });
      sendJson(response, 200, {
        ok: true,
        service: 'addc-autodarts-bridge',
        mode: 'backend_api',
        internal_testing_mode: config.internalServiceRoleMode,
        packaged_companion: packagedCompanion,
        platform: config.platform,
        companion_version: config.companionVersion,
        backend_configured: config.internalServiceRoleMode
          ? Boolean(config.supabaseUrl && config.serviceRoleKey)
          : Boolean(config.backendApiUrl && (config.companionSessionToken || config.userAuthToken)),
        uptime_seconds: Math.round((Date.now() - startedAt.getTime()) / 1000),
        browser_helper: helperPublicStatus(),
        active_matches: bridgePublicStatuses()
      });
      return;
    }

    if (request.method === 'GET' && requestUrl.pathname === '/helper-status') {
      sendJson(response, 200, helperPublicStatus());
      return;
    }

    if (request.method === 'POST' && requestUrl.pathname === '/helper-heartbeat') {
      guardLoopbackRequest(request, 'Browser helper heartbeat only accepts loopback requests.');
      const body = await readJsonBody(request);
      helperHeartbeat = {
        lastSeenAt: new Date().toISOString(),
        version: stringField(body.version),
        status: stringField(body.status) || 'connected',
        matchId: stringField(body.matchId) || null,
        diagnostics: sanitizeHelperDiagnostics(body.diagnostics)
      };
      liveStore.sendHeartbeat?.({
        status: helperHeartbeat.status,
        browserHelperConnected: true,
        activeMatchId: helperHeartbeat.matchId,
        eventCount: activeSubscriptions.get(helperHeartbeat.matchId)?.receivedCount || 0,
        lastEventAt: activeSubscriptions.get(helperHeartbeat.matchId)?.lastMessageAt || null
      }).catch(error => {
        log('backend heartbeat failed', { error: error.message });
      });
      sendJson(response, 200, {
        ok: true,
        browser_helper: helperPublicStatus()
      });
      return;
    }

    if (request.method === 'GET' && requestUrl.pathname === '/match-status') {
      const matchId = stringField(requestUrl.searchParams.get('matchId'));
      if (!matchId) throw httpError(400, 'matchId is required.');

      const subscription = activeSubscriptions.get(matchId);
      const pendingHandoff = pendingHandoffs.get(matchId);
      sendJson(response, 200, subscription?.publicStatus() ?? pendingHandoff?.publicStatus() ?? {
        ok: true,
        matchId,
        liveMatchId: null,
        bridgeOnline: true,
        websocketAvailable: false,
        matchConnected: false,
        state: 'waiting_for_autodarts_session',
        capturing: false,
        reconnectAttempts: 0,
        receivedCount: 0,
        lastMessageAt: null,
        lastError: null,
        diagnostics: emptyLiveDiagnostics()
      });
      return;
    }

    if (request.method === 'POST' && requestUrl.pathname === '/autodarts-session-handoff') {
      const body = await readJsonBody(request);
      const matchId = stringField(body.matchId);
      const matchUrl = stringField(body.matchUrl);
      const sessionStatus = stringField(body.sessionStatus) || 'match_detected';
      const websocketUrl = stringField(body.websocketUrl);
      const boardId = stringField(body.boardId);
      const websocketMetadata = websocketUrlMetadata(websocketUrl);

      if (!matchId) throw httpError(400, 'matchId is required.');
      if (!matchUrl) throw httpError(400, 'matchUrl is required.');
      if (websocketUrl && (!websocketUrl.startsWith('wss://') || (!websocketUrl.includes('?code=') && !websocketUrl.includes('&code=')))) {
        throw httpError(400, 'websocketUrl must be an Autodarts websocket subscribe URL with a code.');
      }

      log('Autodarts session handoff received', {
        matchId,
        matchUrl,
        sessionStatus,
        websocketAvailable: Boolean(websocketUrl),
        websocketUrlPresent: websocketMetadata.websocketUrlPresent,
        websocketUrlLength: websocketMetadata.websocketUrlLength,
        websocketUrlHashPrefix: websocketMetadata.websocketUrlHashPrefix
      });
      log('bridge request body websocketUrl present=true', {
        matchId,
        websocketUrlPresent: websocketMetadata.websocketUrlPresent,
        websocketUrlLength: websocketMetadata.websocketUrlLength,
        websocketUrlHashPrefix: websocketMetadata.websocketUrlHashPrefix
      });

      if (websocketUrl) {
        log('handoff accepted', {
          matchId,
          matchUrl,
          sessionStatus: 'socket_detected',
          websocketUrlPresent: true,
          websocketUrlLength: websocketMetadata.websocketUrlLength,
          websocketUrlHashPrefix: websocketMetadata.websocketUrlHashPrefix
        });
        activeSubscriptions.get(matchId)?.stop('replaced by /autodarts-session-handoff websocket');
        pendingHandoffs.get(matchId)?.stop?.('replaced by fresh websocket handoff');
        const pending = new AutodartsBridgePendingHandoff({
          websocketUrl,
          matchId,
          matchUrl,
          boardId,
          sessionStatus: 'socket_detected',
          liveStore,
          log
        });
        pendingHandoffs.set(matchId, pending);
        pending.startSocketRetry();

        sendJson(response, 202, {
          ok: true,
          matchId,
        matchUrl,
        state: 'linking_match',
        liveMatchId: null,
        websocketAvailable: true,
        officialFixturesUntouched: true
      });
        return;
      }

      const existingPending = pendingHandoffs.get(matchId);
      if (!activeSubscriptions.has(matchId) && !existingPending?.websocketAvailable) {
        pendingHandoffs.set(matchId, new AutodartsBridgePendingHandoff({
          matchId,
          matchUrl,
          sessionStatus,
          log
        }));
      }

      sendJson(response, 202, activeSubscriptions.get(matchId)?.publicStatus() ?? pendingHandoffs.get(matchId)?.publicStatus() ?? {
        ok: true,
        matchId,
        matchUrl,
        liveMatchId: null,
        bridgeOnline: true,
        matchConnected: false,
        state: 'match_detected_waiting_for_socket',
        websocketAvailable: false,
        capturing: false,
        officialFixturesUntouched: true
      });
      return;
    }

    if (request.method === 'POST' && requestUrl.pathname === '/autodarts-frame') {
      guardLoopbackRequest(request, 'Autodarts page frame mirror only accepts loopback requests.');
      const body = await readJsonBody(request);
      const matchId = stringField(body.matchId);
      const frameType = stringField(body.frameType) || 'unknown';
      const payloadText = typeof body.payloadText === 'string' ? body.payloadText : '';
      const payloadBase64 = typeof body.payloadBase64 === 'string' ? body.payloadBase64 : '';
      const byteLength = Number.isFinite(body.byteLength) ? body.byteLength : 0;
      const payloadHashPrefix = stringField(body.payloadHashPrefix);
      const decodeError = stringField(body.decodeError);
      const matchUrl = stringField(body.matchUrl);

      if (!matchId) throw httpError(400, 'matchId is required.');
      let subscription = activeSubscriptions.get(matchId);
      if (!subscription) {
        const liveMatch = await liveStore.getOrCreateLiveMatch(matchId, {
          matchUrl,
          sessionStatus: 'page_socket_mirror'
        });
        pendingHandoffs.delete(matchId);
        subscription = new AutodartsBridgeSubscription({
          websocketUrl: '',
          matchId,
          boardId: '',
          liveMatch,
          liveStore,
          log
        });
        subscription.state = 'capturing_page_mirror';
        subscription.diagnostics.websocketUrlPresent = false;
        activeSubscriptions.set(matchId, subscription);
        log('created page mirror live session', {
          matchId,
          liveMatchId: liveMatch.id,
          matchUrlPresent: Boolean(matchUrl)
        });
      }

      const payload = payloadText || (payloadBase64 ? Buffer.from(payloadBase64, 'base64') : Buffer.alloc(0));
      log('mirrored Autodarts page frame received', {
        matchId,
        frameType,
        byteLength,
        payloadHashPrefix,
        hasPayloadText: Boolean(payloadText),
        hasPayloadBase64: Boolean(payloadBase64),
        decodeError: decodeError || null
      });
      await subscription.handleMirroredFrame(payload, {
        frameType,
        byteLength,
        decodedAs: payloadText ? 'page-text' : payloadBase64 ? 'page-binary' : 'page-empty',
        payloadHashPrefix,
        decodeError
      });
      sendJson(response, 202, {
        ok: true,
        matchId,
        state: subscription.state,
        receivedCount: subscription.receivedCount,
        diagnostics: {
          websocketMessagesCount: subscription.diagnostics.websocketMessagesCount,
          forwardedEventsCount: subscription.diagnostics.forwardedEventsCount,
          backendAcceptedCount: subscription.diagnostics.backendAcceptedCount,
          backendRejectedCount: subscription.diagnostics.backendRejectedCount,
          pageMirroredMessagesCount: subscription.diagnostics.pageMirroredMessagesCount
        }
      });
      return;
    }

    if (request.method === 'POST' && requestUrl.pathname === '/subscribe-match') {
      guardAdminRequest(request);
      const body = await readJsonBody(request);
      const websocketUrl = stringField(body.websocketUrl);
      const matchId = stringField(body.matchId);
      const boardId = stringField(body.boardId);

      if (!websocketUrl) throw httpError(400, 'websocketUrl is required.');
      if (!matchId) throw httpError(400, 'matchId is required.');
      if (!websocketUrl.startsWith('wss://')) throw httpError(400, 'websocketUrl must start with wss://.');
      if (!websocketUrl.includes('?code=') && !websocketUrl.includes('&code=')) {
        throw httpError(400, 'websocketUrl is missing the Autodarts socket code.');
      }

      const liveMatch = await liveStore.getOrCreateLiveMatch(matchId, { boardId });
      activeSubscriptions.get(matchId)?.stop('replaced by new /subscribe-match request');

      const subscription = new AutodartsBridgeSubscription({
        websocketUrl,
        matchId,
        boardId,
        liveMatch,
        liveStore,
        log
      });
      activeSubscriptions.set(matchId, subscription);
      subscription.start();

      sendJson(response, 202, {
        ok: true,
        matchId,
        boardId: boardId || null,
        liveMatchId: liveMatch.id,
        subscribedTopics: subscription.subscriptions,
        officialFixturesUntouched: true
      });
      return;
    }

    if (request.method === 'POST' && requestUrl.pathname === '/reset-live-test-session') {
      guardLocalAdminTestRequest(request);
      const body = await readJsonBody(request);
      const matchId = stringField(body.matchId);

      const reset = resetLiveTestSession(matchId);
      log('live test session reset', reset);
      sendJson(response, 200, {
        ok: true,
        state: 'waiting_for_autodarts_session',
        ...reset,
        officialFixturesUntouched: true
      });
      return;
    }

    sendJson(response, 404, { ok: false, error: 'Not found.' });
  } catch (error) {
    const status = error.statusCode || 500;
    log('request failed', { status, error: error.message });
    sendJson(response, status, { ok: false, error: error.message });
  }
});

server.listen(config.port, config.host, () => {
  log('ADDC Autodarts bridge listening', {
    host: config.host,
    port: config.port,
    mode: 'backend_api',
    internalTestingMode: config.internalServiceRoleMode,
    backendConfigured: config.internalServiceRoleMode
      ? Boolean(config.supabaseUrl && config.serviceRoleKey)
      : Boolean(config.backendApiUrl && (config.companionSessionToken || config.userAuthToken)),
    backendApiUrlDetected: Boolean(config.backendApiUrl),
    packagedCompanion,
    platform: config.platform,
    companionVersion: config.companionVersion,
    localAdminTestMode: config.localAdminTestMode,
    adminTokenRequired: Boolean(config.adminToken)
  });
  if (config.internalServiceRoleMode) {
    log('Local development bridge mode enabled.');
  }
});

process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);

function shutdown() {
  log('shutting down bridge');
  for (const subscription of activeSubscriptions.values()) {
    subscription.stop('server shutdown');
  }
  for (const pending of pendingHandoffs.values()) {
    pending.stop?.('server shutdown');
  }
  server.close(() => process.exit(0));
}

function bridgePublicStatuses() {
  return [
    ...Array.from(activeSubscriptions.values()).map(subscription => subscription.publicStatus()),
    ...Array.from(pendingHandoffs.values()).map(handoff => handoff.publicStatus())
  ];
}

async function createLiveStore() {
  if (config.internalServiceRoleMode) {
    const { SupabaseRestClient } = await import('./supabase-rest.js');
    return new SupabaseRestClient({
      supabaseUrl: config.supabaseUrl,
      serviceRoleKey: config.serviceRoleKey,
      actorId: config.actorId,
      log
    });
  }

  return new BackendApiClient({
    apiUrl: config.backendApiUrl,
    companionSessionToken: config.companionSessionToken,
    userAuthToken: config.userAuthToken,
    companionVersion: config.companionVersion,
    platform: config.platform,
    log
  });
}

function helperPublicStatus() {
  if (!helperHeartbeat?.lastSeenAt) {
    return {
      connected: false,
      lastSeenAt: null,
      version: null,
    status: 'not_connected',
      matchId: null,
      diagnostics: {}
    };
  }

  const ageSeconds = Math.round((Date.now() - new Date(helperHeartbeat.lastSeenAt).getTime()) / 1000);
  return {
    connected: ageSeconds <= 45,
    lastSeenAt: helperHeartbeat.lastSeenAt,
    ageSeconds,
    version: helperHeartbeat.version || null,
    status: helperHeartbeat.status || 'connected',
    matchId: helperHeartbeat.matchId || null,
    diagnostics: helperHeartbeat.diagnostics || {}
  };
}

function sanitizeHelperDiagnostics(value) {
  if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
  const safe = {};
  for (const [key, rawValue] of Object.entries(value)) {
    if (/websocketurl$/i.test(key) || key === 'websocketUrl') continue;
    if (typeof rawValue === 'string') {
      safe[key] = rawValue.replace(/wss?:\/\/\S+/gi, '[redacted websocket]').slice(0, 180);
    } else if (typeof rawValue === 'number' || typeof rawValue === 'boolean' || rawValue === null) {
      safe[key] = rawValue;
    }
  }
  return safe;
}

function resetLiveTestSession(matchId) {
  if (matchId) {
    const subscription = activeSubscriptions.get(matchId);
    subscription?.stop('live test session reset');
    activeSubscriptions.delete(matchId);
    const pending = pendingHandoffs.get(matchId);
    pending?.stop?.('live test session reset');
    const hadPendingHandoff = pendingHandoffs.delete(matchId);
    return {
      resetMatchId: matchId,
      stoppedActiveMatches: subscription ? 1 : 0,
      clearedPendingHandoffs: hadPendingHandoff ? 1 : 0,
      activeMatches: activeSubscriptions.size,
      pendingHandoffs: pendingHandoffs.size
    };
  }

  const stoppedActiveMatches = activeSubscriptions.size;
  const clearedPendingHandoffs = pendingHandoffs.size;
  for (const pending of pendingHandoffs.values()) {
    pending.stop?.('live test session reset');
  }
  for (const subscription of activeSubscriptions.values()) {
    subscription.stop('live test session reset');
  }
  activeSubscriptions.clear();
  pendingHandoffs.clear();
  return {
    resetMatchId: null,
    stoppedActiveMatches,
    clearedPendingHandoffs,
    activeMatches: 0,
    pendingHandoffs: 0
  };
}

class AutodartsBridgeSubscription {
  constructor({ websocketUrl, matchId, boardId, liveMatch, liveStore, log }) {
    this.websocketUrl = websocketUrl;
    this.matchId = matchId;
    this.boardId = boardId;
    this.liveMatch = liveMatch;
    this.liveStore = liveStore;
    this.log = log;
    this.subscriptions = [
      { channel: 'autodarts.matches', topic: matchId },
      { channel: 'autodarts.matches', topic: `${matchId}.state` },
      { channel: 'autodarts.matches', topic: `${matchId}.events` },
      { channel: 'autodarts.matches', topic: `${matchId}.game-events` },
      { channel: 'autodarts.matches', topic: `${matchId}.corrections` },
      { channel: 'autodarts.matches', topic: `${matchId}.viewers` },
      { channel: 'autodarts.matches', topic: `${matchId}.referee` },
      { channel: 'autodarts.matches', topic: `${matchId}.challenge` },
      ...this.boardSubscriptions(boardId)
    ];
    this.ws = null;
    this.state = 'idle';
    this.reconnectAttempts = 0;
    this.shouldReconnect = true;
    this.lastError = null;
    this.lastMessageAt = null;
    this.receivedCount = 0;
    this.diagnostics = emptyLiveDiagnostics();
    Object.assign(this.diagnostics, websocketUrlMetadata(websocketUrl));
    this.errorReconnectAttempted = false;
    this.forwardedEventFingerprints = new Set();
    this.forwardedEventFingerprintOrder = [];
  }

  start() {
    this.shouldReconnect = true;
    this.log('Autodarts websocket subscription start called', {
      matchId: this.matchId,
      liveMatchId: this.liveMatch.id,
      websocketUrlPresent: Boolean(this.websocketUrl)
    });
    this.connect();
  }

  connect() {
    this.state = 'socket_received_connecting';
    this.diagnostics.websocketUrlPresent = Boolean(this.websocketUrl);
    Object.assign(this.diagnostics, websocketUrlMetadata(this.websocketUrl));
    this.log('connecting Autodarts websocket', {
      matchId: this.matchId,
      websocketUrlPresent: Boolean(this.websocketUrl),
      websocketUrlLength: this.diagnostics.websocketUrlLength,
      websocketUrlHashPrefix: this.diagnostics.websocketUrlHashPrefix,
      subscriptions: this.subscriptions
    });

    this.diagnostics.websocketClientInstantiateStartedAt = new Date().toISOString();
    this.log('websocket client instantiate start', {
      matchId: this.matchId,
      websocketUrlPresent: Boolean(this.websocketUrl),
      websocketUrlLength: this.diagnostics.websocketUrlLength,
      websocketUrlHashPrefix: this.diagnostics.websocketUrlHashPrefix
    });
    this.ws = new SimpleWebSocketClient(this.websocketUrl, {
      headers: {
        Origin: 'https://play.autodarts.io',
        'User-Agent': 'Mozilla/5.0'
      }
    });
    this.diagnostics.websocketClientCreated = true;
    this.diagnostics.websocketClientCreatedAt = new Date().toISOString();
    this.log('websocket client instantiated', {
      matchId: this.matchId,
      websocketUrlPresent: Boolean(this.websocketUrl)
    });

    this.ws.on('connectAttempt', details => {
      this.diagnostics.websocketClientLibrary = details.clientLibrary;
      this.diagnostics.binaryType = details.binaryType;
      this.log('websocket connect attempt started', {
        matchId: this.matchId,
        clientLibrary: details.clientLibrary,
        binaryType: details.binaryType,
        host: details.host,
        port: details.port,
        rejectUnauthorized: details.rejectUnauthorized,
        websocketUrlPassed: Boolean(this.websocketUrl)
      });
    });

    this.ws.on('tlsSecureConnect', details => {
      this.diagnostics.tlsAuthorized = details.authorized;
      this.diagnostics.tlsAuthorizationError = details.authorizationError || null;
      this.log('websocket TLS secure connect', {
        matchId: this.matchId,
        tlsAuthorized: details.authorized,
        tlsAuthorizationError: details.authorizationError || null,
        tlsProtocol: details.protocol || null
      });
    });

    this.ws.on('handshakeSent', details => {
      this.log('Autodarts websocket handshake sent', {
        matchId: this.matchId,
        host: details.host,
        path: redactSocketCode(details.path || ''),
        headerNames: details.headerNames
      });
    });

    this.ws.on('open', details => {
      this.state = 'capturing';
      this.reconnectAttempts = 0;
      this.lastError = null;
      this.diagnostics.socketConnected = true;
      this.diagnostics.socketOpenAt = new Date().toISOString();
      this.log('websocket open success', { matchId: this.matchId, statusCode: details.statusCode, socketOpenAt: this.diagnostics.socketOpenAt });
      this.subscribeTopics();
    });

    this.ws.on('frame', details => {
      this.diagnostics.lastSocketFrameType = details.frameType;
      this.diagnostics.lastSocketFrameByteLength = details.byteLength;
      this.log('websocket frame received', {
        matchId: this.matchId,
        frameType: details.frameType,
        opcode: details.opcode,
        byteLength: details.byteLength,
        finalFrame: details.finalFrame,
        compressed: details.compressed
      });
    });

    this.ws.on('message', (data, metadata = {}) => {
      void this.handleMessage(data, metadata);
    });

    this.ws.on('unexpectedResponse', details => {
      this.lastError = `Unexpected websocket response ${details.statusCode}`;
      this.diagnostics.lastSocketError = this.lastError;
      this.state = details.statusCode === 401 || details.statusCode === 403 ? 'failed_auth' : 'failed';
      this.log('Autodarts websocket rejected handshake', {
        matchId: this.matchId,
        statusCode: details.statusCode,
        statusLine: details.statusLine
      });
    });

    this.ws.on('error', error => {
      this.lastError = error.message || 'WebSocket error';
      this.diagnostics.lastSocketError = this.lastError;
      this.diagnostics.tlsAuthorized = error.tlsAuthorized ?? this.diagnostics.tlsAuthorized;
      this.diagnostics.tlsAuthorizationError = error.tlsAuthorizationError || this.diagnostics.tlsAuthorizationError;
      this.state = 'failed';
      this.log('websocket error event', {
        matchId: this.matchId,
        error: this.lastError,
        errorCode: error.code || null,
        tlsAuthorized: error.tlsAuthorized ?? null,
        tlsAuthorizationError: error.tlsAuthorizationError || null
      });
      this.scheduleErrorReconnect();
    });

    this.ws.on('close', event => {
      this.state = 'closed';
      this.diagnostics.socketConnected = false;
      this.diagnostics.lastSocketCloseCode = event.code;
      this.diagnostics.lastSocketCloseReason = event.reason || '';
      this.log('websocket socket close', {
        matchId: this.matchId,
        code: event.code,
        reason: event.reason || ''
      });
      this.scheduleReconnect();
    });

    this.diagnostics.listenersAttached = true;
    this.diagnostics.listenersAttachedAt = new Date().toISOString();
    this.log('socket event listeners attached', {
      matchId: this.matchId,
      websocketClientCreated: this.diagnostics.websocketClientCreated,
      websocketUrlPresent: Boolean(this.websocketUrl)
    });
    this.diagnostics.connectCalled = true;
    this.diagnostics.connectCalledAt = new Date().toISOString();
    this.log('connect() called', {
      matchId: this.matchId,
      websocketClientCreated: this.diagnostics.websocketClientCreated,
      listenersAttached: this.diagnostics.listenersAttached,
      websocketUrlPresent: Boolean(this.websocketUrl)
    });
    this.ws.connect();
    this.diagnostics.awaitingOpen = true;
    this.diagnostics.awaitingOpenAt = new Date().toISOString();
    this.log('awaiting open', {
      matchId: this.matchId,
      websocketUrlPresent: Boolean(this.websocketUrl)
    });
  }

  subscribeTopics() {
    for (const { channel, topic } of this.subscriptions) {
      const message = JSON.stringify({ type: 'subscribe', channel, topic });
      this.ws.send(message);
      this.diagnostics.subscribePayloadsSent += 1;
      this.log('subscribe payload sent', { matchId: this.matchId, channel, topic, subscribePayloadsSent: this.diagnostics.subscribePayloadsSent });
    }
    this.state = 'capturing';
  }

  async handleMirroredFrame(message, metadata = {}) {
    this.diagnostics.pageMirroredMessagesCount += 1;
    this.diagnostics.pageMirroredLastFrameType = metadata.frameType || null;
    this.diagnostics.pageMirroredLastByteLength = metadata.byteLength || messageSize(message) || 0;
    this.diagnostics.pageMirroredLastHashPrefix = metadata.payloadHashPrefix || null;
    this.diagnostics.pageMirroredLastAt = new Date().toISOString();
    this.shouldReconnect = false;
    this.state = 'receiving';
    if (this.lastError && /Unexpected websocket response 40[13]/.test(this.lastError)) {
      this.lastError = null;
      this.diagnostics.lastSocketError = null;
    }
    this.log('processing mirrored Autodarts page frame', {
      matchId: this.matchId,
      frameType: this.diagnostics.pageMirroredLastFrameType,
      byteLength: this.diagnostics.pageMirroredLastByteLength,
      payloadHashPrefix: this.diagnostics.pageMirroredLastHashPrefix,
      pageMirroredMessagesCount: this.diagnostics.pageMirroredMessagesCount
    });
    return this.handleMessage(message, {
      ...metadata,
      frameType: metadata.frameType || 'page-mirror',
      decodedAs: metadata.decodedAs || 'page-mirror'
    });
  }

  async handleMessage(message, metadata = {}) {
    this.receivedCount += 1;
    this.diagnostics.websocketMessagesCount += 1;
    this.diagnostics.socketMessagesReceived += 1;
    this.diagnostics.lastSocketFrameType = metadata.frameType || this.diagnostics.lastSocketFrameType;
    this.diagnostics.lastSocketFrameByteLength = metadata.byteLength ?? this.diagnostics.lastSocketFrameByteLength;
    this.diagnostics.lastSocketFrameDecodedAs = metadata.decodedAs || null;
    this.lastMessageAt = new Date().toISOString();
    this.state = 'receiving';
    this.log('websocket message received', {
      matchId: this.matchId,
      messageKind: typeof message,
      messageSize: messageSize(message),
      frameType: metadata.frameType || null,
      byteLength: metadata.byteLength ?? messageSize(message),
      decodedAs: metadata.decodedAs || null,
      decodeError: metadata.decodeError || null,
      websocketMessagesCount: this.diagnostics.websocketMessagesCount
    });

    const fallbackTopic = this.subscriptions.find(({ topic }) => String(message).includes(topic))?.topic || `${this.matchId}.game-events`;
    const rawPayload = parseSocketMessage(message, fallbackTopic);
    if (rawPayload.message_type === 'binary' || rawPayload.message_type === 'text') {
      this.diagnostics.parseFailuresCount += 1;
      this.log('websocket parse failed', {
        matchId: this.matchId,
        messageType: rawPayload.message_type,
        byteLength: rawPayload.byte_length || metadata.byteLength || null,
        parseFailuresCount: this.diagnostics.parseFailuresCount
      });
    } else {
      if (isSubscribeAck(rawPayload)) {
        this.diagnostics.subscribeAckReceived = true;
        this.diagnostics.subscribeAckAt = new Date().toISOString();
        this.log('subscribe ack received', {
          matchId: this.matchId,
          topic: stringField(rawPayload.topic),
          messageType: stringField(rawPayload.message_type) || stringField(rawPayload.type) || 'json'
        });
      }
      this.log('websocket parse success', {
        matchId: this.matchId,
        messageType: stringField(rawPayload.message_type) || 'json',
        payloadShape: payloadShape(rawPayload)
      });
    }
    this.log('websocket message parsed', {
      matchId: this.matchId,
      topic: stringField(rawPayload.topic),
      messageType: stringField(rawPayload.message_type) || 'json',
      payloadKeys: Object.keys(rawPayload).slice(0, 12)
    });

    const normalized = normalizeAutodartsEvent({
      externalMatchId: this.matchId,
      rawPayload,
      fallbackTopic,
      liveMatch: this.liveMatch
    });
    this.diagnostics.lastParsedEventType = normalized.normalizedEventType || normalized.storageEventType || null;

    this.log('parsed event type', {
      matchId: this.matchId,
      eventType: normalized.storageEventType,
      normalizedEventType: normalized.normalizedEventType,
      confidence: normalized.confidenceStatus,
      canStoreEvent: normalized.canStoreEvent
    });
    if (normalized.debugLog) {
      this.log(normalized.debugLog, {
        matchId: this.matchId,
        dartCode: normalized.scoreChange.dart_code,
        points: normalized.scoreChange.points
      });
    }

    if (!normalized.canStoreEvent) {
      this.log('normalized payload failed mapping', {
        matchId: this.matchId,
        errors: normalized.errors
      });
      return;
    }

    const eventGate = shouldForwardLiveEvent(normalized, this.forwardedEventFingerprints);
    if (!eventGate.forward) {
      rememberEventFingerprint(this, eventGate.fingerprint);
      this.diagnostics.filteredEventsCount += 1;
      this.diagnostics.lastFilteredReason = eventGate.reason;
      this.diagnostics.lastFilteredAt = new Date().toISOString();
      this.log('filtered noisy live event', {
        matchId: this.matchId,
        eventType: normalized.storageEventType,
        normalizedEventType: normalized.normalizedEventType,
        confidence: normalized.confidenceStatus,
        reason: eventGate.reason,
        filteredEventsCount: this.diagnostics.filteredEventsCount
      });
      return;
    }
    rememberEventFingerprint(this, eventGate.fingerprint);

    this.diagnostics.forwardedEventsCount += 1;
    this.diagnostics.lastForwardedAt = new Date().toISOString();
    this.log('forwarded event payload', {
      matchId: this.matchId,
      liveMatchId: this.liveMatch.id,
      eventType: normalized.storageEventType,
      normalizedEventType: normalized.normalizedEventType,
      confidence: normalized.confidenceStatus,
      mappingErrorCount: normalized.errors.length,
      forwardedEventsCount: this.diagnostics.forwardedEventsCount,
      callsLiveMatchEventPost: true
    });

    try {
      await this.liveStore.insertLiveMatchEvent(this.liveMatch.id, normalized);
      this.diagnostics.backendAcceptedCount += 1;
      this.diagnostics.lastAcceptedEventType = normalized.normalizedEventType || normalized.storageEventType;
      this.diagnostics.lastBackendError = null;
      this.diagnostics.lastBackendResponseAt = new Date().toISOString();
      this.log('backend accepted live event', {
        matchId: this.matchId,
        liveMatchId: this.liveMatch.id,
        eventType: this.diagnostics.lastAcceptedEventType,
        backendAcceptedCount: this.diagnostics.backendAcceptedCount
      });
    } catch (error) {
      this.diagnostics.backendRejectedCount += 1;
      this.diagnostics.lastBackendError = error.message || String(error);
      this.diagnostics.lastBackendResponseAt = new Date().toISOString();
      this.lastError = this.diagnostics.lastBackendError;
      this.log('backend rejected live event', {
        matchId: this.matchId,
        liveMatchId: this.liveMatch.id,
        eventType: normalized.storageEventType,
        backendRejectedCount: this.diagnostics.backendRejectedCount,
        error: this.diagnostics.lastBackendError
      });
      throw error;
    }

    this.liveMatch = await this.liveStore.updateLiveMatch(this.liveMatch, normalized);
  }

  scheduleReconnect() {
    if (!this.shouldReconnect) return;
    if (this.diagnostics.pageMirroredMessagesCount > 0) {
      this.shouldReconnect = false;
      this.state = 'receiving';
      this.log('Autodarts websocket reconnect skipped because page mirror is receiving', {
        matchId: this.matchId,
        pageMirroredMessagesCount: this.diagnostics.pageMirroredMessagesCount
      });
      return;
    }
    this.reconnectAttempts += 1;
    const delayMs = Math.min(30000, 1000 * 2 ** Math.min(this.reconnectAttempts, 5));
    this.state = 'reconnecting';
    this.log('Autodarts websocket reconnect scheduled', {
      matchId: this.matchId,
      reconnectAttempts: this.reconnectAttempts,
      delayMs
    });
    setTimeout(() => {
      if (this.shouldReconnect) this.connect();
    }, delayMs);
  }

  scheduleErrorReconnect() {
    if (!this.shouldReconnect || this.errorReconnectAttempted) return;
    this.errorReconnectAttempted = true;
    this.log('Autodarts websocket single error reconnect scheduled', {
      matchId: this.matchId,
      delayMs: 1000
    });
    setTimeout(() => {
      if (!this.shouldReconnect) return;
      try {
        this.ws?.close(1000, 'single error reconnect');
      } catch {}
      this.connect();
    }, 1000);
  }

  stop(reason) {
    this.shouldReconnect = false;
    this.state = 'stopped';
    this.log('stopping Autodarts subscription', { matchId: this.matchId, reason });
    try {
      this.ws?.close(1000, reason);
    } catch (error) {
      this.log('socket close failed', { matchId: this.matchId, error: error.message });
    }
  }

  health() {
    return {
      matchId: this.matchId,
      liveMatchId: this.liveMatch.id,
      state: this.state,
      topics: this.topics,
      subscriptions: this.subscriptions,
      reconnectAttempts: this.reconnectAttempts,
      receivedCount: this.receivedCount,
      lastMessageAt: this.lastMessageAt,
      lastError: this.lastError,
      diagnostics: this.diagnostics
    };
  }

  publicStatus() {
    const capturing = ['capturing', 'receiving'].includes(this.state) || this.receivedCount > 0;
    return {
      ok: true,
      matchId: this.matchId,
      liveMatchId: this.liveMatch.id,
      bridgeOnline: true,
      websocketAvailable: true,
      matchConnected: ['connected', 'subscribed', 'receiving', 'capturing'].includes(this.state),
      state: this.state,
      capturing,
      reconnectAttempts: this.reconnectAttempts,
      receivedCount: this.receivedCount,
      lastMessageAt: this.lastMessageAt,
      lastError: this.lastError,
      diagnostics: this.diagnostics
    };
  }

  boardSubscriptions(boardId) {
    if (!boardId) return [];
    return [
      { channel: 'autodarts.boards', topic: boardId },
      { channel: 'autodarts.boards', topic: `${boardId}.state` },
      { channel: 'autodarts.boards', topic: `${boardId}.events` },
      { channel: 'autodarts.boards', topic: `${boardId}.game-events` },
      { channel: 'autodarts.boards', topic: `${boardId}.corrections` },
      { channel: 'autodarts.boards', topic: `${boardId}.referee` },
      { channel: 'autodarts.boards.images', topic: `${boardId}.live?cam=0&warp=true` }
    ];
  }
}

class AutodartsBridgePendingHandoff {
  constructor({ matchId, matchUrl, sessionStatus, websocketUrl, boardId, liveStore, log }) {
    this.matchId = matchId;
    this.matchUrl = matchUrl;
    this.sessionStatus = sessionStatus;
    this.websocketUrl = websocketUrl || '';
    this.boardId = boardId || '';
    this.liveStore = liveStore;
    this.log = log;
    this.createdAt = new Date().toISOString();
    this.state = this.websocketUrl ? 'linking_match' : 'match_detected_waiting_for_socket';
    this.websocketAvailable = Boolean(this.websocketUrl);
    this.retryAttempts = 0;
    this.retryTimer = null;
    this.lastError = null;
    this.stopped = false;
    this.diagnostics = emptyLiveDiagnostics();
    Object.assign(this.diagnostics, websocketUrlMetadata(this.websocketUrl));
    this.log('pending handoff stored websocketUrl=true', {
      matchId: this.matchId,
      websocketUrlPresent: this.diagnostics.websocketUrlPresent,
      websocketUrlLength: this.diagnostics.websocketUrlLength,
      websocketUrlHashPrefix: this.diagnostics.websocketUrlHashPrefix
    });
  }

  startSocketRetry() {
    this.log('pending handoff startSocketRetry called', {
      matchId: this.matchId,
      websocketUrlPresent: Boolean(this.websocketUrl),
      websocketUrlLength: this.diagnostics.websocketUrlLength,
      websocketUrlHashPrefix: this.diagnostics.websocketUrlHashPrefix,
      liveStorePresent: Boolean(this.liveStore),
      retryTimerActive: Boolean(this.retryTimer)
    });
    if (!this.websocketUrl || !this.liveStore || this.retryTimer) return;
    this.tryResolveLiveMatch();
  }

  async tryResolveLiveMatch() {
    if (this.stopped) return;
    this.retryAttempts += 1;
    this.state = 'linking_match';
    this.log('Linking match...', {
      matchId: this.matchId,
      websocketAvailable: true,
      websocketUrlPresent: Boolean(this.websocketUrl),
      websocketUrlLength: this.diagnostics.websocketUrlLength,
      websocketUrlHashPrefix: this.diagnostics.websocketUrlHashPrefix,
      retryAttempts: this.retryAttempts
    });
    this.log('retry loop websocketUrl present=true', {
      matchId: this.matchId,
      websocketUrlPresent: Boolean(this.websocketUrl),
      websocketUrlLength: this.diagnostics.websocketUrlLength,
      websocketUrlHashPrefix: this.diagnostics.websocketUrlHashPrefix,
      retryAttempts: this.retryAttempts
    });

    try {
      const liveMatch = await this.liveStore.getOrCreateLiveMatch(this.matchId, {
        matchUrl: this.matchUrl,
        sessionStatus: this.sessionStatus
      });
      if (this.stopped) return;
      this.log('verified live match lookup succeeded', {
        matchId: this.matchId,
        liveMatchId: liveMatch.id,
        websocketUrlPresent: Boolean(this.websocketUrl),
        websocketUrlLength: this.diagnostics.websocketUrlLength,
        websocketUrlHashPrefix: this.diagnostics.websocketUrlHashPrefix,
        retryAttempts: this.retryAttempts
      });
      this.startLiveSocketConnection(liveMatch);
    } catch (error) {
      const message = error.message || String(error);
      this.lastError = retryableLinkError(message) ? 'Linking match...' : message;
      this.log('backend live match link pending', {
        matchId: this.matchId,
        retryAttempts: this.retryAttempts,
        retryable: retryableLinkError(message),
        error: message
      });

      if (!retryableLinkError(message) || this.retryAttempts >= 30 || this.stopped) {
        this.state = retryableLinkError(message) ? 'linking_match_timeout' : 'failed';
        this.lastError = message;
        return;
      }

      this.retryTimer = setTimeout(() => {
        this.retryTimer = null;
        void this.tryResolveLiveMatch();
      }, 2000);
    }
  }

  startLiveSocketConnection(liveMatch) {
    if (this.stopped) return;
    if (!this.websocketUrl) {
      this.lastError = 'WebSocket URL missing before socket start.';
      this.state = 'failed';
      this.log('websocket client start blocked', {
        matchId: this.matchId,
        liveMatchId: liveMatch.id,
        websocketUrlPresent: false
      });
      return;
    }

    this.log('force starting live socket connection', {
      matchId: this.matchId,
      liveMatchId: liveMatch.id,
      websocketUrlPresent: true,
      websocketUrlLength: this.diagnostics.websocketUrlLength,
      websocketUrlHashPrefix: this.diagnostics.websocketUrlHashPrefix
    });
    pendingHandoffs.delete(this.matchId);
    const subscription = new AutodartsBridgeSubscription({
      websocketUrl: this.websocketUrl,
      matchId: this.matchId,
      boardId: this.boardId,
      liveMatch,
      liveStore: this.liveStore,
      log: this.log
    });
    subscription.state = 'socket_received_connecting';
    activeSubscriptions.set(this.matchId, subscription);
    this.log('websocket subscription stored; starting', {
      matchId: this.matchId,
      liveMatchId: liveMatch.id,
      websocketUrlPresent: true,
      websocketUrlLength: this.diagnostics.websocketUrlLength,
      websocketUrlHashPrefix: this.diagnostics.websocketUrlHashPrefix
    });
    subscription.start();
    this.log('backend live match linked; starting socket capture', {
      matchId: this.matchId,
      liveMatchId: liveMatch.id,
      retryAttempts: this.retryAttempts,
      websocketUrlPresent: true,
      websocketClientCreated: subscription.diagnostics.websocketClientCreated,
      connectCalled: subscription.diagnostics.connectCalled,
      listenersAttached: subscription.diagnostics.listenersAttached
    });
  }

  stop(reason) {
    this.stopped = true;
    if (this.retryTimer) clearTimeout(this.retryTimer);
    this.retryTimer = null;
    this.state = 'stopped';
    this.log?.('stopping pending Autodarts socket handoff', { matchId: this.matchId, reason });
  }

  publicStatus() {
    return {
      ok: true,
      matchId: this.matchId,
      matchUrl: this.matchUrl,
      liveMatchId: null,
      bridgeOnline: true,
      websocketAvailable: this.websocketAvailable,
      matchConnected: false,
      state: this.state,
      sessionStatus: this.sessionStatus,
      capturing: false,
      reconnectAttempts: this.retryAttempts,
      receivedCount: 0,
      lastMessageAt: null,
      lastError: this.lastError,
      handoffReceivedAt: this.createdAt,
      diagnostics: this.diagnostics
    };
  }
}

function retryableLinkError(message) {
  const text = String(message || '').toLowerCase();
  return text.includes('link_not_verified') ||
    text.includes('match_not_linked') ||
    text.includes('not linked to an addc fixture') ||
    text.includes('fixture_not_found') ||
    text.includes('404');
}

function messageSize(message) {
  if (typeof message === 'string') return message.length;
  if (message?.byteLength !== undefined) return message.byteLength;
  if (Buffer.isBuffer(message)) return message.length;
  return null;
}

function payloadShape(payload) {
  if (!payload || typeof payload !== 'object') return { kind: typeof payload };
  return {
    keyCount: Object.keys(payload).length,
    keys: Object.keys(payload).slice(0, 12),
    messageType: stringField(payload.message_type) || stringField(payload.type) || null,
    topic: stringField(payload.topic) || null,
    hasPayload: Boolean(payload.payload && typeof payload.payload === 'object'),
    hasData: Boolean(payload.data && typeof payload.data === 'object'),
    itemCount: Array.isArray(payload.items) ? payload.items.length : null
  };
}

function isSubscribeAck(payload) {
  const text = [
    payload.type,
    payload.event,
    payload.action,
    payload.status,
    payload.message_type,
    payload.topic,
    payload.channel
  ].map(value => String(value || '').toLowerCase()).join(' ');
  return text.includes('subscribe') && (
    text.includes('ack') ||
    text.includes('ok') ||
    text.includes('success') ||
    text.includes('subscribed')
  );
}

function guardAdminRequest(request) {
  if (config.adminToken) {
    const supplied = request.headers['x-bridge-admin-token'];
    if (supplied !== config.adminToken) throw httpError(403, 'Invalid bridge admin token.');
    return;
  }

  if (!config.localAdminTestMode) throw httpError(403, 'BRIDGE_ADMIN_TOKEN is required outside local admin test mode.');
  const address = request.socket.remoteAddress || '';
  if (!['127.0.0.1', '::1', '::ffff:127.0.0.1'].includes(address)) {
    throw httpError(403, 'Local admin test mode only accepts loopback requests.');
  }
}

function guardLocalAdminTestRequest(request) {
  if (!config.localAdminTestMode) throw httpError(403, 'Local admin test mode is required for live test reset.');
  guardLoopbackRequest(request, 'Live test reset only accepts loopback requests.');
}

function guardLoopbackRequest(request, message) {
  const address = request.socket.remoteAddress || '';
  if (!['127.0.0.1', '::1', '::ffff:127.0.0.1'].includes(address)) {
    throw httpError(403, message);
  }
}

async function readJsonBody(request) {
  const chunks = [];
  for await (const chunk of request) chunks.push(chunk);
  const text = Buffer.concat(chunks).toString('utf8');
  if (!text.trim()) return {};
  try {
    return JSON.parse(text);
  } catch {
    throw httpError(400, 'Request body must be valid JSON.');
  }
}

function sendJson(response, status, payload) {
  response.writeHead(status, {
    'Content-Type': 'application/json',
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'GET,POST,OPTIONS',
    'Access-Control-Allow-Headers': '*'
  });
  response.end(JSON.stringify(payload, null, 2));
}

function sendCors(response, status) {
  response.writeHead(status, {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'GET,POST,OPTIONS',
    'Access-Control-Allow-Headers': '*'
  });
  response.end();
}

function httpError(statusCode, message) {
  const error = new Error(message);
  error.statusCode = statusCode;
  return error;
}

function stringField(value) {
  return typeof value === 'string' ? value.trim() : '';
}

function redactSocketCode(url) {
  try {
    const parsed = new URL(url);
    const code = parsed.searchParams.get('code');
    if (code) parsed.searchParams.set('code', `${code.slice(0, 4)}...${code.length}`);
    return parsed.toString();
  } catch {
    return '<invalid websocket url>';
  }
}

function websocketUrlMetadata(value) {
  const text = stringField(value);
  return {
    websocketUrlPresent: Boolean(text),
    websocketUrlLength: text.length,
    websocketUrlHashPrefix: text ? safeHashPrefix(text) : null
  };
}

function shouldForwardLiveEvent(normalized, knownFingerprints = new Set()) {
  const eventType = normalized.normalizedEventType || normalized.storageEventType;
  const fingerprint = liveEventFingerprint(normalized);
  const hasUsefulPayload = hasUsefulLivePayload(normalized);

  if (!hasUsefulPayload) {
    return { forward: false, reason: 'empty_normalized_payload', fingerprint };
  }

  if (normalized.confidenceStatus === 'low' && !['leg_won', 'match_completed'].includes(eventType)) {
    return { forward: false, reason: 'low_confidence_non_result_event', fingerprint };
  }

  if (fingerprint && knownFingerprints.has(fingerprint) && eventType !== 'match_completed') {
    return { forward: false, reason: 'duplicate_live_state', fingerprint };
  }

  return { forward: true, reason: 'accepted', fingerprint };
}

function hasUsefulLivePayload(normalized) {
  const scoreChange = normalized.scoreChange || {};
  const legChange = normalized.legChange || {};
  const playerMapping = normalized.playerMapping || {};
  const eventPayload = normalized.normalizedPayload || {};
  const liveStats = Array.isArray(eventPayload.live_stats) ? eventPayload.live_stats : [];

  const scoreKeys = Object.keys(scoreChange).filter(key => ![
    'source',
    'variant',
    'adapter_event_kind',
    'notifications_sent',
    'official_result_submission',
    'automatic_ingestion_enabled'
  ].includes(key));
  if (scoreKeys.length > 0) return true;
  if (Object.keys(legChange).length > 0) return true;
  if (Object.keys(playerMapping).length > 0) return true;
  return liveStats.some(stat => stat && Object.values(stat).some(value => {
    if (typeof value === 'number') return value !== 0;
    if (typeof value === 'string') return value.trim() !== '';
    return value !== null && value !== undefined;
  }));
}

function liveEventFingerprint(normalized) {
  const eventType = normalized.normalizedEventType || normalized.storageEventType || 'unknown';
  const eventPayload = normalized.normalizedPayload || {};
  return safeHashPrefix(stableJson({
    eventType,
    scoreChange: normalized.scoreChange || {},
    legChange: normalized.legChange || {},
    playerMapping: normalized.playerMapping || {},
    liveStats: eventPayload.live_stats || []
  }));
}

function rememberEventFingerprint(subscription, fingerprint) {
  if (!fingerprint) return;
  if (!subscription.forwardedEventFingerprints.has(fingerprint)) {
    subscription.forwardedEventFingerprints.add(fingerprint);
    subscription.forwardedEventFingerprintOrder.push(fingerprint);
  }
  while (subscription.forwardedEventFingerprintOrder.length > 700) {
    const oldest = subscription.forwardedEventFingerprintOrder.shift();
    subscription.forwardedEventFingerprints.delete(oldest);
  }
}

function stableJson(value) {
  if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`;
  if (value && typeof value === 'object') {
    return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(',')}}`;
  }
  return JSON.stringify(value);
}

function safeHashPrefix(value) {
  let hash = 2166136261;
  for (let index = 0; index < value.length; index += 1) {
    hash ^= value.charCodeAt(index);
    hash = Math.imul(hash, 16777619);
  }
  return (hash >>> 0).toString(16).padStart(8, '0').slice(0, 8);
}

function loadDotEnv() {
  if (packagedCompanion) return;
  if (process.env.ADDC_BRIDGE_LOAD_ENV_LOCAL !== 'true') return;
  const envPath = resolve(process.cwd(), '.env.local');
  if (existsSync(envPath)) loadEnvFile(envPath);
}

function loadEnvFile(envPath) {
  const lines = readFileSync(envPath, 'utf8').split(/\r?\n/);
  for (const line of lines) {
    const trimmed = line.trim();
    if (!trimmed || trimmed.startsWith('#')) continue;
    const separator = trimmed.indexOf('=');
    if (separator <= 0) continue;
    const key = trimmed.slice(0, separator).trim();
    const value = trimmed.slice(separator + 1).trim().replace(/^['"]|['"]$/g, '');
    if (!process.env[key]) process.env[key] = value;
  }
}
