'use client';

import { ApiCode, type ApiResponse } from '@pac/types';
import { env } from './env';
import { useAuthStore } from '@/stores/auth-store';
import { reportAuthDiag, authStoreSnap, authEnvSnap } from './auth-diag';

/**
 * PAC API client.
 *
 * Wire contract: every response is HTTP 200 (except true server crashes 5xx)
 * with body { code, msg, data, details? }.
 *
 * 401 等价处理(wrapped code 区分):
 *   - 10104 AUTH_TOKEN_MISSING       → silent refresh(请求漏带 token)
 *   - 10105 AUTH_TOKEN_EXPIRED       → silent refresh(主路径)
 *   - 10106 AUTH_TOKEN_INVALID       → silent refresh(access 失效但 refresh 可能还在)
 *                                       常见:dev 改 JWT_SECRET / 重启后 localStorage 旧 token
 *                                       理由:access/refresh 是两个独立 secret,access 烂不代表 refresh 烂
 *   - 10103 AUTH_INVALID_REFRESH_TOKEN → 清 session(refresh 自身失败,真没救了)
 *   - 10107 AUTH_PERMISSION_DENIED   → 不清 session(只是这条权限不够)
 *   - 其他 101xx                      → 清 session(保守)
 *
 * HTTP 5xx (genuine crash) → reject with code=500.
 */
export class ApiError extends Error {
  constructor(
    public readonly code: number,
    message: string,
    public readonly details?: unknown,
  ) {
    super(message);
    this.name = 'ApiError';
  }
}

interface RequestOptions extends Omit<RequestInit, 'body'> {
  body?: unknown;
  query?: Record<string, string | number | boolean | undefined | null>;
  /** default true */
  auth?: boolean;
  /** 内部用:silent refresh 重试时置 true,防止递归 */
  _retried?: boolean;
}

async function request<T>(path: string, opts: RequestOptions = {}): Promise<T> {
  const { body, query, auth = true, headers, _retried, ...rest } = opts;
  const url = new URL(path, env.apiBaseUrl);
  if (query) {
    for (const [k, v] of Object.entries(query)) {
      if (v !== undefined && v !== null) url.searchParams.set(k, String(v));
    }
  }

  const finalHeaders = new Headers(headers);
  if (body !== undefined && !(body instanceof FormData)) {
    finalHeaders.set('Content-Type', 'application/json');
  }
  if (auth) {
    const token = useAuthStore.getState().accessToken;
    if (token) finalHeaders.set('Authorization', `Bearer ${token}`);
  }

  const res = await fetch(url, {
    ...rest,
    headers: finalHeaders,
    body: body === undefined ? undefined : body instanceof FormData ? body : JSON.stringify(body),
  });

  // True server crash — preserve HTTP semantics here so monitoring still works.
  if (res.status >= 500) {
    throw new ApiError(ApiCode.INTERNAL_ERROR, `Server error ${res.status}`);
  }

  // Everything else is HTTP 200 with a wrapped body.
  let payload: ApiResponse<T>;
  try {
    payload = (await res.json()) as ApiResponse<T>;
  } catch {
    throw new ApiError(ApiCode.INTERNAL_ERROR, `Malformed response (HTTP ${res.status})`);
  }

  if (payload.code === ApiCode.OK) {
    return payload.data as T;
  }

  // ── silent refresh 兜底(只要 refresh token 还在就值得试一次,失败再清) ──
  if (
    auth &&
    !_retried &&
    (payload.code === ApiCode.AUTH_TOKEN_EXPIRED ||
      payload.code === ApiCode.AUTH_TOKEN_MISSING ||
      payload.code === ApiCode.AUTH_TOKEN_INVALID) &&
    useAuthStore.getState().canRefresh()
  ) {
    const refreshed = await tryRefresh();
    if (refreshed) {
      return request<T>(path, { ...opts, _retried: true });
    }
  }

  // ── 决定要不要清 session ──
  // 只有"带 token 的受保护请求(auth)"失败才可能意味着会话真的废了。
  // auth:false 的公开端点(exchange-code / refresh / mock-*)失败不代表现有会话失效 —
  // 尤其一次性 code 被重放/过期(10102),绝不能把已建立的会话清掉
  // (否则宿主 iframe 首屏用同一 code 换两次时,第二次的 10102 会误清刚建立的会话 → 「登录已过期」)。
  // 这些公开端点的失败由各自调用方(bootstrap / doRefresh)按语义自行决定是否 clear。
  // 保留 10107(权限不足):session 还有效,只是这条权限不够,不要把人踹下线。
  if (
    auth &&
    payload.code >= 10100 &&
    payload.code < 10200 &&
    payload.code !== ApiCode.AUTH_PERMISSION_DENIED
  ) {
    // 运行期掉线埋点 —— 这条路径过去没信标,导致「用着用着登录已过期」在服务端无迹可寻、
    // 也不触发企微告警。补上(clear 前取快照,否则字段全空)。
    reportAuthDiag({
      ev: 'runtime-auth-clear',
      err: `code=${payload.code}`,
      ...authStoreSnap(),
      ...authEnvSnap(),
    });
    useAuthStore.getState().clear();
  }

  throw new ApiError(payload.code, payload.msg ?? 'Request failed', payload.details);
}

