const DEFAULT_SAMPLE_RATE = 0.2;
const DEFAULT_DEDUPE_TTL_MS = 5 * 60 * 1000;
const DEFAULT_MAX_EVENTS_PER_MINUTE = 20;
const MAX_MESSAGE_LENGTH = 280;
const MAX_FINGERPRINT_LENGTH = 96;
const MAX_META_ENTRIES = 12;
const MAX_META_VALUE_LENGTH = 140;
const MAX_META_JSON_LENGTH = 1800;

const EVENT_KEY_TO_TYPE = Object.freeze({
  warn_once: 'frontend.warn_once',
  ajax_error: 'frontend.ajax_error',
});

const EVENT_TYPE_TO_KEY = Object.freeze({
  'frontend.warn_once': 'warn_once',
  'frontend.ajax_error': 'ajax_error',
});

const DEFAULT_ALLOWED_EVENT_KEYS = Object.freeze(['warn_once', 'ajax_error']);
const DEFAULT_ALLOWED_EVENT_TYPES = new Set(DEFAULT_ALLOWED_EVENT_KEYS.map((key) => EVENT_KEY_TO_TYPE[key]));

const AJAX_ERROR_TYPE = EVENT_KEY_TO_TYPE.ajax_error;

const ALLOWED_META_KEYS = new Set([
  'action',
  'code',
  'component',
  'error_code',
  'feature',
  'http_method',
  'http_status',
  'location',
  'module',
  'page_request_id',
  'request_path',
  'source',
  'status_text',
]);

const seenFingerprints = new Map();
let fetchHookInstalled = false;
let clientRateWindowStartMs = 0;
let clientRateWindowCount = 0;

const normalizeWhitespace = (value) => String(value ?? '').replace(/\s+/g, ' ').trim();

const clampRate = (value, fallback = DEFAULT_SAMPLE_RATE) => {
  const numeric = Number.parseFloat(String(value ?? ''));
  if (!Number.isFinite(numeric)) {
    return fallback;
  }
  if (numeric < 0) {return 0;}
  if (numeric > 1) {return 1;}
  return numeric;
};

const clampPositiveInt = (value, fallback) => {
  const numeric = Number.parseInt(String(value ?? ''), 10);
  if (!Number.isFinite(numeric) || numeric < 1) {
    return fallback;
  }
  return numeric;
};

const normalizeEventType = (eventType) => {
  const normalized = normalizeWhitespace(eventType).toLowerCase();
  if (normalized in EVENT_KEY_TO_TYPE) {
    return EVENT_KEY_TO_TYPE[normalized];
  }
  if (normalized in EVENT_TYPE_TO_KEY) {
    return normalized;
  }
  return '';
};

const normalizeSeverity = (severity, eventType) => {
  const normalized = normalizeWhitespace(severity).toLowerCase();
  if (normalized === 'error' || normalized === 'warning' || normalized === 'info') {
    return normalized;
  }
  return eventType === AJAX_ERROR_TYPE ? 'error' : 'warning';
};

const normalizeRoute = (value) => {
  const raw = normalizeWhitespace(value);
  if (raw === '') {
    return normalizeWhitespace(window.location.pathname || '/') || '/';
  }
  try {
    const url = new URL(raw, window.location.href);
    if (url.origin !== window.location.origin) {
      return '';
    }
    return normalizeWhitespace(url.pathname || '/') || '/';
  } catch {
    const stripped = raw.split('?')[0].split('#')[0];
    return normalizeWhitespace(stripped || '/') || '/';
  }
};

const normalizeMessage = (value) => {
  const normalized = normalizeWhitespace(value);
  if (normalized === '') {
    return '';
  }
  return normalized.slice(0, MAX_MESSAGE_LENGTH);
};

const normalizeFingerprint = (value) => {
  const normalized = normalizeWhitespace(value).toLowerCase();
  if (normalized === '') {
    return '';
  }
  const safe = normalized.replace(/[^a-z0-9:_-]/g, '');
  return safe.slice(0, MAX_FINGERPRINT_LENGTH);
};

const hashString = (value) => {
  let hash = 2166136261;
  const source = String(value ?? '');
  for (let index = 0; index < source.length; index += 1) {
    hash ^= source.charCodeAt(index);
    hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
  }
  return (hash >>> 0).toString(16).padStart(8, '0');
};

const buildFingerprint = (eventType, message, route) => normalizeFingerprint(
  `v1_${hashString(`${eventType}|${route}|${message}`)}`
);

