import type { WebUserSession } from "../types";

export const SERVER_SESSION_STORAGE_KEY = "omniai-web-session";
export const SERVER_SESSION_REPLACED_EVENT = "omniai:session-replaced";
export const SERVER_SESSION_EXPIRED_EVENT = "omniai:session-expired";

export type ServerConnectionState = "checking" | "connected" | "degraded";

export interface ServerConnectionHealth {
  state: ServerConnectionState;
  baseUrl: string;
  checkedAt: string;
  errorMessage?: string;
}

export interface ServerRequestOptions {
  method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
  body?: unknown;
  token?: string;
  headers?: Record<string, string>;
  raw?: boolean;
  signal?: AbortSignal;
  /** Per-request timeout in ms. Defaults to DEFAULT_REQUEST_TIMEOUT_MS. Pass 0 to disable. */
  timeoutMs?: number;
  /** Defaults to 2. Use 0 for non-idempotent task submission endpoints. */
  maxRetries?: number;
  fallbackMessage?: string;
}

export const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;

export interface ServerSessionReplacedDetail {
  status?: number;
  code?: string;
  message: string;
}

export class ServerRequestError extends Error {
  status?: number;
  code?: string;
  payload?: unknown;

  constructor(message: string, status?: number, payload?: unknown) {
    super(message);
    this.name = "ServerRequestError";
    this.status = status;
    this.payload = payload;

    if (isRecord(payload) && typeof payload.code === "string") {
      this.code = payload.code;
    }
  }
}

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

export function compactMessage(value: string): string {
  return value.replace(/\s+/g, " ").trim().slice(0, 240);
}

export function getServerBaseUrl(): string {
  return "";
}

export function buildApiUrl(path: string): string {
  const cleanPath = path.replace(/^\/+/, "");
  return `/api/${cleanPath}`;
}

export function canUseSessionStorage(): boolean {
  return typeof window !== "undefined" && typeof window.sessionStorage !== "undefined";
}

function canUseLocalStorage(): boolean {
  return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
}

function parseStoredSession(raw: string | null): WebUserSession | null {
  if (!raw) return null;

  try {
    const parsed = JSON.parse(raw) as unknown;
    return isRecord(parsed) && typeof parsed.token === "string" && isRecord(parsed.user)
      ? (parsed as unknown as WebUserSession)
      : null;
  } catch {
    return null;
  }
}

export function readStoredSession(): WebUserSession | null {
  let fallbackSession: WebUserSession | null = null;

  try {
    if (canUseLocalStorage()) {
      const localSession = parseStoredSession(window.localStorage.getItem(SERVER_SESSION_STORAGE_KEY));
      if (localSession) return localSession;
    }
  } catch {
    // Fall through to the legacy session-scoped copy.
  }

  try {
    if (canUseSessionStorage()) {
      fallbackSession = parseStoredSession(window.sessionStorage.getItem(SERVER_SESSION_STORAGE_KEY));
    }
  } catch {
    fallbackSession = null;
  }

  if (fallbackSession && canUseLocalStorage()) {
    try {
      window.localStorage.setItem(SERVER_SESSION_STORAGE_KEY, JSON.stringify(fallbackSession));
    } catch {
      // Migrating the legacy session is best-effort.
    }
  }

  return fallbackSession;
}

export function writeStoredSession(session: WebUserSession | null): void {
  try {
    if (canUseLocalStorage()) {
      if (session) {
        window.localStorage.setItem(SERVER_SESSION_STORAGE_KEY, JSON.stringify(session));
      } else {
        window.localStorage.removeItem(SERVER_SESSION_STORAGE_KEY);
      }
    }
  } catch {
    // Browser persistence is a convenience layer, not a hard dependency.
  }

  try {
    if (canUseSessionStorage()) {
      if (session) {
        window.sessionStorage.setItem(SERVER_SESSION_STORAGE_KEY, JSON.stringify(session));
      } else {
        window.sessionStorage.removeItem(SERVER_SESSION_STORAGE_KEY);
      }
    }
  } catch {
    // Keep the local copy as the primary persistence layer.
  }
}

export function clearAllUserStorage(): void {
  writeStoredSession(null);

  try {
    if (typeof window === "undefined") return;
    const legacyKeys = ["omniai:token", "omniai:session"];
    for (const key of legacyKeys) {
      window.localStorage.removeItem(key);
      window.sessionStorage.removeItem(key);
    }
    const prefixKeys = [
      "omniai-web-profile-ui",
      "omniai:more-recent-tools",
      "omniai:generation-queue",
      "omniai-canvas-saved-assets",
    ];
    for (let i = window.localStorage.length - 1; i >= 0; i--) {
      const key = window.localStorage.key(i);
      if (key && prefixKeys.some((p) => key.startsWith(p))) {
        window.localStorage.removeItem(key);
      }
    }
    for (let i = window.sessionStorage.length - 1; i >= 0; i--) {
      const key = window.sessionStorage.key(i);
      if (key && prefixKeys.some((p) => key.startsWith(p))) {
        window.sessionStorage.removeItem(key);
      }
    }
  } catch {
    // best-effort cleanup
  }
}

