/**
 * FreePBX Blacklist — NOT a MySQL table (verified on the PBX: `asterisk`/
 * `asteriskcdrdb` have no `blacklist` table). The Blacklist module stores
 * entries directly in Asterisk's internal astdb, family `blacklist`, via AMI
 * `DBPut`/`DBDel` (see `Blacklist.class.php::numberAdd()` /
 * `Blacklist.class.php::numberDel()` on the PBX — this mirrors it exactly).
 * The dialplan's `BLACKLIST()` check reads astdb live on every inbound call,
 * so a DBPut takes effect immediately — no FreePBX "Apply Config" needed.
 *
 * `DBPut`/`DBDel` require the AMI `system` write-privilege class (confirmed
 * via `asterisk -rx "manager show command DBPut"` → Privilege: system,all).
 * The [logship] AMI account (manager_custom.conf on the PBX) was granted
 * `write=…,system` for this feature — deliberately NOT the `command` class
 * (that's what actually gates raw CLI exec via the `Command` AMI action).
 */
import { connectAmi, amiValue, type AmiClient } from './amiClient'
import { randomBytes, randomUUID } from 'node:crypto'

/** Same shape FreePBX's own form accepts; 'dest'/'blocked' are the module's own astdb keys
 *  (blacklist destination / global on-off) — a caller number must never collide with those. */
export const isValidBlacklistNumber = (number: string): boolean =>
  /^\+?\d{3,20}$/.test(number) && !['dest', 'blocked'].includes(number.toLowerCase())

// FreePBX's own "add number" form defaults an empty Description to a bare
// '1' — but its GUI still treats the field as mandatory, so entries added
// there always carry text. We synthesize one the same way (never blank),
// optionally folding in the caller name/label for context.
export const randomBlacklistDescription = (label?: string): string => {
  const tag = `LogShip ${randomBytes(3).toString('hex')}`
  const clean = String(label || '').trim().replace(/[\r\n]+/g, ' ').slice(0, 40)
  return (clean ? `${clean} - ${tag}` : tag).slice(0, 60)
}

const openAmi = async () => {
  const config = useRuntimeConfig()
  const cfg = (config as any).asterisk || {}
  if (!cfg.secret) throw createError({ statusCode: 503, statusMessage: 'Telefonanlage nicht konfiguriert' })
  try {
    return await connectAmi({ host: cfg.host, port: cfg.port, username: cfg.username, secret: cfg.secret })
  } catch (err: any) {
    throw createError({ statusCode: 502, statusMessage: 'Telefonanlage nicht erreichbar' })
  }
}

/** Adds `number` to the FreePBX Blacklist. `description` must be non-empty. */
export const addToBlacklist = async (number: string, description: string): Promise<void> => {
  const ami = await openAmi()
  try {
    const res = await ami.send('DBPut', { Family: 'blacklist', Key: number, Val: description })
    if (amiValue(res, 'Response') !== 'Success') {
      throw createError({ statusCode: 502, statusMessage: amiValue(res, 'Message') || 'Sperren fehlgeschlagen' })
    }
  } finally {
    ami.close()
  }
}

/**
 * Reads ONE blacklist entry on an open AMI session. `DBGet` only needs the `reporting`
 * class. Its `Response` packet is just an ack — `Success` = the key exists (= blocked),
 * `Error` + "Database entry not found" = not blocked (any OTHER error throws → the UI
 * shows "status unknown", never a false "not blocked"); the stored value (our description)
 * arrives separately as `Event: DBGetResponse` with the same ActionID, so a listener is
 * registered BEFORE sending. The value is optional garnish: a missing event never changes
 * the blocked verdict.
 */
const readEntry = async (ami: AmiClient, number: string): Promise<{ blocked: boolean; description: string | null }> => {
  const actionId = randomUUID()
  let off: () => void = () => {}
  const value = new Promise<string | null>((resolve) => {
    const timer = setTimeout(() => resolve(null), 1500)
    timer.unref?.()
    off = ami.onEvent((evt) => {
      if (amiValue(evt, 'Event') === 'DBGetResponse' && amiValue(evt, 'ActionID') === actionId) {
        clearTimeout(timer)
        resolve(amiValue(evt, 'Val') || null)
      }
    })
  })
  try {
    const res = await ami.send('DBGet', { Family: 'blacklist', Key: number }, 5000, actionId)
    if (amiValue(res, 'Response') === 'Success') return { blocked: true, description: await value }
    // Only "Database entry not found" means NOT blocked. Any other error (permission denied,
    // unknown action, …) must not be shown as "not blocked" — surface it as "status unknown".
    const msg = amiValue(res, 'Message')
    if (/not found/i.test(msg)) return { blocked: false, description: null }
    throw createError({ statusCode: 502, statusMessage: msg || 'Sperrstatus konnte nicht gelesen werden' })
  } finally {
    off()
  }
}

/** Blacklist status of several numbers over ONE AMI session (dashboard card: ≤ 10 per page). */
export const getBlacklistStatus = async (numbers: string[]): Promise<Record<string, { blocked: boolean; description: string | null }>> => {
  const out: Record<string, { blocked: boolean; description: string | null }> = {}
  const list = [...new Set(numbers)].filter(isValidBlacklistNumber).slice(0, 50)
  if (!list.length) return out
  const ami = await openAmi()
  try {
    for (const n of list) out[n] = await readEntry(ami, n)
  } finally {
    ami.close()
  }
  return out
}

/** Removes `number` from the FreePBX Blacklist (mirrors Blacklist.class.php::numberDel()). */
export const removeFromBlacklist = async (number: string): Promise<void> => {
  const ami = await openAmi()
  try {
    const res = await ami.send('DBDel', { Family: 'blacklist', Key: number })
    if (amiValue(res, 'Response') !== 'Success') {
      throw createError({ statusCode: 502, statusMessage: amiValue(res, 'Message') || 'Entsperren fehlgeschlagen' })
    }
  } finally {
    ami.close()
  }
}

