'use client';

import { useEffect, useRef, useState } from 'react';
import { useAuthStore } from '@/stores/auth-store';
import { exchangeCode, refreshToken } from '@/lib/auth-api';
import { tryRefresh } from '@/lib/api-client';
import { env } from '@/lib/env';

const REFRESH_LEAD_MS = 60_000;

/** 当前 store 的 token 快照(反水合后)—— 诊断嵌入重载时"localStorage 到底读没读到" */
function storeSnap() {
  const s = useAuthStore.getState();
  return {
    storedAccess: Boolean(s.accessToken),
    storedRefresh: Boolean(s.refreshToken),
    storedAccessValid: Boolean(s.accessToken && s.expiresAt && Date.now() < s.expiresAt),
  };
}

/**
 * 环境/存储探针 —— 定位「登录已过期」根因的关键。
 * storedRefresh=N 分不清"真没登录过"(正常)vs"存储被 iframe 分区/ITP 清了"(bug);
 * 这几个字段才能证死:storageWritable(存储到底能不能用)+ isEmbedded(是否在 iframe)
 * + hostOrigin(哪个宿主)+ isReload(是不是重载)+ ua(Safari→ITP)。
 */
function envSnap() {
  let storageWritable = false;
  try {
    const k = '__pac_probe__';
    localStorage.setItem(k, '1');
    storageWritable = localStorage.getItem(k) === '1';
    localStorage.removeItem(k);
  } catch {
    storageWritable = false; // 抛错 = 存储被禁/分区(隐私模式 / 第三方 iframe 被拦)
  }
  let isEmbedded = false;
  try {
    isEmbedded = window.self !== window.top;
  } catch {
    isEmbedded = true; // 跨源访问 window.top 抛错 = 确实被跨源嵌入
  }
  let hostOrigin: string | null = null;
  try {
    const anc = window.location.ancestorOrigins;
    hostOrigin =
      (anc && anc.length ? anc[0] : null) ??
      (document.referrer ? new URL(document.referrer).origin : null);
  } catch {
    hostOrigin = null;
  }
  let isReload = false;
  try {
    const nav = performance.getEntriesByType('navigation')[0] as
      | PerformanceNavigationTiming
      | undefined;
    isReload = nav?.type === 'reload';
  } catch {
    /* noop */
  }
  const ua = typeof navigator !== 'undefined' ? navigator.userAgent.slice(0, 160) : '';
  return { storageWritable, isEmbedded, hostOrigin, isReload, ua };
}

/**
 * bootstrap 诊断信标 —— 同时打 console(本地/开 devtools 可看)+ 打服务端日志
 * (navigator.sendBeacon,远端对接方复现时我们 grep pac-service 日志即可,不用他们开 devtools)。
 * 纯诊断,失败静默,绝不影响鉴权主流程。
 */
function reportDiag(payload: {
  ev: string;
  hadCode?: boolean;
  codeTag?: string | null;
  storedAccess?: boolean;
  storedRefresh?: boolean;
  storedAccessValid?: boolean;
  err?: string | null;
  // 环境/存储探针(失败事件带上,server 据此判"存储被清 vs 真没登录 + 哪个宿主/浏览器")
  storageWritable?: boolean;
  isEmbedded?: boolean;
  hostOrigin?: string | null;
  isReload?: boolean;
  ua?: string;
}) {
  try {
    // eslint-disable-next-line no-console
    console.info('[auth-bootstrap]', payload);
  } catch {
    /* noop */
  }
  try {
    const url = new URL('/pac/v1/auth/client-diag', env.apiBaseUrl).toString();
    const body = JSON.stringify({ phase: 'bootstrap', ...payload });
    // 用 fetch(不用 sendBeacon):sendBeacon 强制带凭据,遇到服务端 CORS='*' 会被浏览器拦
    // (凭据+通配 origin 不允许)。fetch 默认 same-origin 凭据 → 跨源即不带,与 api-client 一致。
    // keepalive 保证即使随后页面跳转/卸载,这条诊断也能发出。
    void fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body,
      keepalive: true,
      credentials: 'omit',
    }).catch(() => {
      /* 诊断失败静默 */
    });
  } catch {
    /* noop */
  }
}

/**
 * 鉴权启动 — 三路兜底:
 *   1. URL 有 ?code= → 优先 exchange 拿新 token(host 刚跳来 / 用户手动重开)
 *   2. localStorage 有 access token 且未过期 → 直接 ready(F5 主路径)
 *   3. localStorage 仅有 refresh token(access 过期或没了)→ silent refresh
 *   4. 全没了 → error,提示回宿主
 *
 * 之后挂"过期前 60s 自动 silent refresh"定时器。
 */
