import type { ApiError, ApiResult, ErrorDiagnostic } from "@/types";
import { recordApiTiming } from "@/utils/rum";
import { recordWorkbenchApiRequest } from "@/utils/workbench-performance";

export interface ActivityRef {
  lastActivityAt: number;
  lastActivityIso?: string;
  waitingFor?: string | null;
  lastEventLabel?: string | null;
}

export type ActivityRefSource = (() => ActivityRef | null) | null;

export interface ApiRequestOptions extends RequestInit {
  timeoutMs?: number;
  timeoutName?: string;
  activityRef?: ActivityRef | ActivityRefSource;
}

export interface ApiErrorContext {
  error: string | null;
  apiError: ApiError | null;
  diagnostic: ErrorDiagnostic | null;
  status?: number | null;
}

const ACTIVITY_RESCHEDULER_MS = 1000;
const DEFAULT_TIMEOUT_MS = 4500;

export async function fetchJson<T>(path: string, options: ApiRequestOptions = {}): Promise<ApiResult<T>> {
  const controller = new AbortController();
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
  const activitySource = resolveActivitySource(options.activityRef);
  let timer: ReturnType<typeof setTimeout> | null = null;
  const startedAt = Date.now();
  const inactivityRef: { current: { idleMs: number; lastActivityAt: number; lastActivityIso?: string; waitingFor?: string | null; lastEventLabel?: string | null } | null } = { current: null };

  function readActivity(): ActivityRef {
    const fallback = { lastActivityAt: startedAt, lastActivityIso: new Date(startedAt).toISOString() };
    if (!activitySource) return fallback;
    const ref = activitySource();
    if (!ref || !Number.isFinite(ref.lastActivityAt) || ref.lastActivityAt <= 0) return fallback;
    return ref;
  }

  function schedule(): void {
    if (timer !== null) {
      clearTimeout(timer);
      timer = null;
    }
    if (!activitySource) {
      timer = setTimeout(() => controller.abort(), timeoutMs);
      return;
    }
    const activity = readActivity();
    const idleMs = Math.max(0, Date.now() - activity.lastActivityAt);
    const remaining = timeoutMs - idleMs;
    if (remaining <= 0) {
      inactivityRef.current = { idleMs, lastActivityAt: activity.lastActivityAt, lastActivityIso: activity.lastActivityIso, waitingFor: activity.waitingFor, lastEventLabel: activity.lastEventLabel };
      controller.abort();
      return;
    }
    timer = setTimeout(schedule, Math.max(250, Math.min(remaining, ACTIVITY_RESCHEDULER_MS)));
  }

  schedule();

  try {
    const response = await fetch(path, {
      ...options,
      credentials: options.credentials ?? "same-origin",
      headers: {
        accept: "application/json",
        ...(options.body ? { "content-type": "application/json" } : {}),
        ...(options.headers ?? {})
      },
      signal: controller.signal
    });
    const payload = await response.json().catch(() => null) as T | null;
    recordApiTiming({ route: path, method: options.method, status: response.status, startedAt, outcome: response.ok ? "ok" : "http_error" });
    recordWorkbenchApiRequest({ route: path, method: options.method, status: response.status, startedAtEpochMs: startedAt, endedAtEpochMs: Date.now(), outcome: response.ok ? "ok" : "http_error", timeoutName: options.timeoutName });
    const context = response.ok ? emptyErrorContext() : responseErrorContext(payload, response, path, `HTTP ${response.status}`);
    return { ok: response.ok, status: response.status, data: payload, error: context.error, apiError: context.apiError, diagnostic: context.diagnostic };
  } catch (error) {
    const outcome = error instanceof DOMException && error.name === "AbortError" ? "timeout" : "network_error";
    const activity = inactivityRef.current;
    recordApiTiming({ route: path, method: options.method, status: 0, startedAt, outcome });
    recordWorkbenchApiRequest({
      route: path,
      method: options.method,
      status: 0,
      startedAtEpochMs: startedAt,
      endedAtEpochMs: Date.now(),
      outcome,
      timeoutName: options.timeoutName,
      activityIdleMs: activity?.idleMs,
      activityWaitingFor: activity?.waitingFor,
      activityLastEventLabel: activity?.lastEventLabel,
      errorName: error instanceof Error ? error.name : undefined
    });
    if (error instanceof DOMException && error.name === "AbortError") {
      if (activity) {
        const detail = activity;
        return browserFailure<T>(path, options, `${options.timeoutName ?? path} 超过 ${timeoutMs}ms 无新活动（idle ${Math.round(detail.idleMs / 1000)}s；waitingFor=${detail.waitingFor ?? "unknown"}；lastEventLabel=${detail.lastEventLabel ?? "none"}）`, "browser_timeout", "timeout");
      }
      return browserFailure<T>(path, options, `${options.timeoutName ?? path} 超时`, "browser_timeout", "timeout");
    }
    return browserFailure<T>(path, options, error instanceof Error ? error.message : String(error), "browser_network_error", "network");
  } finally {
    if (timer !== null) clearTimeout(timer);
  }
}

