/**
 * Browser/React Native-safe feature flag client for platform-service.
 *
 * Supports two modes:
 * 1. **Polling** (default) — GET /flags/poll on a configurable interval
 * 2. **Streaming** — SSE via GET /flags/stream for real-time updates
 *
 * Both modes support multi-variate evaluation via POST /flags/evaluate.
 * No Node.js dependencies — uses globalThis.fetch and EventSource.
 *
 * @example
 * ```ts
 * import { createFeatureFlagClient } from '@bytelyst/feature-flag-client';
 *
 * const flags = createFeatureFlagClient({
 *   baseUrl: 'http://localhost:4003/api',
 *   productId: 'nomgap',
 *   platform: 'mobile',
 * });
 *
 * await flags.init({ userId: 'user-123' });
 *
 * // Boolean check (legacy)
 * if (flags.isEnabled('premium_body_viz')) { ... }
 *
 * // Multi-variate
 * const color = flags.getValue('cta_color', '#000000');
 * const config = flags.getValue('rate_limits', { maxReqs: 100 });
 *
 * // Listen for changes (SSE or polling refresh)
 * const unsub = flags.onChange((key) => console.log(`${key} changed`));
 * ```
 */

import type { FeatureFlagClient, FeatureFlagClientConfig, EvaluationResult } from './types.js';

