'use client';

import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import type { AccessTokenPayload } from '@pac/types';

/**
 * 前端会话用户 = JWT 解码(role/permissions/sub/tenantId/dictionary/orgScope,即时)
 *   + 会话补丁(clinicIds/sourceUnits,异步从 GET /auth/session 拉)。
 * 设计 B:JWT 只装通用 orgScope,前端展不开,故诊所/品牌过滤维由后端展开后补进来。
 */
export type SessionUser = AccessTokenPayload & {
  clinicIds?: string[];
  sourceUnits?: string[];
  /// 宿主动作跳转模板(actionUrls)—— 从 /auth/session 拉;"新建预约"等按钮据此打开宿主页。
  actionUrls?: Record<string, string | null>;
};

/**
 * 用户可见诊所选项(诊所筛选/计数用)。
 * clinicIds 非空 = 受限范围,以它为准;空 = 不限(集团级)→ 回退到字典里全部诊所
 * (否则集团级用户诊所筛选会空白)。名字取 dictionary,缺则回退 id。
 */
export function visibleClinics(user: SessionUser | null): { id: string; name: string }[] {
  const dict = user?.dictionary?.clinics ?? {};
  const ids = user?.clinicIds?.length ? user.clinicIds : Object.keys(dict);
  return ids.map((id) => ({ id, name: String(dict[id] ?? id) }));
}

/**
 * 🔴 **诊所范围到底加载出来了没有**。
 *
 * ⚠️ `visibleClinics` 分不出这两种情况,它俩都会回落到"字典里的全部诊所":
 *     · `clinicIds === undefined` —— `/auth/session` **还没回来**(JWT 里根本没有这一项)
 *     · `clinicIds === []`        —— 集团级,**真的**不限诊所
 *   对筛选器来说回落是对的(集团级不该空白),但对"**默认选中哪一家**"是致命的。
 *
 * 🔴 2026-08-10 测试机实测的后果:朝阳公园的主管打开工作台,第一帧 `clinicIds` 还是
 *   undefined → 回落到全部诊所 → 锁了第一个「杭州大厦」→ **带着别家 id 发了一次
 *   `/workload`**,而那时服务端还没有越权闸,那一发**真的把别家 26 位客服的姓名与负载
 *   取回来了**。session 回来后界面自己纠正成朝阳公园,所以肉眼只看见"发了两个请求"。
 *
 * ⇒ 凡是要拿某一家诊所**去发请求**的地方,先等这个为 true。
 *   ⛔ 别改 `visibleClinics` 的回落语义去"顺便修掉" —— 那会让集团级用户的筛选器空掉。
 * ⚠️ 服务端那道闸(`resolveClinicId`)是**独立**的第二层,⛔ 不能因为这里等了就把它省掉:
 *   查询串是用户可改的。
 */
export function clinicScopeReady(user: SessionUser | null): boolean {
  return Array.isArray(user?.clinicIds);
}

interface AuthState {
  accessToken: string | null;
  refreshToken: string | null;
  expiresAt: number | null;
  user: SessionUser | null;
  setTokens: (input: { accessToken: string; refreshToken: string; expiresIn: number }) => void;
  /** 拉 /auth/session,把展开后的 clinicIds/sourceUnits + 现算的 permissions 合并进 user */
  loadSession: () => Promise<void>;
  clear: () => void;
  isAuthenticated: () => boolean;
  /** refresh token 还在(可能 access 过期),允许 silent refresh */
  canRefresh: () => boolean;
}

function decodeJwt(token: string): AccessTokenPayload | null {
  try {
    const [, payload] = token.split('.');
    if (!payload) return null;
    // atob 解出的是二进制字符串(每字符 = 1 字节);直接 JSON.parse 会把多字节
    // UTF-8(如中文诊所名 dictionary)按 Latin-1 误读成乱码,必须按 UTF-8 重新解码。
    const binary = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
    const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
    const json = new TextDecoder('utf-8').decode(bytes);
    return JSON.parse(json) as AccessTokenPayload;
  } catch {
    return null;
  }
}