const normalizeOccurredAt = (value) => {
  const raw = normalizeWhitespace(value);
  if (raw !== '') {
    const timestamp = Date.parse(raw);
    if (Number.isFinite(timestamp)) {
      return new Date(timestamp).toISOString();
    }
  }
  return new Date().toISOString();
};

const normalizeMetaValue = (value) => {
  if (typeof value === 'boolean') {
    return value ? '1' : '0';
  }
  if (typeof value === 'number') {
    return Number.isFinite(value) ? String(value) : '';
  }
  if (typeof value === 'string') {
    return normalizeWhitespace(value).slice(0, MAX_META_VALUE_LENGTH);
  }
  if (value instanceof Error) {
    return normalizeWhitespace(value.name || 'Error').slice(0, MAX_META_VALUE_LENGTH);
  }
  if (value === null || value === undefined) {
    return '';
  }
  return normalizeWhitespace(String(value)).slice(0, MAX_META_VALUE_LENGTH);
};

const normalizeMeta = (meta) => {
  if (!meta || typeof meta !== 'object' || Array.isArray(meta)) {
    return {};
  }

  const normalized = {};
  let index = 0;

  Object.entries(meta).forEach(([rawKey, rawValue]) => {
    if (index >= MAX_META_ENTRIES) {
      return;
    }

    const key = normalizeWhitespace(rawKey).toLowerCase();
    if (key === '' || !ALLOWED_META_KEYS.has(key)) {
      return;
    }

    const value = normalizeMetaValue(rawValue);
    if (value === '') {
      return;
    }

    normalized[key] = value;
    index += 1;
  });

  return normalized;
};

const normalizeAllowedEventKeys = (value) => {
  const keys = new Set();
  const raw = Array.isArray(value)
    ? value
    : String(value ?? '')
      .split(/[,\n]/g)
      .map((entry) => entry.trim())
      .filter(Boolean);

  raw.forEach((entry) => {
    const key = String(entry).trim().toLowerCase();
    if (key in EVENT_KEY_TO_TYPE) {
      keys.add(key);
    } else if (key in EVENT_TYPE_TO_KEY) {
      keys.add(EVENT_TYPE_TO_KEY[key]);
    }
  });

  if (keys.size === 0) {
    DEFAULT_ALLOWED_EVENT_KEYS.forEach((key) => keys.add(key));
  }

  return keys;
};

const resolveConfig = () => {
  const root = document.documentElement;
  if (!root) {
    return {
      enabled: false,
      endpoint: '',
      csrfKey: '',
      csrfToken: '',
      sampleRate: DEFAULT_SAMPLE_RATE,
      maxEventsPerMinute: DEFAULT_MAX_EVENTS_PER_MINUTE,
      allowedEventTypes: DEFAULT_ALLOWED_EVENT_TYPES,
    };
  }

  const enabledRaw = normalizeWhitespace(root.dataset.telemetryEnabled || '');
  const enabled = enabledRaw !== '' && ['1', 'true', 'yes', 'on'].includes(enabledRaw.toLowerCase());

  const allowedEventKeys = normalizeAllowedEventKeys(root.dataset.telemetryAllowedEvents || '');
  const allowedEventTypes = new Set();
  allowedEventKeys.forEach((key) => {
    const eventType = EVENT_KEY_TO_TYPE[key];
    if (eventType) {
      allowedEventTypes.add(eventType);
    }
  });

  return {
    enabled,
    endpoint: normalizeWhitespace(root.dataset.telemetryUrl || ''),
    csrfKey: normalizeWhitespace(root.dataset.csrfKey || ''),
    csrfToken: normalizeWhitespace(root.dataset.csrfToken || ''),
    sampleRate: clampRate(root.dataset.telemetrySampleRate || DEFAULT_SAMPLE_RATE, DEFAULT_SAMPLE_RATE),
    maxEventsPerMinute: clampPositiveInt(
      root.dataset.telemetryMaxEventsPerMinute || DEFAULT_MAX_EVENTS_PER_MINUTE,
      DEFAULT_MAX_EVENTS_PER_MINUTE
    ),
    allowedEventTypes: allowedEventTypes.size > 0 ? allowedEventTypes : DEFAULT_ALLOWED_EVENT_TYPES,
  };
};

const shouldSample = (rate) => {
  if (rate <= 0) {return false;}
  if (rate >= 1) {return true;}
  return Math.random() < rate;
};

