import { displayTimeLabel, fmtDate } from "./time";

type AnyRecord = Record<string, any>;

export type UniDeskFailureField = string | false;

export interface UniDeskRequestJsonOptions extends Omit<RequestInit, "body"> {
  body?: BodyInit | AnyRecord | any[] | null;
  failureFields?: UniDeskFailureField[];
  strictJson?: boolean;
  retryInvalidJson?: number;
  retryDelayMs?: number;
  invalidJsonPrefix?: string;
  invalidJsonPreview?: boolean;
  responsePreviewLength?: number;
}

export interface UniDeskRequestErrorMeta {
  kind: "http" | "network" | "parse";
  method: string;
  url: string;
  occurredAt: string;
  status?: number;
  statusText?: string;
  upstreamMessage?: string;
  responsePreview?: string;
  parseError?: string;
}

export interface UniDeskErrorView {
  title: string;
  message: string;
  status?: number;
  statusText?: string;
  method?: string;
  url?: string;
  occurredAt?: string;
  responsePreview?: string;
  parseError?: string;
  structured: boolean;
}

export class UniDeskRequestError extends Error {
  readonly unideskRequestError = true;
  readonly meta: UniDeskRequestErrorMeta;

  constructor(message: string, meta: UniDeskRequestErrorMeta) {
    super(message);
    this.name = "UniDeskRequestError";
    this.meta = meta;
  }
}

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function basicErrorMessage(error: unknown, fallback = "操作失败"): string {
  return error instanceof Error ? error.message : String(error || fallback);
}

function safePreview(value: unknown, max = 500): string {
  if (value === null || value === undefined) return "";
  const raw = typeof value === "string" ? value : JSON.stringify(value);
  const text = String(raw || "").replace(/\s+/gu, " ").trim();
  return text.length > max ? `${text.slice(0, max)}...` : text;
}

function requestUrl(path: string): string {
  try {
    const origin = typeof location !== "undefined" && location.origin ? location.origin : "http://localhost";
    return new URL(path, origin).toString();
  } catch {
    return path;
  }
}

function requestMethod(options: AnyRecord): string {
  return String(options.method || "GET").toUpperCase();
}

function isJsonBody(value: unknown): boolean {
  if (value === null || value === undefined) return false;
  if (typeof value !== "object") return false;
  if (typeof Blob !== "undefined" && value instanceof Blob) return false;
  if (typeof FormData !== "undefined" && value instanceof FormData) return false;
  if (typeof URLSearchParams !== "undefined" && value instanceof URLSearchParams) return false;
  if (typeof ArrayBuffer !== "undefined" && value instanceof ArrayBuffer) return false;
  return true;
}

function normalizeFetchOptions(options: UniDeskRequestJsonOptions): RequestInit {
  const headers = new Headers(options.headers || {});
  const body = isJsonBody(options.body) ? JSON.stringify(options.body) : options.body;
  if (body && !headers.has("content-type") && typeof body === "string") headers.set("content-type", "application/json");
  return { ...options, credentials: options.credentials || "same-origin", body: body as BodyInit | null | undefined, headers };
}

function responseMessage(body: any): string {
  if (body?.error && typeof body.error === "object" && typeof body.error.message === "string") return body.error.message;
  if (typeof body?.error === "string") return body.error;
  if (typeof body?.message === "string") return body.message;
  if (typeof body?.detail === "string") return body.detail;
  return "";
}

function emitAuthRequired(path: string, method: string, occurredAt: Date): void {
  if (typeof window === "undefined" || typeof window.dispatchEvent !== "function") return;
  window.dispatchEvent(new CustomEvent("unidesk-auth-required", {
    detail: { path: requestUrl(path), method, occurredAt: occurredAt.toISOString() },
  }));
}

function isAuthRequiredResponse(status: number, message: string): boolean {
  return status === 401 && message.trim().toLowerCase() === "authentication required";
}

function bodyIndicatesFailure(body: any, fields: UniDeskFailureField[]): boolean {
  if (!body || typeof body !== "object" || Array.isArray(body)) return false;
  return fields.some((field) => field !== false && body[field] === false);
}