// ─────────────────────────────────────────────────────
// Silent refresh — 单飞模式(并发请求只调一次 /auth/refresh)
// ─────────────────────────────────────────────────────

let refreshInflight: Promise<boolean> | null = null;

/**
 * 调用 /auth/refresh。成功 → 写 store 返回 true;失败 → 清 store 返回 false。
 * 多请求同时撞 401 时,只调一次 refresh,其它请求 await 同一个 promise。
 */
export async function tryRefresh(): Promise<boolean> {
  if (refreshInflight) return refreshInflight;
  refreshInflight = doRefresh().finally(() => {
    refreshInflight = null;
  });
  return refreshInflight;
}

async function doRefresh(): Promise<boolean> {
  const store = useAuthStore.getState();
  const rt = store.refreshToken;
  if (!rt) return false;

  // 不复用 request(),避免循环:用裸 fetch 调 public 端点
  try {
    const url = new URL('/pac/v1/auth/refresh', env.apiBaseUrl);
    const res = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ refreshToken: rt }),
    });
    if (res.status >= 500) {
      // 5xx 不动 store(可能是暂时的),让上层报错
      return false;
    }
    const payload = (await res.json()) as ApiResponse<{
      accessToken: string;
      refreshToken: string;
      expiresIn: number;
    }>;
    if (payload.code === ApiCode.OK && payload.data) {
      store.setTokens({
        accessToken: payload.data.accessToken,
        refreshToken: payload.data.refreshToken,
        expiresIn: payload.data.expiresIn,
      });
      return true;
    }
    // refresh 也失败 → 清 store,引导上层走"需要重新登录"
    // 埋点:refresh 被拒(常见 10103 revoked = refresh token 被轮换/过期)是运行期掉线主因,必须可见+可告警。
    reportAuthDiag({
      ev: 'runtime-refresh-fail',
      err: `code=${payload.code}`,
      ...authStoreSnap(),
      ...authEnvSnap(),
    });
    store.clear();
    return false;
  } catch {
    return false;
  }
}

export const api = {
  get: <T>(path: string, opts?: RequestOptions) => request<T>(path, { ...opts, method: 'GET' }),
  post: <T>(path: string, body?: unknown, opts?: RequestOptions) =>
    request<T>(path, { ...opts, method: 'POST', body }),
  patch: <T>(path: string, body?: unknown, opts?: RequestOptions) =>
    request<T>(path, { ...opts, method: 'PATCH', body }),
  delete: <T>(path: string, opts?: RequestOptions) =>
    request<T>(path, { ...opts, method: 'DELETE' }),
};