const trimSeenFingerprints = (now = Date.now()) => {
  seenFingerprints.forEach((expiresAt, key) => {
    if (expiresAt <= now) {
      seenFingerprints.delete(key);
    }
  });
};

const isDuplicateFingerprint = (fingerprint, ttlMs, now = Date.now()) => {
  trimSeenFingerprints(now);
  const expiresAt = seenFingerprints.get(fingerprint) || 0;
  if (expiresAt > now) {
    return true;
  }
  seenFingerprints.set(fingerprint, now + ttlMs);
  return false;
};

const hasClientRateBudget = (maxEventsPerMinute, nowMs = Date.now()) => {
  const limit = clampPositiveInt(maxEventsPerMinute, DEFAULT_MAX_EVENTS_PER_MINUTE);
  const windowMs = 60 * 1000;
  if (clientRateWindowStartMs <= 0 || (nowMs - clientRateWindowStartMs) >= windowMs) {
    clientRateWindowStartMs = nowMs;
    clientRateWindowCount = 0;
  }

  if (clientRateWindowCount >= limit) {
    return false;
  }

  clientRateWindowCount += 1;
  return true;
};

const toUrlSearchParams = (payload, config) => {
  const body = new URLSearchParams();
  body.set('event_type', payload.eventType);
  body.set('severity', payload.severity);
  body.set('message', payload.message);
  body.set('fingerprint', payload.fingerprint);
  body.set('occurred_at', payload.occurredAt);

  const metaJson = JSON.stringify(payload.meta);
  if (metaJson && metaJson.length <= MAX_META_JSON_LENGTH) {
    body.set('meta', metaJson);
  }

  body.set(config.csrfKey, config.csrfToken);
  return body;
};

const sendPayload = (payload, config) => {
  if (!config.endpoint || !config.csrfKey || !config.csrfToken) {
    return false;
  }

  const body = toUrlSearchParams(payload, config);

  try {
    if (navigator.sendBeacon && navigator.sendBeacon(config.endpoint, body)) {
      return true;
    }
  } catch {
    // continue with fetch fallback.
  }

  try {
    void fetch(config.endpoint, {
      method: 'POST',
      credentials: 'same-origin',
      keepalive: true,
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
        'Accept': 'application/json',
        'X-Requested-With': 'fetch',
        'X-Telemetry-Request': '1',
      },
      body,
    }).catch(() => {});
    return true;
  } catch {
    return false;
  }
};

const headerValue = (headers, key) => {
  if (!headers) {
    return '';
  }

  if (typeof Headers !== 'undefined' && headers instanceof Headers) {
    return normalizeWhitespace(headers.get(key) || '');
  }

  if (Array.isArray(headers)) {
    const tuple = headers.find((entry) => Array.isArray(entry) && String(entry[0] || '').toLowerCase() === key.toLowerCase());
    return tuple ? normalizeWhitespace(tuple[1] || '') : '';
  }

  if (typeof headers === 'object') {
    const direct = headers[key] ?? headers[key.toLowerCase()] ?? headers[key.toUpperCase()];
    return normalizeWhitespace(direct || '');
  }

  return '';
};

const fetchInputUrl = (input) => {
  if (typeof input === 'string') {
    return input;
  }
  if (input instanceof URL) {
    return input.toString();
  }
  if (typeof Request !== 'undefined' && input instanceof Request) {
    return input.url;
  }
  return '';
};

const fetchInputMethod = (input, init) => {
  const initMethod = normalizeWhitespace(init?.method || '').toUpperCase();
  if (initMethod !== '') {
    return initMethod;
  }
  if (typeof Request !== 'undefined' && input instanceof Request) {
    const requestMethod = normalizeWhitespace(input.method || '').toUpperCase();
    if (requestMethod !== '') {
      return requestMethod;
    }
  }
  return 'GET';
};

const isTelemetryFetch = (input, init, telemetryPath) => {
  if (telemetryPath === '') {
    return false;
  }

  const requestHeaders = init?.headers;
  const inputHeaders = typeof Request !== 'undefined' && input instanceof Request ? input.headers : null;
  const marker = headerValue(requestHeaders, 'X-Telemetry-Request') || headerValue(inputHeaders, 'X-Telemetry-Request');
  if (marker === '1') {
    return true;
  }

  const requestPath = normalizeRoute(fetchInputUrl(input));
  return requestPath !== '' && requestPath === telemetryPath;
};

