/**
 * Server-authoritative access control for the staff live-chat module.
 *
 * The JWT carries only AD_User_ID + AD_Role_ID — NOT IsSystemUser or
 * FrontendMenu — so a client-side getMenuType() check is not enough. Here we
 * resolve the role's FrontendMenu and the user's IsSystemUser flag against
 * iDempiere once, then cache the verdict (TTL) so reconnects/REST calls don't
 * re-hit the backend every time.
 *
 * Fail-closed: any lookup error rejects access.
 *
 * Used both from the WebSocket handler (raw cookie header, no h3 event) and the
 * REST routes (h3 event) — so the iDempiere call goes through $fetch directly
 * with the bearer token rather than fetchHelper(event).
 */

import refreshTokenHelper from './refreshTokenHelper'

const TTL_MS = 10 * 60 * 1000

const g = globalThis as any
const accessCache: Map<number, { ok: boolean; expires: number }> =
  g.__chatAccessCache ?? (g.__chatAccessCache = new Map())
const staffCache: Map<number, { staff: boolean; expires: number }> =
  g.__chatStaffCache ?? (g.__chatStaffCache = new Map())

const now = () => Date.now()

/** Parse a raw Cookie header string into a plain object. (Named to avoid
 *  colliding with h3's auto-imported parseCookies(event).) */
export const parseCookieHeader = (header: string | null | undefined): Record<string, string> => {
  const out: Record<string, string> = {}
  if (!header) return out
  for (const part of header.split(';')) {
    const idx = part.indexOf('=')
    if (idx === -1) continue
    const k = part.slice(0, idx).trim()
    const v = part.slice(idx + 1).trim()
    if (k) out[k] = decodeURIComponent(v)
  }
  return out
}

/** Decode a JWT payload (no signature verification — identity only). */
export const decodeJwt = (token: string): any => {
  const payload = token.split('.')[1]
  if (!payload) throw new Error('malformed token')
  return JSON.parse(Buffer.from(payload, 'base64').toString())
}

const idempiereGet = async (urlPath: string, token: string) => {
  const config = useRuntimeConfig()
  return await $fetch(`${config.api.url}/${urlPath}`, {
    headers: {
      Authorization: 'Bearer ' + token,
      Accept: 'application/json'
    }
  })
}

const readMenuValue = (frontendMenu: any): string | null => {
  if (!frontendMenu) return null
  if (typeof frontendMenu === 'string') return frontendMenu
  return frontendMenu.id ?? frontendMenu.identifier ?? null
}

const isSystemUserFlag = (user: any): boolean =>
  user?.IsSystemUser === true || user?.isSystemUser === true

// Per-user chat opt-in flag (custom ad_user column). Both casings accepted
// because iDempiere OData response casing varies by column.
const isChatEnabledFlag = (user: any): boolean =>
  user?.IsChatEnabled === true || user?.isChatEnabled === true

/**
 * Throws if the (userId, roleId) is not allowed to use chat:
 *   - role FrontendMenu === 'c' (limited/customer) → rejected
 *   - user IsSystemUser !== true → rejected
 * Resolves to true (and caches the verdict) when allowed.
 */
export const assertStaffAccess = async (userId: number, roleId: number, token: string): Promise<true> => {
  const cached = accessCache.get(userId)
  if (cached && cached.expires > now()) {
    if (!cached.ok) throw new Error('chat access denied (cached)')
    return true
  }

  // Role gate
  const role: any = await idempiereGet('models/ad_role/' + roleId, token)
  const menu = readMenuValue(role?.FrontendMenu)
  if (menu === 'c') {
    accessCache.set(userId, { ok: false, expires: now() + TTL_MS })
    throw new Error('chat access denied (limited role)')
  }

  // User gate — must be a system user AND have opted into chat (IsChatEnabled).
  const user: any = await idempiereGet('models/ad_user/' + userId, token)
  if (!isSystemUserFlag(user)) {
    accessCache.set(userId, { ok: false, expires: now() + TTL_MS })
    throw new Error('chat access denied (not a system user)')
  }
  if (!isChatEnabledFlag(user)) {
    accessCache.set(userId, { ok: false, expires: now() + TTL_MS })
    throw new Error('chat access denied (chat disabled for user)')
  }

  accessCache.set(userId, { ok: true, expires: now() + TTL_MS })
  staffCache.set(userId, { staff: true, expires: now() + TTL_MS }) // we just proved it
  return true
}

/**
 * Best-effort check that a message recipient is a valid chat participant — a
 * staff (IsSystemUser) user who has opted into chat (IsChatEnabled) — so a DM
 * can't be addressed to a customer's user_id or a colleague who turned chat
 * off. Returns:
 *   true  → confirmed chat-enabled staff
 *   false → confirmed NOT a valid recipient (reject the send)
 *   null  → indeterminate (lookup failed, e.g. token expired) → caller may allow
 * Cached by user id.
 */
export const isStaffUser = async (userId: number, token: string): Promise<boolean | null> => {
  const cached = staffCache.get(userId)
  if (cached && cached.expires > now()) return cached.staff
  try {
    const user: any = await idempiereGet('models/ad_user/' + userId, token)
    const staff = isSystemUserFlag(user) && isChatEnabledFlag(user)
    staffCache.set(userId, { staff, expires: now() + TTL_MS })
    return staff
  } catch {
    return null
  }
}

/**
 * REST-route gate: reads the logship_it cookie from the h3 event, decodes it,
 * and runs assertStaffAccess. Throws an h3 error (401/403) on failure.
 * Returns { userId, roleId, token } on success.
 */
export const gateFromEvent = async (event: any): Promise<{ userId: number; roleId: number; token: string }> => {
  const token = getCookie(event, 'logship_it')
  if (!token) throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
  let userId: number, roleId: number
  try {
    const p = decodeJwt(token)
    userId = Number(p.AD_User_ID)
    roleId = Number(p.AD_Role_ID)
  } catch {
    throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
  }
  if (!userId || !roleId) throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })

  try {
    await assertStaffAccess(userId, roleId, token)
    return { userId, roleId, token }
  } catch {
    // A definitive, freshly-cached denial (limited role / not a chat-enabled
    // system user) is final — don't waste a token refresh on it.
    const denied = accessCache.get(userId)
    if (denied && denied.expires > now() && denied.ok === false) {
      throw createError({ statusCode: 403, statusMessage: 'Forbidden' })
    }
    // Otherwise the failure is likely an expired access token — refresh once and
    // retry, mirroring the app-wide try/refreshTokenHelper/retry pattern. The
    // refresh also Set-Cookies a fresh logship_it, which lets the chat WebSocket
    // succeed on its next reconnect (the WS handler can't refresh on its own).
    try {
      const fresh = await refreshTokenHelper(event)
      await assertStaffAccess(userId, roleId, fresh)
      return { userId, roleId, token: fresh }
    } catch {
      throw createError({ statusCode: 403, statusMessage: 'Forbidden' })
    }
  }
}
