// Global index of enrolled security keys: credentialId → ad_user.
//
// Login is "tap only, no username": login-options sends the credential IDs of ALL enrolled keys
// as allowCredentials, the YubiKey answers for the one it owns, and login-verify maps that
// credential ID back to the user here. Non-discoverable credentials return no userHandle, so
// this index is the only way to know who tapped.
//
// Source of truth stays AD_User.WebAuthnCredentials (adUserCredentials.ts); this is a 60 s
// per-worker read cache with single-flight, invalidated by the enrol / delete routes. PM2 workers
// cache independently — a key enrolled on worker A is visible on worker B after ≤ 60 s, or at once
// when B gets a lookup miss (a miss always triggers a refresh, see lookup()).

import fetchHelper from './fetchHelper'
import type { WebAuthnKeyEntry } from './adUserCredentials'

export interface IndexedCredential {
  userId: number
  userName: string      // ad_user.Name (the login id auth/tokens accepts)
  userValue: string     // ad_user.Value (search key, alternative login id)
  key: WebAuthnKeyEntry
}

interface IndexState {
  byCredential: Map<string, IndexedCredential>
  byUser: Map<number, IndexedCredential[]>
  loadedAt: number
}

const TTL_MS = 60_000
const PAGE = 500
const MAX_USERS = 2000

let state: IndexState | null = null
let inflight: Promise<IndexState> | null = null

function serviceToken(): string {
  const config = useRuntimeConfig()
  const token: string | undefined = (config.api as any)?.idempieretoken
  if (!token) throw new Error('IDEMPIERETOKEN env var is not set — cannot build the security-key index')
  return token
}

function parseKeys(raw: any): WebAuthnKeyEntry[] {
  if (!raw) return []
  try {
    const obj = typeof raw === 'string' ? JSON.parse(raw) : raw
    return Array.isArray(obj?.keys) ? obj.keys.filter((k: any) => k && typeof k.credentialId === 'string') : []
  } catch {
    return []
  }
}

async function load(event: any): Promise<IndexState> {
  const token = serviceToken()
  const byCredential = new Map<string, IndexedCredential>()
  const byUser = new Map<number, IndexedCredential[]>()

  // `neq null` is the only supported null test in idempiere-rest OData (see
  // .claude/rules/idempiere/write-casing-and-flags.md). Paged with $top/$skip, hard-capped.
  for (let skip = 0; skip < MAX_USERS; skip += PAGE) {
    const url = 'models/ad_user?$filter=WebAuthnCredentials neq null and IsActive eq true'
      + '&$select=Name,Value,WebAuthnCredentials&$orderby=AD_User_ID asc'
      + '&$top=' + PAGE + '&$skip=' + skip
    const res: any = await fetchHelper(event, url, 'GET', token, null)
    const records: any[] = Array.isArray(res?.records) ? res.records : []
    for (const rec of records) {
      const userId = Number(rec?.id ?? rec?.AD_User_ID?.id ?? rec?.AD_User_ID)
      if (!userId) continue
      const keys = parseKeys(rec?.WebAuthnCredentials ?? rec?.webAuthnCredentials)
      if (!keys.length) continue
      const userName = String(rec?.Name ?? rec?.name ?? '')
      const userValue = String(rec?.Value ?? rec?.value ?? '')
      const list: IndexedCredential[] = []
      for (const key of keys) {
        const entry: IndexedCredential = { userId, userName, userValue, key }
        byCredential.set(key.credentialId, entry)
        list.push(entry)
      }
      byUser.set(userId, list)
    }
    if (records.length < PAGE) break
  }
  return { byCredential, byUser, loadedAt: Date.now() }
}

async function ensure(event: any, force = false): Promise<IndexState> {
  if (!force && state && Date.now() - state.loadedAt < TTL_MS) return state
  if (!inflight) {
    inflight = load(event)
      .then(s => { state = s; return s })
      .finally(() => { inflight = null })
  }
  return inflight
}

export function invalidateWebAuthnIndex(): void {
  state = null
}

/** All enrolled credentials (allow-list for the username-less login). `fresh` bypasses the cache. */
export async function listAllCredentials(event: any, fresh = false): Promise<IndexedCredential[]> {
  const s = await ensure(event, fresh)
  return Array.from(s.byCredential.values())
}

/** Credentials of one user, matched by Name or Value (case-insensitive). */
export async function listCredentialsForUserName(event: any, userName: string): Promise<IndexedCredential[]> {
  const needle = String(userName ?? '').trim().toLowerCase()
  if (!needle) return []
  const s = await ensure(event)
  const out: IndexedCredential[] = []
  for (const list of s.byUser.values()) {
    const first = list[0]
    if (!first) continue
    if (first.userName.toLowerCase() === needle || first.userValue.toLowerCase() === needle) out.push(...list)
  }
  return out
}

/** Resolve a credential ID to its user. A miss forces one refresh (key enrolled on another worker). */
export async function lookupCredential(event: any, credentialId: string): Promise<IndexedCredential | null> {
  if (!credentialId) return null
  let s = await ensure(event)
  let hit = s.byCredential.get(credentialId)
  if (hit) return hit
  s = await ensure(event, true)
  hit = s.byCredential.get(credentialId)
  return hit ?? null
}