export function getStoredToken(): string | null {
  return readStoredSession()?.token ?? null;
}

export function buildAuthHeaders(tokenOverride?: string, extraHeaders?: Record<string, string>): Record<string, string> {
  const token = (tokenOverride ?? getStoredToken() ?? "").trim();
  return {
    "Content-Type": "application/json",
    ...(token ? { Authorization: `Bearer ${token}` } : {}),
    ...(extraHeaders || {}),
  };
}

export function parseResponseBody(text: string): unknown {
  const trimmed = text.trim();
  if (!trimmed) return null;

  try {
    return JSON.parse(trimmed);
  } catch {
    return trimmed;
  }
}

export function unwrapApiPayload(payload: unknown): unknown {
  if (!isRecord(payload)) return payload;

  const nested = payload.data ?? payload.result ?? payload.payload;
  return nested === undefined ? payload : nested;
}

export function getPayloadMessage(payload: unknown): string | null {
  if (typeof payload === "string") {
    const message = compactMessage(payload);
    if (/^<!doctype html|<html[\s>]|<head[\s>]|<body[\s>]|^<\?xml|<Error[\s>]/i.test(message)) {
      return null;
    }
    return message || null;
  }
  if (!isRecord(payload)) return null;

  const message = payload.error ?? payload.message ?? payload.errorMessage;
  if (typeof message !== "string") return null;

  const compacted = compactMessage(message);
  if (/^<!doctype html|<html[\s>]|<head[\s>]|<body[\s>]|^<\?xml|<Error[\s>]/i.test(compacted)) {
    return null;
  }
  return compacted || null;
}

function getPayloadCode(payload: unknown): string | undefined {
  return isRecord(payload) && typeof payload.code === "string" ? payload.code : undefined;
}

let lastSessionReplacedEventAt = 0;

let lastSessionExpiredEventAt = 0;

function isNonAuthErrorCode(code: string | undefined): boolean {
  if (!code) return false;
  return [
    "ENTERPRISE_VIDEO_MODEL_NOT_ALLOWED",
    "INSUFFICIENT_BALANCE",
    "INSUFFICIENT_ENTERPRISE_BALANCE",
  ].includes(code);
}

function isAuthFailureResponse(status: number, payload: unknown): boolean {
  if (status === 401) return true;
  if (status !== 403) return false;

  const code = getPayloadCode(payload);
  if (code === "SESSION_REPLACED" || code === "TOKEN_EXPIRED" || code === "ACCOUNT_DISABLED") return true;

  const message = getPayloadMessage(payload) || "";
  return /账号已禁用|登录已过期|登录状态|session|token|企业信息不存在/i.test(message);
}