export function createFeatureFlagClient(config: FeatureFlagClientConfig): FeatureFlagClient {
  const {
    baseUrl,
    productId,
    platform,
    pollIntervalMs = 5 * 60 * 1000,
    storage,
    storagePrefix,
    useStreaming = false,
    getAccessToken,
  } = config;

  const prefix = storagePrefix ?? productId;
  const BOOL_KEY = `${prefix}-feature-flags`;
  const EVAL_KEY = `${prefix}-feature-evals`;

  let boolFlags: Record<string, boolean> = {};
  let evaluations: Record<string, EvaluationResult> = {};
  let initialized = false;
  let intervalId: ReturnType<typeof setInterval> | null = null;
  // eslint-disable-next-line no-undef
  let eventSource: InstanceType<typeof EventSource> | null = null;
  let userId: string | undefined;
  const listeners = new Set<(flagKey: string) => void>();

  // Restore from storage on creation
  if (storage) {
    try {
      const cached = storage.getItem(BOOL_KEY);
      if (cached) boolFlags = JSON.parse(cached);
    } catch {
      /* Ignore parse errors */
    }
    try {
      const cached = storage.getItem(EVAL_KEY);
      if (cached) evaluations = JSON.parse(cached);
    } catch {
      /* Ignore parse errors */
    }
  }

  function buildHeaders(): Record<string, string> {
    const requestId =
      typeof globalThis.crypto?.randomUUID === 'function'
        ? globalThis.crypto.randomUUID()
        : `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;

    const headers: Record<string, string> = {
      'x-product-id': productId,
      'x-request-id': requestId,
    };
    if (getAccessToken) {
      const token = getAccessToken();
      if (token) headers['authorization'] = `Bearer ${token}`;
    }
    return headers;
  }

  function notifyListeners(flagKey: string): void {
    for (const listener of listeners) {
      try {
        listener(flagKey);
      } catch {
        /* best-effort */
      }
    }
  }

  function persistToStorage(): void {
    if (!storage) return;
    try {
      storage.setItem(BOOL_KEY, JSON.stringify(boolFlags));
    } catch {
      /* non-fatal */
    }
    try {
      storage.setItem(EVAL_KEY, JSON.stringify(evaluations));
    } catch {
      /* non-fatal */
    }
  }

  async function fetchBoolFlags(): Promise<void> {
    try {
      const parts = [`platform=${encodeURIComponent(platform)}`];
      if (userId) parts.push(`userId=${encodeURIComponent(userId)}`);

      const res = await globalThis.fetch(`${baseUrl}/flags/poll?${parts.join('&')}`, {
        headers: buildHeaders(),
      });

      if (!res.ok) return;

      const data = (await res.json()) as { flags?: Record<string, boolean> };
      const prev = boolFlags;
      boolFlags = data.flags ?? {};

      // Detect changes and notify
      const allKeys = new Set([...Object.keys(prev), ...Object.keys(boolFlags)]);
      for (const key of allKeys) {
        if (prev[key] !== boolFlags[key]) notifyListeners(key);
      }

      persistToStorage();
    } catch {
      // Keep existing flags on network error
    }
  }

  async function fetchEvaluations(): Promise<void> {
    try {
      const body: Record<string, unknown> = { platform };
      if (userId) body.userId = userId;

      const res = await globalThis.fetch(`${baseUrl}/flags/evaluate`, {
        method: 'POST',
        headers: { ...buildHeaders(), 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
      });

      if (!res.ok) return;

      const data = (await res.json()) as { evaluations?: Record<string, EvaluationResult> };
      const prev = evaluations;
      evaluations = data.evaluations ?? {};

      // Also update bool flags for backward compatibility
      for (const [key, result] of Object.entries(evaluations)) {
        const newBool =
          result.reason !== 'off' &&
          result.reason !== 'prerequisite_failed' &&
          result.reason !== 'schedule_inactive' &&
          result.reason !== 'error';
        if (boolFlags[key] !== newBool) {
          boolFlags[key] = newBool;
          notifyListeners(key);
        } else if (JSON.stringify(prev[key]?.value) !== JSON.stringify(result.value)) {
          notifyListeners(key);
        }
      }

      persistToStorage();
    } catch {
      // Keep existing evaluations on network error
    }
  }

  async function fetchAll(): Promise<void> {
    await Promise.all([fetchBoolFlags(), fetchEvaluations()]);
  }

  function startStreaming(): void {
    if (typeof globalThis.EventSource === 'undefined') {
      // SSE not available (e.g. React Native) — fall back to polling
      intervalId = setInterval(() => {
        void fetchAll();
      }, pollIntervalMs);
      return;
    }

    const parts = [`productId=${encodeURIComponent(productId)}`];
    if (getAccessToken) {
      const token = getAccessToken();
      if (token) parts.push(`token=${encodeURIComponent(token)}`);
    }
    const url = `${baseUrl}/flags/stream?${parts.join('&')}`;
    eventSource = new globalThis.EventSource(url);

    eventSource.onmessage = event => {
      try {
        const data = JSON.parse(event.data) as { type?: string; flagKey?: string };
        if (data.type === 'flag_change' && data.flagKey) {
          // Re-fetch all evaluations on any flag change
          void fetchAll().then(() => notifyListeners(data.flagKey!));
        }
      } catch {
        /* Ignore parse errors */
      }
    };

    eventSource.onerror = () => {
      // Reconnect handled automatically by EventSource spec
    };
  }

  async function init(params?: { userId?: string }): Promise<void> {
    if (initialized) return;
    initialized = true;
    userId = params?.userId;

    await fetchAll();

    if (useStreaming) {
      startStreaming();
    } else {
      intervalId = setInterval(() => {
        void fetchAll();
      }, pollIntervalMs);
    }
  }

  function isEnabled(key: string): boolean {
    return boolFlags[key] === true;
  }

  function getValue<T = boolean | string | number | Record<string, unknown>>(
    key: string,
    defaultValue: T
  ): T {
    const result = evaluations[key];
    if (!result) return defaultValue;
    if (result.reason === 'off' || result.reason === 'error') return defaultValue;
    return result.value as T;
  }

  function getEvaluation(key: string): EvaluationResult | undefined {
    return evaluations[key];
  }

  function getAllFlags(): Readonly<Record<string, boolean>> {
    return boolFlags;
  }

  function getAllEvaluations(): Readonly<Record<string, EvaluationResult>> {
    return evaluations;
  }

  async function refresh(): Promise<void> {
    await fetchAll();
  }

  function onChange(listener: (flagKey: string) => void): () => void {
    listeners.add(listener);
    return () => {
      listeners.delete(listener);
    };
  }

  function stop(): void {
    if (intervalId) clearInterval(intervalId);
    intervalId = null;
    if (eventSource) {
      eventSource.close();
      eventSource = null;
    }
    boolFlags = {};
    evaluations = {};
    initialized = false;
    userId = undefined;
    listeners.clear();
  }

  return {
    init,
    isEnabled,
    getValue,
    getEvaluation,
    getAllFlags,
    getAllEvaluations,
    refresh,
    onChange,
    stop,
  };
}