function requestMeta(
  kind: UniDeskRequestErrorMeta["kind"],
  path: string,
  method: string,
  occurredAt: Date,
  extra: Partial<UniDeskRequestErrorMeta> = {},
): UniDeskRequestErrorMeta {
  return {
    kind,
    method,
    url: requestUrl(path),
    occurredAt: occurredAt.toISOString(),
    ...extra,
  };
}

function httpTitle(status?: number, statusText?: string): string {
  if (!status) return "请求失败";
  return `HTTP ${status}${statusText ? ` ${statusText}` : ""}`;
}

function parseJson(text: string): { body: any; parseError: string } {
  try {
    return { body: text ? JSON.parse(text) : null, parseError: "" };
  } catch (error) {
    return { body: { text }, parseError: basicErrorMessage(error, "parse failed") };
  }
}

export async function requestJson(path: string, options: UniDeskRequestJsonOptions = {}, attempt = 0): Promise<any> {
  const {
    failureFields = ["ok"],
    strictJson = false,
    retryInvalidJson = 0,
    retryDelayMs = 120,
    invalidJsonPrefix = "服务返回了无效 JSON",
    invalidJsonPreview = false,
    responsePreviewLength = 500,
    ...fetchOptions
  } = options;
  const method = requestMethod(fetchOptions);
  const occurredAt = new Date();
  let response: Response;
  try {
    response = await fetch(path, normalizeFetchOptions(fetchOptions));
  } catch (error) {
    const message = basicErrorMessage(error, "网络请求失败");
    throw new UniDeskRequestError(message, requestMeta("network", path, method, occurredAt, { upstreamMessage: message }));
  }

  const text = await response.text();
  const parsed = parseJson(text);
  if (parsed.parseError) {
    if (strictJson && method === "GET" && attempt < retryInvalidJson) {
      await sleep(retryDelayMs);
      return requestJson(path, options, attempt + 1);
    }
    if (strictJson) {
      const preview = invalidJsonPreview ? `；响应预览：${safePreview(text, 180)}` : "";
      throw new UniDeskRequestError(
        `${invalidJsonPrefix}（${text.length} bytes）：${parsed.parseError}${preview}`,
        requestMeta("parse", path, method, occurredAt, {
          status: response.status,
          statusText: response.statusText,
          parseError: parsed.parseError,
          responsePreview: safePreview(text, responsePreviewLength),
        }),
      );
    }
  }

  if (!response.ok || bodyIndicatesFailure(parsed.body, failureFields)) {
    const upstreamMessage = responseMessage(parsed.body);
    const message = upstreamMessage || httpTitle(response.status, response.statusText);
    if (isAuthRequiredResponse(response.status, message)) emitAuthRequired(path, method, occurredAt);
    throw new UniDeskRequestError(
      message,
      requestMeta("http", path, method, occurredAt, {
        status: response.status,
        statusText: response.statusText,
        upstreamMessage,
        responsePreview: safePreview(parsed.parseError ? text : parsed.body, responsePreviewLength),
      }),
    );
  }

  return parsed.body;
}

export async function requestBlob(path: string, options: RequestInit = {}): Promise<Blob> {
  const method = requestMethod(options as AnyRecord);
  const occurredAt = new Date();
  let response: Response;
  try {
    response = await fetch(path, normalizeFetchOptions(options as UniDeskRequestJsonOptions));
  } catch (error) {
    const message = basicErrorMessage(error, "网络请求失败");
    throw new UniDeskRequestError(message, requestMeta("network", path, method, occurredAt, { upstreamMessage: message }));
  }
  if (response.ok) return response.blob();

  const text = await response.text();
  const parsed = parseJson(text);
  const upstreamMessage = responseMessage(parsed.body);
  const message = upstreamMessage || httpTitle(response.status, response.statusText);
  if (isAuthRequiredResponse(response.status, message)) emitAuthRequired(path, method, occurredAt);
  throw new UniDeskRequestError(
    message,
    requestMeta("http", path, method, occurredAt, {
      status: response.status,
      statusText: response.statusText,
      upstreamMessage,
      responsePreview: safePreview(parsed.parseError ? text : parsed.body),
      parseError: parsed.parseError || undefined,
    }),
  );
}