/**
 * Auth state — 持久化到 localStorage(per-origin)。
 *
 * 解决:iframe / 新标签 F5 后丢 session 的问题。
 *   - URL 上的 ?code= 一次性消费完会被剥掉,F5 后 URL 没 code → 之前内存态会死
 *   - 现在:localStorage 兜底,F5 后从 store 读 token,api-client 自动 silent refresh
 *
 * 安全考量:
 *   - accessToken 2h,泄漏窗口短
 *   - refreshToken 7d,后端有 rotation(刷新即作废老的),被偷会留痕
 *   - XSS 风险:Next + Tailwind 默认无 innerHTML 注入路径,业务上避免 dangerouslySetInnerHTML
 *   - 医疗 SaaS 行业标准做法
 *
 * Safari ITP 注:iframe 第三方 localStorage 可能 7 天没活跃被清。
 * 兜底由调用方处理:store 空 → URL 看有没 code → 都没有 → 引导用户回 host 重开。
 */
export const useAuthStore = create<AuthState>()(
  persist(
    (set, get) => ({
      accessToken: null,
      refreshToken: null,
      expiresAt: null,
      user: null,
      setTokens: ({ accessToken, refreshToken, expiresIn }) => {
        const decoded = decodeJwt(accessToken);
        const prev = get().user;
        // 防闪空:silent refresh 重签 token 时,同一 sub 沿用上次已展开的 clinicIds/sourceUnits
        // 作占位,避免 user 被 decodeJwt 重置后诊所筛选短暂清空(loadSession 随后会刷新)。
        // permissions 同理:decodeJwt 拿到的是**旧 token 的快照**,不留住的话每次 silent refresh
        // 都会让新权限的入口(如分配按钮)闪一下消失 —— 正是 loadSession 刚修好的那个问题。
        const user =
          decoded && prev && prev.sub === decoded.sub
            ? {
                ...decoded,
                clinicIds: prev.clinicIds,
                sourceUnits: prev.sourceUnits,
                permissions: prev.permissions ?? decoded.permissions,
              }
            : decoded;
        set({
          accessToken,
          refreshToken,
          expiresAt: Date.now() + expiresIn * 1000,
          user,
        });
        // 异步补齐/刷新展开后的数据范围(不阻塞登录)
        void get().loadSession();
      },
      loadSession: async () => {
        if (!get().accessToken) return;
        try {
          const { getSession } = await import('@/lib/auth-api');
          const s = await getSession();
          set((st) =>
            st.user
              ? {
                  user: {
                    ...st.user,
                    clinicIds: s.clinicIds,
                    sourceUnits: s.sourceUnits,
                    actionUrls: s.actionUrls,
                    // ⭐ permissions 必须 merge:JWT 里那份是签发时的快照,access token 2h 不刷。
                    //   发版新增一个权限(如 plan:dispatch)后,已登录的人在 token 到期前
                    //   前端一直按老清单渲染 —— 按钮藏着、入口没有,**不报错**,像是功能没上线。
                    //   服务端 /auth/session 已改成按 role 现算(见 auth.controller.session 注释),
                    //   这里接住即可,不必让全员重登。
                    permissions: s.permissions,
                    // session 已把服务端诊所名合并进 dictionary(登录传的优先);无则保留 JWT 里的
                    dictionary: s.dictionary ?? st.user.dictionary,
                  },
                }
              : {},
          );
        } catch {
          // 静默:拿不到就保持 token decode 的基础 user(诊所筛选回退为空)
        }
      },
      clear: () => set({ accessToken: null, refreshToken: null, expiresAt: null, user: null }),
      isAuthenticated: () => {
        const { accessToken, expiresAt } = get();
        return Boolean(accessToken && expiresAt && Date.now() < expiresAt);
      },
      canRefresh: () => Boolean(get().refreshToken),
    }),
    {
      name: 'pac-auth',
      storage: createJSONStorage(() => localStorage),
      // 只持久化原始 token,派生字段(user)反水合时从 token 重新 decode
      partialize: (s) => ({
        accessToken: s.accessToken,
        refreshToken: s.refreshToken,
        expiresAt: s.expiresAt,
      }),
      onRehydrateStorage: () => (state) => {
        if (state?.accessToken) {
          state.user = decodeJwt(state.accessToken);
          // F5/反水合后补齐 clinicIds/sourceUnits(token decode 拿不到)
          void state.loadSession();
        }
      },
    },
  ),
);
