/**
 * Auth store — manages JWT, current user, and permission checks.
 *
 * U7 (Centralized Auth & Token Persistence):
 * - 3-state startup: `valid` / `invalid` / `error` / `pending`
 * - Access token in memory only (NEVER written to localStorage)
 * - Refresh token persisted via tauriAuthStorage (Tauri Keychain or
 *   Web localStorage fallback)
 * - Pre-emptive refresh: when access expires in <2 min, refresh BEFORE
 *   the next request fires (no 401 storms)
 *
 * Token lifecycle:
 * - ``login()`` calls /auth/login, stores both tokens.
 * - ``startupCheck()`` runs on app boot — calls /auth/whoami with the
 *   persisted refresh token to validate it and rehydrate the user.
 * - On 401 from the API, ``BaseApiClient`` calls ``silentRefresh()``;
 *   if refresh succeeds, the original request is retried; if refresh
 *   fails, the store transitions to `invalid` and the router redirects
 *   to /login.
 * - ``logout()`` calls the server to revoke the refresh token, then
 *   clears local state regardless of the server response.
 */

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { authApi, type IAuthUser, type ITokenPair, type ISessionInfo } from '@/api/auth'
import { tauriAuthStorage } from '@/api/tauri-auth'
import { isTauri, startBackend, checkBackendHealth } from '@/api/tauri'
import { initApiBaseURL, setTokenProvider, setRefreshProvider, setUnauthorizedHandler, setPreEmptiveRefreshProvider } from '@/api/base'

const USER_KEY = 'agentkit.user'

/** 3-state startup (F11) + a `pending` initial value. */
export type AuthStartupState = 'valid' | 'invalid' | 'error' | 'pending'

/** Permission bits — must mirror server-side ``Permission`` enum. */
export type Permission =
  | 'CHAT'
  | 'KB_QUERY'
  | 'KB_WRITE'
  | 'WORKFLOW_EXECUTE'
  | 'TERMINAL_LOCAL_USE'
  | 'TERMINAL_SERVER_USE'
  | 'TERMINAL_WHITELIST_MANAGE'
  | 'USER_MANAGE'
  | 'SYSTEM_CONFIG'

/** Role → permission mapping (mirrors server-side ROLE_PERMISSIONS). */
const ROLE_PERMISSIONS: Record<string, Permission[]> = {
  member: ['CHAT', 'KB_QUERY', 'WORKFLOW_EXECUTE'],
  operator: [
    'CHAT',
    'KB_QUERY',
    'KB_WRITE',
    'WORKFLOW_EXECUTE',
    'TERMINAL_LOCAL_USE',
    'TERMINAL_WHITELIST_MANAGE',
  ],
  admin: [
    'CHAT',
    'KB_QUERY',
    'KB_WRITE',
    'WORKFLOW_EXECUTE',
    'TERMINAL_LOCAL_USE',
    'TERMINAL_SERVER_USE',
    'TERMINAL_WHITELIST_MANAGE',
    'USER_MANAGE',
    'SYSTEM_CONFIG',
  ],
}

/** Pre-emptive refresh threshold — refresh when access expires within this window. */
const REFRESH_HEADROOM_SECONDS = 120

function readStored(key: string): string | null {
  try {
    return localStorage.getItem(key)
  } catch {
    return null
  }
}

function writeStored(key: string, value: string): void {
  try {
    localStorage.setItem(key, value)
  } catch {
    /* localStorage may be unavailable in some sandboxed contexts */
  }
}

function removeStored(key: string): void {
  try {
    localStorage.removeItem(key)
  } catch {
    /* ignore */
  }
}

function readStoredUser(): IAuthUser | null {
  const raw = readStored(USER_KEY)
  if (!raw) return null
  try {
    return JSON.parse(raw) as IAuthUser
  } catch {
    return null
  }
}

/**
 * Decode the ``exp`` claim from a JWT without verifying the signature.
 *
 * The signature is verified by the server; this is only used to compute
 * the remaining lifetime client-side for the pre-emptive refresh
 * trigger.  Tampered tokens will fail server-side validation; reading
 * ``exp`` from an unverified token is safe in this context (no
 * authorization decision is made based on the result).
 */
function decodeJwtExp(token: string | null): number | null {
  if (!token) return null
  const parts = token.split('.')
  if (parts.length !== 3) return null
  try {
    const payload = parts[1]
    if (!payload) return null
    const padded = payload.replace(/-/g, '+').replace(/_/g, '/')
    const decoded = atob(padded)
    const obj = JSON.parse(decoded) as { exp?: unknown }
    return typeof obj.exp === 'number' ? obj.exp : null
  } catch {
    return null
  }
}