export function useAuthBootstrap() {
  const setTokens = useAuthStore((s) => s.setTokens);
  const clear = useAuthStore((s) => s.clear);
  const expiresAt = useAuthStore((s) => s.expiresAt);
  const refresh = useAuthStore((s) => s.refreshToken);

  const [status, setStatus] = useState<'idle' | 'authenticating' | 'ready' | 'error'>('idle');
  const bootstrapped = useRef(false);

  useEffect(() => {
    if (bootstrapped.current) return;
    bootstrapped.current = true;

    const url = new URL(window.location.href);
    const code = url.searchParams.get('code');
    const codeTag = code ? code.slice(0, 8) : null;
    const envInfo = envSnap(); // 存储/环境探针,一次算好,失败事件带上

    /**
     * 🔴 **code 一读到就立刻从地址栏剥掉**,⛔ 不能等 exchange 回来再剥。
     *
     * ── 2026-08-10 实测的卡死 ───────────────────────────────────────
     * 带 `?code=` 打开 `/plans`,而 localStorage 里**已经有票**(第二次进来):
     *   ① store 反水合 → `isAuthenticated` 立刻为 true → AuthGate 放行 children
     *   ② `/plans` 的落地解析器跑起来 → 主管 → `router.replace('/supervisor')`
     *   ③ 这里的 `await exchangeCode()` 才回来,然后 `replaceState` 把地址写回 `/plans`
     * ③ **把 ② 那次跳转冲掉了** —— Next 的路由树被拉回 `plans`,
     * 而解析器已经 `return`,不会再试一次 ⇒ 页面永远停在「正在进入召回工作台…」。
     * (localStorage 干净时不复现:那时 AuthGate 还在占位,解析器压根没挂载。)
     *
     * ⇒ 剥 code 这件事**与 exchange 成不成功无关**,提到 await 之前做:
     *   本 effect 跑在**首次提交**上(那时 `mounted` 还是 false、children 尚未挂载),
     *   所以任何页面的落地跳转都一定发生在这之后,不会再被回写覆盖。
     * ⚠️ 第二个参数要传 `window.history.state` 而不是 `{}` ——
     *   App Router 的路由树就存在 history state 里,传空对象等于把它抹掉。
     */
    if (code) {
      url.searchParams.delete('code');
      window.history.replaceState(window.history.state, '', url.toString());
    }

    // 入口快照:URL 有没有 code + 反水合后 store 有没有 token + 环境(嵌入首开路径起点证据)
    reportDiag({ ev: 'start', hadCode: Boolean(code), codeTag, ...storeSnap(), ...envInfo });

    const run = async () => {
      // ─── 1. URL 有 code → 优先(新人接管,覆盖旧 token)───
      if (code) {
        setStatus('authenticating');
        try {
          const r = await exchangeCode(code);
          setTokens(r);
          // ⚠️ 地址栏上面已经剥干净了 —— ⛔ 别在这里再 replaceState 一次(那正是卡死的成因)
          reportDiag({ ev: 'code-ok', codeTag });
          setStatus('ready');
          return;
        } catch (e) {
          // code 失效(过期 / 重放) — 不一定要崩,localStorage 可能还有效(地址栏已剥,同上)
          // ⭐ 关键证据:换 code 失败瞬间,store 里到底有没有 token(有→能兜住;无→必弹登录已过期)
          reportDiag({
            ev: 'code-fail',
            codeTag,
            err: e instanceof Error ? e.message.slice(0, 100) : String(e).slice(0, 100),
            ...storeSnap(),
            ...envInfo,
          });
          // fall through 试 store
        }
      }

      // ─── 2. localStorage 反水合的 access token 还活着 → ready ───
      const s = useAuthStore.getState();
      if (s.accessToken && s.expiresAt && Date.now() < s.expiresAt) {
        reportDiag({ ev: 'access-ok', codeTag, ...storeSnap() });
        setStatus('ready');
        return;
      }

      // ─── 3. 仅 refresh token 还在 → silent refresh ───
      if (s.refreshToken) {
        setStatus('authenticating');
        try {
          const r = await refreshToken(s.refreshToken);
          setTokens(r);
          reportDiag({ ev: 'refresh-ok', codeTag });
          setStatus('ready');
          return;
        } catch {
          reportDiag({ ev: 'refresh-fail', codeTag, ...storeSnap(), ...envInfo });
          clear();
          setStatus('error');
          return;
        }
      }

      // ─── 4. 全没了 → 弹「登录已过期」───
      reportDiag({
        ev: 'no-credentials',
        codeTag,
        hadCode: Boolean(code),
        ...storeSnap(),
        ...envInfo,
      });
      setStatus('error');
    };

    void run();
  }, [setTokens, clear]);

  // 过期前 60s 自动 silent refresh —— 走 api-client 的共享单飞 tryRefresh,
  // 与请求 401 兜底刷共用同一个"在途 refresh"。否则定时器和请求兜底可能同时拿
  // 同一个一次性 refresh token 去换:一个成功轮换(del 旧 jti),另一个拿已作废 jti
  // → 10103 revoked → 误清会话 =「用着用着登录已过期」的竞态根因。
  // tryRefresh 内部成功即 setTokens、失败即 clear + 发运行期掉线信标,故此处不再手动处理。
  useEffect(() => {
    if (!expiresAt || !refresh) return;
    const delay = Math.max(0, expiresAt - Date.now() - REFRESH_LEAD_MS);
    const timer = setTimeout(() => {
      void tryRefresh();
    }, delay);
    return () => clearTimeout(timer);
  }, [expiresAt, refresh]);

  return { status };
}