export function isUniDeskRequestError(error: unknown): error is UniDeskRequestError {
  return Boolean(error && typeof error === "object" && (error as AnyRecord).unideskRequestError === true && (error as AnyRecord).meta);
}

function fmtDateTime(value: string | undefined): string {
  if (!value) return "";
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) return value;
  return `${fmtDate(date)} ${displayTimeLabel()}`;
}

export function describeUniDeskError(error: unknown, fallback = "操作失败"): UniDeskErrorView {
  if (isUniDeskRequestError(error)) {
    const title = error.meta.kind === "parse"
      ? "响应解析失败"
      : error.meta.kind === "network"
        ? "网络请求失败"
        : error.meta.status && (error.meta.status < 200 || error.meta.status >= 300)
          ? httpTitle(error.meta.status, error.meta.statusText)
          : "应用请求失败";
    const plainHttpTitle = error.meta.status ? httpTitle(error.meta.status) : "";
    const isDuplicateTitle = (value: string | undefined) => !value || value === title || value === plainHttpTitle;
    const message = !isDuplicateTitle(error.message)
      ? error.message
      : isDuplicateTitle(error.meta.upstreamMessage)
        ? ""
        : error.meta.upstreamMessage || "";
    return {
      title,
      message,
      status: error.meta.status,
      statusText: error.meta.statusText,
      method: error.meta.method,
      url: error.meta.url,
      occurredAt: fmtDateTime(error.meta.occurredAt),
      responsePreview: error.meta.responsePreview,
      parseError: error.meta.parseError,
      structured: true,
    };
  }
  const text = basicErrorMessage(error, fallback);
  const lines = text.split(/\r?\n/u);
  return {
    title: lines[0] || fallback,
    message: lines.slice(1).join("\n"),
    structured: lines.length > 1,
  };
}

export function formatUniDeskError(error: unknown, fallback = "操作失败"): string {
  const detail = describeUniDeskError(error, fallback);
  const lines = [detail.title];
  if (detail.message) lines.push(`原因: ${detail.message}`);
  if (detail.method || detail.url) lines.push(`请求: ${[detail.method, detail.url].filter(Boolean).join(" ")}`);
  if (detail.status) lines.push(`状态: ${httpTitle(detail.status, detail.statusText)}`);
  if (detail.occurredAt) lines.push(`时间: ${detail.occurredAt}`);
  if (detail.parseError) lines.push(`解析错误: ${detail.parseError}`);
  if (detail.responsePreview && detail.responsePreview !== detail.message) lines.push(`响应预览: ${detail.responsePreview}`);
  return lines.filter(Boolean).join("\n");
}

export function errorMessage(error: unknown, fallback = "操作失败"): string {
  return isUniDeskRequestError(error) ? formatUniDeskError(error, fallback) : basicErrorMessage(error, fallback);
}

function escapeHtml(value: unknown): string {
  return String(value ?? "")
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#39;");
}

export function renderUniDeskErrorHtml(error: unknown, options: { wide?: boolean; fallback?: string } = {}): string {
  if (!error) return "";
  const detail = describeUniDeskError(error, options.fallback);
  const className = `form-error unidesk-error${options.wide ? " wide" : ""}`;
  const rows = [
    detail.method || detail.url ? ["请求", [detail.method, detail.url].filter(Boolean).join(" ")] : null,
    detail.status ? ["状态", httpTitle(detail.status, detail.statusText)] : null,
    detail.occurredAt ? ["时间", detail.occurredAt] : null,
    detail.parseError ? ["解析错误", detail.parseError] : null,
    detail.responsePreview ? ["响应预览", detail.responsePreview] : null,
  ].filter((item): item is string[] => Array.isArray(item));
  const message = detail.message ? `<pre class="unidesk-error-message">${escapeHtml(detail.message)}</pre>` : "";
  const details = rows.length > 0
    ? `<dl class="unidesk-error-details">${rows.map(([label, value]) => `<dt>${escapeHtml(label)}</dt><dd>${escapeHtml(value)}</dd>`).join("")}</dl>`
    : "";
  return `<div class="${className}" role="alert" data-testid="unidesk-error"><div class="unidesk-error-title"><strong>${escapeHtml(detail.title)}</strong></div>${message}${details}</div>`;
}