export async function fetchText(path: string, options: ApiRequestOptions = {}): Promise<ApiResult<string>> {
  const startedAt = Date.now();
  try {
    const response = await fetch(path, { ...options, credentials: options.credentials ?? "same-origin" });
    const text = await response.text();
    recordApiTiming({ route: path, method: options.method, status: response.status, startedAt, outcome: response.ok ? "ok" : "http_error" });
    recordWorkbenchApiRequest({ route: path, method: options.method, status: response.status, startedAtEpochMs: startedAt, endedAtEpochMs: Date.now(), outcome: response.ok ? "ok" : "http_error", timeoutName: options.timeoutName });
    const context = response.ok ? emptyErrorContext() : responseErrorContext(null, response, path, `HTTP ${response.status}`);
    return { ok: response.ok, status: response.status, data: text, error: context.error, apiError: context.apiError, diagnostic: context.diagnostic };
  } catch (error) {
    recordApiTiming({ route: path, method: options.method, status: 0, startedAt, outcome: "network_error" });
    recordWorkbenchApiRequest({ route: path, method: options.method, status: 0, startedAtEpochMs: startedAt, endedAtEpochMs: Date.now(), outcome: "network_error", timeoutName: options.timeoutName, errorName: error instanceof Error ? error.name : undefined });
    return browserFailure<string>(path, options, error instanceof Error ? error.message : String(error), "browser_network_error", "network");
  }
}

export function apiErrorContextFromUnknown(err: unknown, fallback = "操作失败"): ApiErrorContext {
  if (isApiResultLike(err)) {
    const diagnostic = normalizeDiagnostic(err.diagnostic) ?? normalizeDiagnostic(err.apiError?.diagnostic) ?? null;
    const apiError = normalizeApiError(err.apiError, err.error ?? fallback, diagnostic);
    const message = firstString(err.error, apiError?.userMessage, apiError?.message, fallback) ?? fallback;
    return { error: message, apiError, diagnostic, status: err.status };
  }
  if (err instanceof Error) return { error: err.message || fallback, apiError: null, diagnostic: null };
  if (isRecord(err)) {
    const nested = isRecord(err.error) ? err.error : null;
    const diagnostic = normalizeDiagnostic(err.diagnostic) ?? normalizeDiagnostic(nested?.diagnostic) ?? null;
    const apiError = normalizeApiError(nested ?? err.apiError, firstString(err.message, err.error, fallback) ?? fallback, diagnostic);
    const message = firstString(err.message, err.reason, err.error, apiError?.userMessage, apiError?.message, fallback) ?? fallback;
    return { error: message, apiError, diagnostic, status: firstNumber(err.status) };
  }
  return { error: String(err || fallback), apiError: null, diagnostic: null };
}

function resolveActivitySource(ref: ApiRequestOptions["activityRef"]): ActivityRefSource {
  if (typeof ref === "function") return ref;
  if (ref && typeof ref === "object") return () => ref;
  return null;
}

function emptyErrorContext(): ApiErrorContext {
  return { error: null, apiError: null, diagnostic: null };
}

