/**
 * ThumbmarkJS: Main fingerprinting and API logic
 *
 * This module handles component collection, API calls, uniqueness scoring, and data filtering
 * for the ThumbmarkJS browser fingerprinting library.
 *
 */

import { defaultOptions, OptionsAfterDefaults, optionsInterface } from "../options";
import {
  timeoutInstance,
  componentInterface,
  componentFunctionInterface,
  tm_component_promises,
  customComponents,
  tm_experimental_component_promises,
  includeComponent as globalIncludeComponent
} from "../factory";
import { hash } from "../utils/hash";
import { raceAllPerformance } from "../utils/raceAll";
import { getVersion } from "../utils/version";
import { filterThumbmarkData, getExcludeList } from './filterComponents'
import { logThumbmarkData } from '../utils/log';
import { getApiPromise, ApiError, infoInterface } from "./api";
import { stableStringify } from "../utils/stableStringify";


/**
 * Final thumbmark response structure
 */
export interface ThumbmarkError {
  type: 'component_timeout' | 'component_error' | 'api_timeout' | 'api_error' | 'api_unauthorized' | 'network_error' | 'fatal';
  message: string;
  component?: string;
}

export interface ThumbmarkResponse {
  /** Hash of all components - the main fingerprint identifier */
  thumbmark: string;
  /** All resolved fingerprint components */
  components: componentInterface;
  /** Information from the API (IP, classification, uniqueness score) */
  info: infoInterface;
  /** Library version */
  version: string;
  /** Persistent visitor identifier (requires API key) */
  visitorId?: string;
  /** Performance timing for each component (only when options.performance is true) */
  elapsed?: Record<string, number>;
  /** Structured error array. Present only when errors occurred. */
  error?: ThumbmarkError[];
  /** Experimental components (only when options.experimental is true) */
  experimental?: componentInterface;
  /** Unique identifier for this API request */
  requestId?: string;
  /** Metadata echoed back from the API */
  metadata?: string | object;
}

/**
 * Main entry point: collects all components, optionally calls API, and returns thumbmark data.
 *
 * @param options - Options for fingerprinting and API
 * @returns ThumbmarkResponse (elapsed is present only if options.performance is true)
 */
export async function getThumbmark(
  options?: optionsInterface,
  instanceCustomComponents: Record<string, componentFunctionInterface | null> = {}
): Promise<ThumbmarkResponse> {
  // Early exit for non-browser environments (Node.js, Jest, SSR)
  if (typeof document === 'undefined' || typeof window === 'undefined') {
    return {
      thumbmark: '',
      components: {},
      info: {},
      version: getVersion(),
      error: [{ type: 'fatal', message: 'Browser environment required' }]
    };
  }

  try {
    const _options = { ...defaultOptions, ...options } as OptionsAfterDefaults;
    const allErrors: ThumbmarkError[] = [];

    // Early logging decision
    const shouldLog = (_options.logging && !sessionStorage.getItem("_tmjs_l") && Math.random() < 0.0001);

    // Merge built-in and user-registered components
    const allComponents = {
      ...tm_component_promises,
      ...customComponents,
      ...instanceCustomComponents,
    } as Record<string, componentFunctionInterface>;
    const { elapsed, resolvedComponents: clientComponentsResult, errors: componentErrors, pipelineTimings: mainPipelineTimings } = await resolveClientComponents(allComponents, _options);
    allErrors.push(...componentErrors);

    // Resolve experimental components only when logging
    let experimentalComponents = {};
    let experimentalElapsed = {};
    let expPipelineTimings: Record<string, number> = {};
    if (shouldLog || _options.experimental) {
      const { elapsed: expElapsed, resolvedComponents, errors: expErrors, pipelineTimings: expTimings } = await resolveClientComponents(tm_experimental_component_promises, _options);
      experimentalComponents = resolvedComponents;
      experimentalElapsed = expElapsed;
      expPipelineTimings = expTimings;
      allErrors.push(...expErrors);
    }

    const apiPromise = _options.api_key ? getApiPromise(_options, clientComponentsResult) : null;
    let apiResult = null;

    if (apiPromise) {
      try {
        apiResult = await apiPromise;
      } catch (error) {
        if (error instanceof ApiError && error.status === 403) {
          return {
            error: [{ type: 'api_unauthorized', message: 'Invalid API key or quota exceeded' }],
            components: {},
            info: {},
            version: getVersion(),
            thumbmark: ''
          };
        }
        allErrors.push({
          type: error instanceof ApiError ? 'api_error' : 'network_error',
          message: error instanceof Error ? error.message : String(error)
        });
      }
    }

    // Surface API timeout as a structured error
    if (apiResult?.info?.timed_out) {
      allErrors.push({ type: 'api_timeout', message: 'API request timed out' });
    }

    const filterStart = performance.now();
    const apiComponents = filterThumbmarkData(apiResult?.components || {}, _options);
    const filterMs = performance.now() - filterStart;

    const components = { ...clientComponentsResult, ...apiComponents };
    const info: infoInterface = apiResult?.info || { uniqueness: { score: 'api only' } };

    // Use API thumbmark if available to ensure API/client sync, otherwise calculate locally
    let thumbmark: string;
    let stringifyMs = 0;
    let hashMs = 0;
    if (apiResult?.thumbmark) {
      thumbmark = apiResult.thumbmark;
    } else {
      const stringifyStart = performance.now();
      const stringified = stableStringify(components);
      stringifyMs = performance.now() - stringifyStart;

      const hashStart = performance.now();
      thumbmark = hash(stringified);
      hashMs = performance.now() - hashStart;
    }

    const version = getVersion();

    // Only log to server when not in debug mode
    if (shouldLog) {
      logThumbmarkData(thumbmark, components, _options, experimentalComponents, allErrors).catch(() => { /* do nothing */ });
    }

    // Accumulate _pipeline timings from both the main and (if run) experimental component phases.
    // Filter time includes: main component filter + optional experimental filter + apiComponents filter.
    const expFilterMs = expPipelineTimings['_pipeline.filter'] ?? 0;
    const _pipelineTimings: Record<string, number> = {
      '_pipeline.dispatch': mainPipelineTimings['_pipeline.dispatch'],
      '_pipeline.resolve': mainPipelineTimings['_pipeline.resolve'],
      '_pipeline.filter': mainPipelineTimings['_pipeline.filter'] + expFilterMs + filterMs,
      '_pipeline.stringify': stringifyMs,
      '_pipeline.hash': hashMs,
      '_pipeline.assembly': 0, // placeholder, updated below after result construction
    };

    // Only add 'elapsed' if performance is true
    // allElapsed holds a live reference to _pipelineTimings entries via spread — we update assembly after.
    // mainPipelineTimings contains both _pipeline.* keys (overridden by _pipelineTimings below) and
    // _dispatch.<name> keys (per-component sync prelude timings) that flow through unchanged.
    const allElapsed: Record<string, number> = { ...elapsed, ...experimentalElapsed, ...mainPipelineTimings, ..._pipelineTimings };
    const maybeElapsed = _options.performance ? { elapsed: allElapsed } : {};

    const assemblyStart = performance.now();
    const result: ThumbmarkResponse = {
      ...(apiResult?.visitorId && { visitorId: apiResult.visitorId }),
      thumbmark,
      components: components,
      info,
      version,
      ...maybeElapsed,
      ...(allErrors.length > 0 && { error: allErrors }),
      ...(Object.keys(experimentalComponents).length > 0 && _options.experimental && { experimental: experimentalComponents }),
      ...(apiResult?.requestId && { requestId: apiResult.requestId }),
      ...(apiResult?.metadata && { metadata: apiResult.metadata }),
    };
    // Update assembly timing in allElapsed directly (allElapsed is the same object referenced by result.elapsed).
    allElapsed['_pipeline.assembly'] = performance.now() - assemblyStart;

    return result;
  } catch (e) {
    return {
      thumbmark: '',
      components: {},
      info: {},
      version: getVersion(),
      error: [{ type: 'fatal', message: e instanceof Error ? e.message : String(e) }],
    };
  }
}