function notifySessionExpired(status: number, response: Response, payload: unknown): void {
  if (status !== 401 && status !== 403) return;
  if (typeof window === "undefined") return;
  // Auth endpoints (login/register/me) surface their own errors — a wrong
  // password must not be mistaken for an expired session.
  if (/\/auth\//i.test(response.url)) return;
  // SESSION_REPLACED has its own dedicated handling/modal.
  if (getPayloadCode(payload) === "SESSION_REPLACED") return;
  // If the user never had a session, a 401 is expected — not a session expiry.
  if (!readStoredSession()) return;
  // Deliberate early-exit for unauthenticated users — not a real auth failure.
  if (getPayloadCode(payload) === "NOT_LOGGED_IN") return;
  // Non-auth 403 errors (enterprise model access, insufficient balance) must
  // not trigger session expiry.
  if (status === 403 && isNonAuthErrorCode(getPayloadCode(payload))) return;
  if (!isAuthFailureResponse(status, payload)) return;

  const now = Date.now();
  if (now - lastSessionExpiredEventAt < 1500) return;
  lastSessionExpiredEventAt = now;
  window.dispatchEvent(
    new CustomEvent<ServerSessionReplacedDetail>(SERVER_SESSION_EXPIRED_EVENT, {
      detail: { status, code: getPayloadCode(payload), message: "登录状态已失效，请重新登录。" },
    }),
  );
}

function notifySessionReplaced(status: number, payload: unknown, fallbackMessage: string): void {
  const code = getPayloadCode(payload);
  const message = getPayloadMessage(payload) || fallbackMessage || "您已在别处登录";
  const isSessionReplaced = code === "SESSION_REPLACED" || message.includes("您已在别处登录");
  if (!isSessionReplaced || typeof window === "undefined") return;
  if (!readStoredSession()) return;

  const now = Date.now();
  if (now - lastSessionReplacedEventAt < 1500) return;
  lastSessionReplacedEventAt = now;
  window.dispatchEvent(
    new CustomEvent<ServerSessionReplacedDetail>(SERVER_SESSION_REPLACED_EVENT, {
      detail: { status, code, message },
    }),
  );
}

export function getErrorMessage(error: unknown): string {
  if (error instanceof Error) return error.message;
  return String(error || "Unknown API error");
}

export function isServerRequestError(error: unknown): error is ServerRequestError {
  return error instanceof ServerRequestError;
}

export async function readJsonResponse<T>(response: Response, fallbackMessage: string): Promise<T> {
  const payload = parseResponseBody(await response.text().catch(() => ""));
  if (!response.ok) {
    const message =
      getPayloadMessage(payload) ||
      compactMessage(response.statusText) ||
      `${fallbackMessage} (${response.status})`;
    notifySessionReplaced(response.status, payload, message);
    notifySessionExpired(response.status, response, payload);
    throw new ServerRequestError(message, response.status, payload);
  }

  return unwrapApiPayload(payload) as T;
}

export async function throwResponseError(response: Response, fallbackMessage: string): Promise<never> {
  const payload = parseResponseBody(await response.text().catch(() => ""));
  const message =
    getPayloadMessage(payload) ||
    compactMessage(response.statusText) ||
    `${fallbackMessage} (${response.status})`;
  notifySessionReplaced(response.status, payload, message);
  notifySessionExpired(response.status, response, payload);
  throw new ServerRequestError(message, response.status, payload);
}

function isRetryable(error: unknown): boolean {
  if (error instanceof ServerRequestError) {
    const s = error.status;
    return s === 429 || (s !== undefined && s >= 500);
  }
  return error instanceof TypeError || (error instanceof DOMException && error.name !== "AbortError");
}

function getRetryDelay(attempt: number, error: unknown): number {
  if (error instanceof ServerRequestError && error.status === 429) {
    return Math.min(5000, 2000 * attempt);
  }
  return Math.min(4000, 300 * 2 ** attempt);
}

const MAX_RETRIES = 2;

export async function serverRequest<T>(path: string, options?: ServerRequestOptions): Promise<T> {
  let lastError: unknown;
  const timeoutMs = options?.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
  const maxRetries = options?.maxRetries ?? MAX_RETRIES;
  const fallbackMessage = options?.fallbackMessage || "Request failed";

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const controller = timeoutMs > 0 ? new AbortController() : null;
    const timeoutId =
      controller && typeof window !== "undefined"
        ? window.setTimeout(() => controller.abort(new DOMException("Request timed out", "TimeoutError")), timeoutMs)
        : null;
    const onCallerAbort = () => controller?.abort((options?.signal as AbortSignal)?.reason);
    if (controller && options?.signal) {
      if (options.signal.aborted) controller.abort(options.signal.reason);
      else options.signal.addEventListener("abort", onCallerAbort, { once: true });
    }

    try {
      const headers = buildAuthHeaders(options?.token, options?.headers);
      const response = await fetch(buildApiUrl(path), {
        method: options?.method || "GET",
        headers,
        body: options?.body === undefined ? undefined : JSON.stringify(options.body),
        signal: controller ? controller.signal : options?.signal,
        credentials: "include",
      });

      const payload = await readJsonResponse<unknown>(response, fallbackMessage);
      return (options?.raw ? payload : unwrapApiPayload(payload)) as T;
    } catch (error) {
      lastError = error;
      if (attempt < maxRetries && isRetryable(error) && !options?.signal?.aborted) {
        await new Promise((r) => setTimeout(r, getRetryDelay(attempt, error)));
        continue;
      }
      throw error;
    } finally {
      if (timeoutId !== null) window.clearTimeout(timeoutId);
      options?.signal?.removeEventListener("abort", onCallerAbort);
    }
  }

  throw lastError;
}

export async function checkServerHealth(timeoutMs = 4500): Promise<ServerConnectionHealth> {
  const controller = new AbortController();
  const timeoutId = window.setTimeout(() => controller.abort(), timeoutMs);

  try {
    await serverRequest<unknown>("/health", { signal: controller.signal });
    return {
      state: "connected",
      baseUrl: getServerBaseUrl(),
      checkedAt: new Date().toISOString(),
    };
  } catch (error) {
    return {
      state: "degraded",
      baseUrl: getServerBaseUrl(),
      checkedAt: new Date().toISOString(),
      errorMessage: getErrorMessage(error),
    };
  } finally {
    window.clearTimeout(timeoutId);
  }
}