function responseErrorContext(payload: unknown, response: Response, path: string, fallback: string): ApiErrorContext {
  const diagnostic = responseDiagnostic(payload, response, path);
  const message = errorMessage(payload, fallback);
  return {
    error: message,
    apiError: responseApiError(payload, path, message, diagnostic),
    diagnostic,
    status: response.status
  };
}

function responseApiError(payload: unknown, path: string, message: string, diagnostic: ErrorDiagnostic): ApiError {
  const record = isRecord(payload) ? payload : null;
  const nested = record && isRecord(record.error) ? record.error : null;
  const source = nested ?? record ?? {};
  const normalized = normalizeApiError(source, message, diagnostic) ?? { message, diagnostic };
  return {
    ...normalized,
    message,
    userMessage: firstString(normalized.userMessage, message),
    code: firstStringOrNumber(normalized.code, diagnostic.code),
    retryable: firstBoolean(normalized.retryable, diagnostic.retryable),
    layer: firstString(normalized.layer, diagnostic.layer),
    category: firstString(normalized.category, diagnostic.category),
    route: firstString(normalized.route, diagnostic.route, path),
    traceId: firstString(normalized.traceId, diagnostic.traceId),
    requestId: firstString(normalized.requestId, diagnostic.requestId),
    source: firstString(normalized.source, diagnostic.source, "server"),
    diagnostic,
    valuesPrinted: normalized.valuesPrinted === true ? true : diagnostic.valuesPrinted === true
  };
}

function responseDiagnostic(payload: unknown, response: Response, path: string): ErrorDiagnostic {
  const record = isRecord(payload) ? payload : null;
  const nested = record && isRecord(record.error) ? record.error : null;
  const body = normalizeDiagnostic(nested?.diagnostic) ?? normalizeDiagnostic(record?.diagnostic) ?? {};
  const headerTraceId = firstString(response.headers.get("x-hwlab-otel-trace-id"), traceIdFromTraceparent(response.headers.get("traceparent")));
  const requestId = response.headers.get("x-request-id");
  return {
    ...body,
    contractVersion: firstString(body.contractVersion, "hwlab-error-diagnostic-v1"),
    traceId: firstString(headerTraceId, body.traceId, nested?.traceId, record?.traceId),
    requestId: firstString(requestId, body.requestId, nested?.requestId, record?.requestId),
    route: firstString(body.route, nested?.route, record?.route, path),
    layer: firstString(body.layer, nested?.layer, record?.layer),
    category: firstString(body.category, nested?.category, record?.category),
    code: firstStringOrNumber(body.code, nested?.code, record?.code),
    httpStatus: firstNumber(body.httpStatus, response.status) ?? response.status,
    serviceId: firstString(body.serviceId, nested?.serviceId, record?.serviceId),
    source: firstString(body.source, "server"),
    observedAt: firstString(body.observedAt, new Date().toISOString()),
    retryable: firstBoolean(body.retryable, nested?.retryable, record?.retryable),
    valuesPrinted: body.valuesPrinted === true
  };
}

function browserFailure<T>(path: string, options: ApiRequestOptions, message: string, code: string, category: string): ApiResult<T> {
  const diagnostic = browserDiagnostic(path, options, code, category);
  return {
    ok: false,
    status: 0,
    data: null,
    error: message,
    apiError: {
      message,
      userMessage: message,
      code,
      retryable: true,
      layer: "web",
      category,
      route: path,
      traceId: diagnostic.traceId,
      requestId: diagnostic.requestId,
      source: "browser",
      diagnostic,
      valuesPrinted: false
    },
    diagnostic
  };
}

function browserDiagnostic(path: string, options: ApiRequestOptions, code: string, category: string): ErrorDiagnostic {
  return {
    contractVersion: "hwlab-error-diagnostic-v1",
    traceId: randomTraceId(),
    requestId: randomRequestId(),
    route: path,
    layer: "web",
    category,
    code,
    httpStatus: 0,
    source: "browser",
    observedAt: new Date().toISOString(),
    retryable: true,
    timeoutName: options.timeoutName ?? null,
    valuesPrinted: false
  };
}