// ===================== Component Resolution & Performance =====================

/**
 * Resolves and times all filtered component promises from a component function map.
 *
 * @param comps - Map of component functions
 * @param options - Options for filtering and timing
 * @returns Object with elapsed times, filtered resolved components, errors, and pipeline phase timings
 */
export async function resolveClientComponents(
  comps: { [key: string]: (options?: optionsInterface) => Promise<componentInterface | null> },
  options?: optionsInterface
): Promise<{ elapsed: Record<string, number>, resolvedComponents: componentInterface, errors: ThumbmarkError[], pipelineTimings: Record<string, number> }> {
  const opts = { ...defaultOptions, ...options };
  const topLevelExcludes = getExcludeList(opts).filter(e => !e.includes('.'));
  const filtered = Object.entries(comps)
    .filter(([key]) => !opts?.exclude?.includes(key))
    .filter(([key]) => !topLevelExcludes.includes(key))
    .filter(([key]) =>
      opts?.include?.some(e => e.includes('.'))
        ? opts?.include?.some(e => e.startsWith(key))
        : opts?.include?.length === 0 || opts?.include?.includes(key)
    );
  const keys = filtered.map(([key]) => key);

  const perComponentDispatch: Record<string, number> = {};
  const dispatchStart = performance.now();
  const promises = filtered.map(([key, fn]) => {
    const t0 = performance.now();
    const p = fn(options);
    perComponentDispatch[`_dispatch.${key}`] = performance.now() - t0;
    return p;
  });
  const dispatchMs = performance.now() - dispatchStart;

  const resolveStart = performance.now();
  const resolvedValues = await raceAllPerformance(promises, opts?.timeout || 5000, timeoutInstance);
  const resolveMs = performance.now() - resolveStart;

  const elapsed: Record<string, number> = {};
  const resolvedComponentsRaw: Record<string, componentInterface> = {};
  const errors: ThumbmarkError[] = [];

  resolvedValues.forEach((result, index) => {
    const key = keys[index];
    elapsed[key] = result.elapsed ?? 0;

    if (result.error === 'timeout') {
      errors.push({ type: 'component_timeout', message: `Component '${key}' timed out`, component: key });
    } else if (result.error) {
      errors.push({ type: 'component_error', message: result.error, component: key });
    }

    if (result.value != null) {
      resolvedComponentsRaw[key] = result.value;
    }
  });

  const filterStart = performance.now();
  const resolvedComponents = filterThumbmarkData(resolvedComponentsRaw, opts);
  const filterMs = performance.now() - filterStart;

  const pipelineTimings: Record<string, number> = {
    '_pipeline.dispatch': dispatchMs,
    '_pipeline.resolve': resolveMs,
    '_pipeline.filter': filterMs,
    ...perComponentDispatch,
  };

  return { elapsed, resolvedComponents, errors, pipelineTimings };
}

export { globalIncludeComponent as includeComponent };