const shouldTrackFetch = (input, init, config) => {
  if (!config.enabled || !config.allowedEventTypes.has(AJAX_ERROR_TYPE)) {
    return false;
  }

  const rawUrl = fetchInputUrl(input);
  if (rawUrl === '') {
    return false;
  }

  const path = normalizeRoute(rawUrl);
  if (path === '') {
    return false;
  }

  const telemetryPath = normalizeRoute(config.endpoint);
  if (isTelemetryFetch(input, init, telemetryPath)) {
    return false;
  }

  return true;
};

export const telemetry = {
  capture(eventType, payload = {}, options = {}) {
    if (typeof document === 'undefined' || typeof window === 'undefined') {
      return false;
    }

    const normalizedEventType = normalizeEventType(eventType);
    if (normalizedEventType === '') {
      return false;
    }

    const config = resolveConfig();
    if (!config.enabled || !config.allowedEventTypes.has(normalizedEventType)) {
      return false;
    }

    const message = normalizeMessage(payload.message || '');
    if (message === '') {
      return false;
    }

    const route = normalizeRoute(payload.route || window.location.pathname || '/');
    const fingerprint = normalizeFingerprint(payload.fingerprint || '')
      || buildFingerprint(normalizedEventType, message, route);
    if (fingerprint === '') {
      return false;
    }

    const dedupeTtlMs = Math.max(0, Number.parseInt(String(options.dedupeTtlMs ?? DEFAULT_DEDUPE_TTL_MS), 10) || 0);
    if (!options.skipDedupe && dedupeTtlMs > 0 && isDuplicateFingerprint(fingerprint, dedupeTtlMs)) {
      return false;
    }

    const sampleRate = clampRate(
      options.sampleRate !== undefined ? options.sampleRate : config.sampleRate,
      config.sampleRate
    );
    if (!shouldSample(sampleRate)) {
      return false;
    }

    const maxEventsPerMinute = clampPositiveInt(
      options.maxEventsPerMinute !== undefined ? options.maxEventsPerMinute : config.maxEventsPerMinute,
      config.maxEventsPerMinute
    );
    if (!options.skipClientRateLimit && !hasClientRateBudget(maxEventsPerMinute)) {
      return false;
    }

    const meta = normalizeMeta(payload.meta || {});
    if (route !== '') {
      meta.location = route;
    }
    const pageRequestId = normalizeWhitespace(document.documentElement?.dataset.requestId || '');
    if (pageRequestId !== '') {
      meta.page_request_id = pageRequestId.slice(0, 36);
    }

    return sendPayload({
      eventType: normalizedEventType,
      severity: normalizeSeverity(payload.severity, normalizedEventType),
      message,
      fingerprint,
      meta,
      occurredAt: normalizeOccurredAt(payload.occurredAt),
    }, config);
  },

  installGlobalFetchHook() {
    if (fetchHookInstalled || typeof window === 'undefined' || typeof window.fetch !== 'function') {
      return;
    }

    const originalFetch = window.fetch.bind(window);
    window.fetch = (input, init) => {
      const config = resolveConfig();
      if (!shouldTrackFetch(input, init, config)) {
        return originalFetch(input, init);
      }

      const method = fetchInputMethod(input, init);
      const requestPath = normalizeRoute(fetchInputUrl(input));

      return originalFetch(input, init)
        .then((response) => {
          const shouldTrackStatus = response.status >= 500 || response.status === 429;
          if (!response.ok && shouldTrackStatus) {
            telemetry.capture(AJAX_ERROR_TYPE, {
              severity: response.status >= 500 ? 'error' : 'warning',
              message: `AJAX request failed (${response.status}) ${method} ${requestPath || '/'}`,
              meta: {
                source: 'fetch',
                module: 'global-fetch',
                http_method: method,
                http_status: response.status,
                status_text: response.statusText || '',
                request_path: requestPath || '/',
              },
            });
          }
          return response;
        })
        .catch((error) => {
          if (!(error instanceof Error && error.name === 'AbortError')) {
            telemetry.capture(AJAX_ERROR_TYPE, {
              severity: 'error',
              message: `AJAX network error ${method} ${requestPath || '/'}`,
              meta: {
                source: 'fetch',
                module: 'global-fetch',
                http_method: method,
                request_path: requestPath || '/',
                error_code: error instanceof Error ? error.name : 'NetworkError',
              },
            });
          }
          throw error;
        });
    };

    fetchHookInstalled = true;
  },
};

if (typeof window !== 'undefined') {
  telemetry.installGlobalFetchHook();
}

export { DEFAULT_DEDUPE_TTL_MS };