export const useAuthStore = defineStore('auth', () => {
  // --- State ---
  /**
   * Access token.  Memory only — never written to localStorage or
   * Keychain.  On app restart the store re-acquires one via
   * ``startupCheck()`` → ``/auth/whoami``.
   */
  const accessToken = ref<string | null>(null)
  const user = ref<IAuthUser | null>(readStoredUser())
  const startupState = ref<AuthStartupState>('pending')
  const isLoading = ref(false)
  const error = ref<string | null>(null)
  /** Current session id (sid) — set on login / whoami. */
  const sessionId = ref<string | null>(null)

  // --- Getters ---
  const isAuthenticated = computed(
    () => startupState.value === 'valid' && !!accessToken.value && !!user.value,
  )
  const role = computed<string | null>(() => user.value?.role ?? null)
  const accessTokenExp = computed<number | null>(() => decodeJwtExp(accessToken.value))
  /** True when the access token will expire within ``REFRESH_HEADROOM_SECONDS``. */
  const shouldRefresh = computed<boolean>(() => {
    const exp = accessTokenExp.value
    if (!exp) return false
    const remainingMs = exp * 1000 - Date.now()
    return remainingMs < REFRESH_HEADROOM_SECONDS * 1000 && remainingMs > -30_000
  })

  /** Permissions for the current role (empty when not authenticated). */
  const permissions = computed<Permission[]>(() => {
    if (!role.value) return []
    return ROLE_PERMISSIONS[role.value] ?? []
  })

  // --- Mutators ---
  /**
   * Persist the access token in memory (NOT localStorage) and the user
   * in localStorage (safe — no secret).  Refresh token is written to
   * Keychain (Tauri) / localStorage fallback by the caller via
   * tauriAuthStorage.
   */
  function _setAccess(token: string, currentUser: IAuthUser, sid: string | null = null): void {
    accessToken.value = token
    user.value = currentUser
    sessionId.value = sid
    writeStored(USER_KEY, JSON.stringify(currentUser))
  }

  /** Persist a fresh token pair from /auth/login or /auth/refresh. */
  async function _persistTokenPair(pair: ITokenPair, sid: string | null = null): Promise<void> {
    _setAccess(pair.access_token, pair.user, sid)
    await tauriAuthStorage.setRefreshToken(pair.refresh_token)
  }

  /** Clear the in-memory access token. Does NOT touch the refresh token. */
  function _clear(): void {
    accessToken.value = null
    sessionId.value = null
    // user is intentionally retained so the UI can show the last-known
    // avatar/role after a forced logout.  Use ``logout()`` to fully
    // clear.
  }

  // --- Actions ---
  /**
   * Authenticate with username + password.
   *
   * @param rememberMe When true, the server issues a 30-day refresh
   *   token (vs default 7 days).  See /auth/login's ``remember_me`` flag.
   */
  async function login(
    username: string,
    password: string,
    rememberMe: boolean = false,
  ): Promise<void> {
    isLoading.value = true
    error.value = null
    try {
      const pair = await authApi.login(username, password, rememberMe)
      // The server's /auth/login response carries session_id (V2)
      // or no session id (legacy).  Either is fine.
      const sid = (pair as ITokenPair & { session_id?: string }).session_id ?? null
      await _persistTokenPair(pair, sid)
      startupState.value = 'valid'
    } catch (err) {
      const msg = _extractErrorMessage(err, '登录失败')
      error.value = msg
      throw err
    } finally {
      isLoading.value = false
    }
  }

  /**
   * Cold-start validation: check the persisted refresh token against
   * the server and rehydrate the user / access token.
   *
   * Transitions:
   *  - no refresh token → 'invalid' (first launch, no login)
   *  - whoami 200       → 'valid'
   *  - whoami 401       → 'invalid' + clear stored refresh token
   *  - whoami network   → 'error' (retryable, refresh token kept)
   */
  async function startupCheck(): Promise<AuthStartupState> {
    startupState.value = 'pending'
    const refresh = await tauriAuthStorage.getRefreshToken()
    if (!refresh) {
      startupState.value = 'invalid'
      return startupState.value
    }
    try {
      const result = await authApi.whoami(refresh)
      // whoami returns { user, access_token, session } when called
      // with a refresh token.
      const sid = result.session?.id ?? null
      _setAccess(result.access_token, result.user, sid)
      startupState.value = 'valid'
    } catch (err) {
      const status = _extractStatus(err)
      if (status === 401 || status === 403 || status === 404) {
        await tauriAuthStorage.clearRefreshToken()
        _clear()
        startupState.value = 'invalid'
      } else {
        // Network error or 5xx — keep the refresh token so the user
        // can retry.  This is the "error" state the UI should surface
        // as "正在重试" rather than "请重新登录".
        startupState.value = 'error'
        error.value = _extractErrorMessage(err, '无法连接服务器')
      }
    }
    return startupState.value
  }

  /**
   * Exchange the persisted refresh token for a new token pair.
   *
   * On 401 the refresh token is treated as revoked (reuse detected
   * or all-sessions-killed) and the store transitions to 'invalid'.
   * Concurrent callers will all await the same in-flight promise so
   * we don't fire multiple refreshes when many 401s land at once.
   */
  let _refreshInFlight: Promise<ITokenPair | null> | null = null
  async function silentRefresh(): Promise<ITokenPair | null> {
    if (_refreshInFlight) return _refreshInFlight
    _refreshInFlight = _doSilentRefresh()
    try {
      return await _refreshInFlight
    } finally {
      _refreshInFlight = null
    }
  }

  async function _doSilentRefresh(): Promise<ITokenPair | null> {
    const refresh = await tauriAuthStorage.getRefreshToken()
    if (!refresh) {
      _clear()
      startupState.value = 'invalid'
      return null
    }
    try {
      const pair = await authApi.refresh(refresh)
      const sid = (pair as ITokenPair & { session_id?: string }).session_id ?? null
      await _persistTokenPair(pair, sid)
      startupState.value = 'valid'
      return pair
    } catch (err) {
      const status = _extractStatus(err)
      if (status === 401) {
        // Reuse detected / all sessions revoked / token expired.
        await tauriAuthStorage.clearRefreshToken()
        _clear()
        startupState.value = 'invalid'
        error.value = _extractErrorMessage(err, '会话已过期，请重新登录')
      } else {
        // Transient — keep the refresh token, leave state as-is.
        error.value = _extractErrorMessage(err, '刷新失败')
      }
      return null
    }
  }

  /**
   * Server-side logout: revoke the refresh token, then clear local state.
   * Safe to call even if the server is unreachable — local state is
   * always cleared.
   */
  async function logout(): Promise<void> {
    const refresh = await tauriAuthStorage.getRefreshToken()
    if (refresh) {
      try {
        await authApi.logout(refresh)
      } catch {
        /* server may be unreachable; still clear local state */
      }
    }
    await tauriAuthStorage.clearRefreshToken()
    _clear()
    user.value = null
    removeStored(USER_KEY)
    startupState.value = 'invalid'
  }

  /** Clear local auth state without calling the server (used on 401 fallback). */
  async function logoutLocal(): Promise<void> {
    await tauriAuthStorage.clearRefreshToken()
    _clear()
    user.value = null
    removeStored(USER_KEY)
    startupState.value = 'invalid'
  }

  // --- Cold-start orchestration (fixes Tauri reload → /login bug) ---
  //
  // The problem: ``router.beforeEach`` runs BEFORE ``App.vue::onMounted``,
  // so on a Tauri reload the guard sees ``startupState === 'pending'`` and
  // ``accessToken === null`` (memory-only), concludes ``isAuthenticated ===
  // false``, and redirects to /login — before ``startupCheck()`` ever runs.
  //
  // Fix: ``main.ts`` calls ``beginStartup()`` BEFORE ``app.mount()``. The
  // router guard then ``await``s ``waitForStartup()`` so it doesn't make
  // a routing decision until the cold-start probe is done.

  let _startupPromise: Promise<AuthStartupState> | null = null
  /** Human-readable progress string for the SplashScreen (Tauri mode). */
  const bootstrapStatus = ref('')

  /**
   * Kick off the cold-start sequence (Tauri backend bootstrap + whoami).
   * Called from ``main.ts`` before ``app.mount()``. Idempotent — subsequent
   * calls are no-ops (the in-flight promise is reused).
   */
  function beginStartup(): void {
    if (_startupPromise) return
    _startupPromise = _doStartup()
  }

  /**
   * The actual cold-start work. In Tauri mode the Python sidecar must be
   * up and the API base URL configured BEFORE we can call ``/auth/whoami``.
   */
  async function _doStartup(): Promise<AuthStartupState> {
    if (isTauri()) {
      try {
        bootstrapStatus.value = '正在启动后端...'
        await startBackend()
        bootstrapStatus.value = '正在配置连接...'
        await initApiBaseURL()
        bootstrapStatus.value = '正在检查服务...'
        const healthy = await checkBackendHealth()
        if (!healthy) {
          startupState.value = 'error'
          error.value =
            '后端服务健康检查失败，请确认 agentkit serve 已在 ' +
            (import.meta.env.BACKEND_PORT || '18001') +
            ' 端口运行后点击重试。'
          return startupState.value
        }
      } catch (err: unknown) {
        startupState.value = 'error'
        error.value = err instanceof Error ? err.message : String(err)
        return startupState.value
      }
    }
    bootstrapStatus.value = '正在验证登录状态...'
    return await startupCheck()
  }

  /**
   * Await the cold-start probe. Used by ``router.beforeEach`` so the guard
   * doesn't redirect to /login while the refresh-token validation is still
   * in flight.
   */
  async function waitForStartup(): Promise<AuthStartupState> {
    if (!_startupPromise) {
      beginStartup()
    }
    return _startupPromise!
  }

  /** Force re-evaluation of the startup state (e.g. after a manual retry). */
  async function retryStartup(): Promise<AuthStartupState> {
    _startupPromise = null
    startupState.value = 'pending'
    error.value = null
    beginStartup()
    return await waitForStartup()
  }

  // --- Session management (U4/U8 surface) ---

  async function listSessions(): Promise<ISessionInfo[]> {
    return await authApi.listSessions()
  }

  async function revokeSession(sid: string): Promise<void> {
    await authApi.revokeSession(sid)
  }

  async function changePassword(
    oldPassword: string,
    newPassword: string,
  ): Promise<{ changed: boolean; revoked_other_sessions: number }> {
    return await authApi.changePassword(oldPassword, newPassword)
  }

  // --- Permission helpers ---
  function hasPermission(perm: Permission): boolean {
    return permissions.value.includes(perm)
  }

  function canUseLocalTerminal(): boolean {
    return !!user.value?.is_terminal_authorized && hasPermission('TERMINAL_LOCAL_USE')
  }

  function canUseServerTerminal(): boolean {
    return (
      !!user.value?.is_server_terminal_authorized &&
      hasPermission('TERMINAL_SERVER_USE')
    )
  }

  function isAdmin(): boolean {
    return role.value === 'admin'
  }

  // Register providers with the API client.  Must run at store-creation
  // time (i.e. before the first API call) so request() can read the
  // current access token.  Without this, every protected endpoint gets
  // a missing Authorization header → 401 → router → /login loop.
  setTokenProvider(() => accessToken.value)
  setRefreshProvider(async () => {
    const pair = await silentRefresh()
    return pair?.access_token ?? null
  })
  setPreEmptiveRefreshProvider(() => shouldRefresh.value)
  setUnauthorizedHandler(() => {
    // _doSilentRefresh already handles state transitions correctly:
    // - 401 (token truly invalid): clears refresh token + sets state='invalid'
    // - transient error (network/5xx): KEEPS refresh token for retry
    //
    // Do NOT call logoutLocal() here unconditionally — it clears the
    // refresh token even on transient failures, defeating _doSilentRefresh's
    // intentional "keep token for retry" design. This was the root cause of
    // the reload-to-login bug: a single transient refresh failure during
    // normal use cleared the token, so the next reload found no token → /login.
    //
    // Only redirect immediately if the state is already 'invalid' (set by
    // _doSilentRefresh's 401 path). For transient failures, let the 401
    // error propagate to the caller; the user can retry.
    if (startupState.value === 'invalid' && window.location.pathname !== '/login') {
      window.location.href = '/login'
    }
  })

  return {
    // state
    accessToken,
    user,
    isLoading,
    error,
    sessionId,
    startupState,
    bootstrapStatus,
    // getters
    isAuthenticated,
    role,
    permissions,
    accessTokenExp,
    shouldRefresh,
    // actions
    login,
    logout,
    logoutLocal,
    silentRefresh,
    startupCheck,
    beginStartup,
    waitForStartup,
    retryStartup,
    listSessions,
    revokeSession,
    changePassword,
    // permission helpers
    hasPermission,
    canUseLocalTerminal,
    canUseServerTerminal,
    isAdmin,
  }
})

function _extractErrorMessage(err: unknown, fallback: string): string {
  if (err && typeof err === 'object') {
    const obj = err as { message?: unknown; detail?: unknown }
    if (typeof obj.message === 'string' && obj.message) return obj.message
    if (typeof obj.detail === 'string' && obj.detail) return obj.detail
  }
  return fallback
}

function _extractStatus(err: unknown): number | null {
  if (err && typeof err === 'object' && 'status' in err) {
    const s = (err as { status?: unknown }).status
    if (typeof s === 'number') return s
  }
  return null
}

// Re-export the session info type so callers can ``import { ISessionInfo } from '@/stores/auth'``
export type { ISessionInfo }
