import fetchHelper from './fetchHelper'
import { decodeJwt } from './chatAccess'

/**
 * Ticket-visibility helpers for the request/ticket routes.
 *
 * Why: `r_requestupdate` rows carry the AD_Org_ID they were POSTed with — for a
 * long time that was the STAFF member's currently active org (LogYou), not the
 * ticket's org. A limited merchant role ('c') only has org access to its own org,
 * so iDempiere's row-level security silently hid every such staff reply from the
 * merchant (they saw only their own messages). AD_Org_ID is not updateable on
 * R_Request / R_RequestUpdate / R_RequestAction, so the rows can't be re-stamped.
 *
 * Pattern (same as `[id]/author-orgs.get.ts` and `merchantPartner.ts`):
 *  1. Access to the TICKET is always proven with the CALLER's own token first
 *     (`assertTicketVisible`) — iDempiere security decides, exactly as before.
 *  2. Only then are the ticket's child rows read with the SERVICE token
 *     (`config.api.idempieretoken`), and only for a limited viewer.
 *  3. Rows a merchant must never see (internal notes 'I', [Zeitabrechnung]
 *     billing blocks) are dropped server-side (`isHiddenForLimited`) — the
 *     client keeps its own filter as well.
 *
 * Role resolution is cached per role id (10 min). Any failure resolves to "not
 * limited", which keeps today's caller-token behaviour — never wider access.
 */

const TTL_MS = 10 * 60 * 1000
const g = globalThis as any
const roleMenuCache: Map<number, { menu: string | null; expires: number }> =
  g.__ticketRoleMenuCache ?? (g.__ticketRoleMenuCache = new Map())

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

/** The iDempiere service (SuperUser) token, or undefined when not configured. */
export const getServiceToken = (): string | undefined => {
  const config = useRuntimeConfig()
  return (config.api as any)?.idempieretoken || undefined
}

/** JWT AD_Role_ID of the caller (the session token), or null. */
export const resolveOwnRoleId = (event: any): number | null => {
  const token = getCookie(event, 'logship_it')
  if (!token) return null
  try {
    const roleId = Number(decodeJwt(token)?.AD_Role_ID)
    return Number.isFinite(roleId) && roleId > 0 ? roleId : null
  } catch {
    return null
  }
}

/**
 * The caller role's FrontendMenu value ('c' = limited Fulfillment Customer),
 * resolved with the service token and cached. `null` when unknown.
 */
export const getRoleMenuType = async (event: any): Promise<string | null> => {
  const roleId = resolveOwnRoleId(event)
  if (!roleId) return null
  const hit = roleMenuCache.get(roleId)
  if (hit && hit.expires > Date.now()) return hit.menu
  const serviceToken = getServiceToken()
  if (!serviceToken) return null
  try {
    const role: any = await fetchHelper(event, `models/ad_role/${roleId}?$select=FrontendMenu`, 'GET', serviceToken, null)
    const menu = readMenuValue(role?.FrontendMenu)
    roleMenuCache.set(roleId, { menu, expires: Date.now() + TTL_MS })
    return menu
  } catch {
    // Not cached — a transient failure must not stick for the process lifetime.
    return null
  }
}

/** True only when the caller is PROVEN to be a limited merchant role. */
export const isLimitedViewer = async (event: any): Promise<boolean> =>
  (await getRoleMenuType(event)) === 'c'

/**
 * Access check with the CALLER's token: throws (iDempiere 401/403/404) when the
 * caller may not read the ticket. Returns the ticket's id + org id.
 */
export const assertTicketVisible = async (event: any, token: string, id: string | number): Promise<{ id: number, orgId: number | null }> => {
  if (!id || !/^\d+$/.test(String(id))) {
    throw createError({ statusCode: 400, statusMessage: 'Invalid ticket id' })
  }
  const ticket: any = await fetchHelper(event, `models/r_request/${id}?$select=AD_Org_ID`, 'GET', token, null)
  if (!ticket?.id) throw createError({ statusCode: 404, statusMessage: 'Ticket not found' })
  const orgId = Number(ticket?.AD_Org_ID?.id ?? ticket?.AD_Org_ID)
  return { id: Number(ticket.id), orgId: Number.isFinite(orgId) ? orgId : null }
}

/**
 * Server-side twin of MyTicketDetail.vue's `isHiddenForLimited`: an entry a
 * limited merchant role must never receive — an internal note ('I') or any text
 * carrying a [Zeitabrechnung] billing block.
 */
export const isHiddenForLimited = (row: any): boolean => {
  const conf = row?.ConfidentialTypeEntry?.id ?? row?.ConfidentialTypeEntry
  if (conf === 'I') return true
  const text = String(row?.Result ?? '') + ' ' + String(row?.Summary ?? '')
  return /\[Zeitabrechnung\]/.test(text)
}

/** Drops hidden rows from an iDempiere list response (records + counts). */
export const filterListForLimited = (res: any): any => {
  if (!res?.records || !Array.isArray(res.records)) return res
  const records = res.records.filter((r: any) => !isHiddenForLimited(r))
  return { ...res, records, 'row-count': records.length, 'records-size': records.length }
}
