/**
 * PBX extension directory for the dial popover's extension chooser.
 *
 * Devices come from AMI `DeviceStateList` (needs the `reporting` write class —
 * granted to the [logship] manager user): every `PJSIP/<digits>` device with
 * its current state. The extension's display name (FreePBX "Display Name" /
 * CallerID name) lives in the astdb as `AMPUSER/<ext>/cidname` and is read
 * with AMI `DBGet` — also gated by `reporting`, so no extra manager class is
 * needed (`PJSIPShowEndpoints` would need `system`). Names are cached 10 min
 * per extension. Extensions without a PBX name fall back to the ERP: active
 * system users whose SIP_Extension matches (cached 5 min). Devices are cached
 * 30 s. Fail-soft: an AMI or iDempiere error yields an empty / name-less list.
 */
import { connectAmi, amiValue, type AmiClient } from './amiClient'
import { string } from 'alga-js'

export interface PbxExtension { ext: string; state: string; pbxName: string }
export interface DirectoryExtension extends PbxExtension { name: string; userId: number | null }

const g = globalThis as any
const devCache: { list: PbxExtension[]; expires: number } = g.__dialDevCache ?? (g.__dialDevCache = { list: [], expires: 0 })
const nameCache: { map: Map<string, { name: string; userId: number }>; expires: number } = g.__dialNameCache ?? (g.__dialNameCache = { map: new Map(), expires: 0 })
/** astdb `AMPUSER/<ext>/cidname` per extension (`''` = no entry), each with its own expiry. */
const pbxNameCache: Map<string, { name: string; expires: number }> = g.__dialPbxNameCache ?? (g.__dialPbxNameCache = new Map())
const PBX_NAME_TTL = 10 * 60 * 1000

const STATE_LABEL: Record<string, string> = {
  NOT_INUSE: 'frei', INUSE: 'im Gespräch', BUSY: 'besetzt', RINGING: 'klingelt', RINGINUSE: 'klingelt',
  ONHOLD: 'gehalten', UNAVAILABLE: 'nicht erreichbar', INVALID: 'ungültig', UNKNOWN: 'unbekannt'
}
export const stateLabel = (s: string) => STATE_LABEL[String(s || '').toUpperCase()] || String(s || '').toLowerCase()

/**
 * AMI `DBGet`: the response only says "Result will follow"; the value arrives
 * as a `DBGetResponse` event and the list ends with `DBGetComplete` (both
 * carrying our ActionID). Resolves `null` when the key does not exist.
 */
const dbGet = (ami: AmiClient, family: string, key: string, timeoutMs = 3000): Promise<string | null> => {
  return new Promise<string | null>((resolve) => {
    const actionId = `dbget-${family}-${key}-${Math.random().toString(36).slice(2, 8)}`
    let value: string | null = null
    let done = false
    const finish = (v: string | null) => { if (done) return; done = true; clearTimeout(timer); off(); resolve(v) }
    const off = ami.onEvent((evt) => {
      if (amiValue(evt, 'ActionID') !== actionId) return
      const name = amiValue(evt, 'Event')
      if (name === 'DBGetResponse') value = amiValue(evt, 'Val')
      else if (name === 'DBGetComplete') finish(value)
    })
    const timer = setTimeout(() => finish(value), timeoutMs)
    timer.unref?.()
    ami.send('DBGet', { Family: family, Key: key }, timeoutMs, actionId).then((res) => {
      if (amiValue(res, 'Response') !== 'Success') finish(null) // "Database entry not found"
    }).catch(() => finish(null))
  })
}

/** Fills `pbxNameCache` for the given extensions (only expired / unknown ones are queried). */
const loadPbxNames = async (ami: AmiClient, exts: string[]) => {
  const now = Date.now()
  const missing = exts.filter((ext) => (pbxNameCache.get(ext)?.expires ?? 0) <= now)
  if (missing.length === 0) return
  await Promise.all(missing.map(async (ext) => {
    try {
      const v = await dbGet(ami, 'AMPUSER', `${ext}/cidname`)
      pbxNameCache.set(ext, { name: String(v ?? '').trim(), expires: Date.now() + PBX_NAME_TTL })
    } catch {
      // keep whatever was cached; retry on the next refresh
    }
  }))
}

export const listPbxExtensions = async (): Promise<PbxExtension[]> => {
  if (devCache.expires > Date.now()) return devCache.list
  const config = useRuntimeConfig()
  const cfg = (config as any).asterisk || {}
  if (!cfg.enabled || !cfg.secret) return []
  const ami = await connectAmi({ host: cfg.host, port: cfg.port, username: cfg.username, secret: cfg.secret })
  try {
    const found = new Map<string, string>()
    const off = ami.onEvent((evt) => {
      if (amiValue(evt, 'Event') !== 'DeviceStateChange') return
      const m = /^(?:PJSIP|SIP)\/(\d{2,6})$/.exec(amiValue(evt, 'Device'))
      if (m) found.set(m[1], amiValue(evt, 'State'))
    })
    const res = await ami.send('DeviceStateList', {}, 8000)
    if (amiValue(res, 'Response') !== 'Success') throw new Error(amiValue(res, 'Message') || 'DeviceStateList failed')
    // The list is streamed as events after the response; give it a moment.
    await new Promise((r) => setTimeout(r, 400))
    off()
    const exts = [...found.keys()]
    try { await loadPbxNames(ami, exts) } catch (err: any) { console.warn('[dial] extension names (astdb) lookup failed:', err?.message || err) }
    const list = exts
      .map((ext) => ({ ext, state: found.get(ext) || '', pbxName: pbxNameCache.get(ext)?.name || '' }))
      .sort((a, b) => a.ext.localeCompare(b.ext, undefined, { numeric: true }))
    devCache.list = list
    devCache.expires = Date.now() + 30 * 1000
    return list
  } finally {
    ami.close()
  }
}

const loadNames = async (): Promise<Map<string, { name: string; userId: number }>> => {
  if (nameCache.expires > Date.now()) return nameCache.map
  const config = useRuntimeConfig()
  const token = (config.api as any)?.idempieretoken
  const map = new Map<string, { name: string; userId: number }>()
  if (token) {
    try {
      const res: any = await $fetch(`${config.api.url}/models/ad_user?$filter=${string.urlEncode('IsSystemUser eq true AND IsActive eq true')}&$top=200`, {
        headers: { Authorization: 'Bearer ' + token, Accept: 'application/json' }, retry: 0, timeout: 15000
      })
      for (const u of (res?.records || [])) {
        const ext = String(u?.SIP_Extension ?? u?.sip_Extension ?? '').replace(/\D+/g, '')
        if (ext) map.set(ext, { name: String(u?.Name || ''), userId: Number(u?.id) })
      }
    } catch (err: any) {
      console.warn('[dial] extension names lookup failed:', err?.data?.message || err?.message || err)
    }
  }
  nameCache.map = map
  nameCache.expires = Date.now() + 5 * 60 * 1000
  return map
}

export const extensionDirectory = async (): Promise<DirectoryExtension[]> => {
  const [devices, names] = await Promise.all([listPbxExtensions().catch(() => [] as PbxExtension[]), loadNames()])
  // Display name: the PBX's own extension name first, the matching ERP user as fallback.
  return devices.map((d) => ({ ...d, name: d.pbxName || names.get(d.ext)?.name || '', userId: names.get(d.ext)?.userId ?? null }))
}