function normalizeApiError(value: unknown, fallback: string, diagnostic: ErrorDiagnostic | null): ApiError | null {
  if (!isRecord(value)) return null;
  const normalizedDiagnostic = normalizeDiagnostic(value.diagnostic) ?? diagnostic;
  return {
    ...value,
    message: firstString(value.message, value.userMessage, value.reason, value.code, fallback) ?? fallback,
    userMessage: firstString(value.userMessage, value.message),
    code: firstStringOrNumber(value.code, normalizedDiagnostic?.code),
    reason: firstString(value.reason),
    retryable: firstBoolean(value.retryable, normalizedDiagnostic?.retryable),
    layer: firstString(value.layer, normalizedDiagnostic?.layer),
    category: firstString(value.category, normalizedDiagnostic?.category),
    route: firstString(value.route, normalizedDiagnostic?.route),
    traceId: firstString(value.traceId, normalizedDiagnostic?.traceId),
    requestId: firstString(value.requestId, normalizedDiagnostic?.requestId),
    source: firstString(value.source, normalizedDiagnostic?.source),
    diagnostic: normalizedDiagnostic,
    valuesPrinted: value.valuesPrinted === true
  } as ApiError;
}

function normalizeDiagnostic(value: unknown): ErrorDiagnostic | null {
  if (!isRecord(value)) return null;
  return {
    ...value,
    contractVersion: firstString(value.contractVersion, value.contract_version),
    traceId: firstString(value.traceId, value.trace_id, value.otelTraceId, value.otel_trace_id),
    requestId: firstString(value.requestId, value.request_id),
    serviceId: firstString(value.serviceId, value.service_id),
    route: firstString(value.route),
    layer: firstString(value.layer),
    category: firstString(value.category),
    code: firstStringOrNumber(value.code),
    httpStatus: firstNumber(value.httpStatus, value.http_status),
    source: firstString(value.source),
    observedAt: firstString(value.observedAt, value.observed_at),
    retryable: firstBoolean(value.retryable),
    valuesPrinted: value.valuesPrinted === true
  };
}

function isApiResultLike(value: unknown): value is ApiResult<unknown> {
  return isRecord(value) && typeof value.ok === "boolean" && typeof value.status === "number" && "data" in value && "error" in value;
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
}

function firstString(...values: unknown[]): string | null {
  for (const value of values) {
    if (typeof value === "string" && value.trim()) return value.trim();
  }
  return null;
}

function firstStringOrNumber(...values: unknown[]): string | number | null {
  for (const value of values) {
    if (typeof value === "number" && Number.isFinite(value)) return value;
    if (typeof value === "string" && value.trim()) return value.trim();
  }
  return null;
}

function firstNumber(...values: unknown[]): number | null {
  for (const value of values) {
    if (typeof value === "number" && Number.isFinite(value)) return value;
    if (typeof value === "string" && value.trim() && Number.isFinite(Number(value))) return Number(value);
  }
  return null;
}

function firstBoolean(...values: unknown[]): boolean | null {
  for (const value of values) {
    if (typeof value === "boolean") return value;
  }
  return null;
}

function traceIdFromTraceparent(value: string | null): string | null {
  const match = /^[\da-f]{2}-([\da-f]{32})-[\da-f]{16}-[\da-f]{2}$/iu.exec(value?.trim() ?? "");
  const traceId = match?.[1]?.toLowerCase() ?? null;
  return traceId && !/^0+$/u.test(traceId) ? traceId : null;
}

function randomTraceId(): string {
  const bytes = new Uint8Array(16);
  if (globalThis.crypto?.getRandomValues) {
    globalThis.crypto.getRandomValues(bytes);
  } else {
    for (let index = 0; index < bytes.length; index += 1) bytes[index] = Math.floor(Math.random() * 256);
  }
  return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
}

function randomRequestId(): string {
  return `req_browser_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
}

function errorMessage(payload: unknown, fallback: string): string {
  if (payload && typeof payload === "object") {
    const record = payload as Record<string, unknown>;
    const nested = record.error && typeof record.error === "object" ? record.error as Record<string, unknown> : null;
    const candidate = nested?.userMessage ?? nested?.message ?? nested?.code ?? record.message ?? record.reason ?? record.error;
    if (typeof candidate === "string" && candidate.trim()) return candidate;
  }
  return fallback;
}
