/**
 * Shared Lexware/lexoffice API client bits used by more than one route
 * (server/api/invoices/lexoffice-upload.post.ts, lexoffice-categories.get.ts).
 * Route-specific logic (contact lookup/creation, the debug-log collector, the plain
 * /v1/files upload) stays local to lexoffice-upload.post.ts — only the credential,
 * the global throttle and the voucher-booking calls live here.
 */
export const LEXOFFICE_API_URL = "https://api.lexware.io"
export const LEXOFFICE_API_KEY = "EVrdrnU7rEhJp93WAJ8MhwPAP.tYOwKTj27Ep2D8fTelLnIJ"

export const lexAuthHeaders = (json: boolean): Record<string, string> => ({
  'Authorization': `Bearer ${LEXOFFICE_API_KEY}`,
  'Accept': 'application/json',
  ...(json ? { 'Content-Type': 'application/json' } : {}),
})

const sleep = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms))

/**
 * Global Lexware throttle — the ONLY way this process may talk to Lexware.
 *
 * Lexware allows 2 requests/second per API client (token bucket, all endpoints
 * combined) and answers 429 when exceeded. `lexFetch` therefore
 *   (a) reserves call starts at least LEX_MIN_INTERVAL_MS apart (process-wide, so
 *       concurrent uploads from several requests still share one budget),
 *   (b) keeps at most LEX_MAX_INFLIGHT calls in flight, and
 *   (c) retries 429/503 with backoff (honouring Retry-After when present).
 * It is process-local: with PM2 running N instances the effective rate is N×; the
 * 429 retry covers that case.
 */
const LEX_MIN_INTERVAL_MS = 520
const LEX_MAX_INFLIGHT = 2
const LEX_MAX_RETRIES = 3

let nextStartAt = 0
let inflight = 0
const slotWaiters: Array<() => void> = []

const acquireSlot = async (): Promise<void> => {
  while (inflight >= LEX_MAX_INFLIGHT) {
    await new Promise<void>(resolve => slotWaiters.push(resolve))
  }
  inflight++
  // Reserve the next start timestamp BEFORE sleeping so two concurrent callers
  // can never claim the same slot (each reservation advances the pacer).
  const startAt = Math.max(Date.now(), nextStartAt)
  nextStartAt = startAt + LEX_MIN_INTERVAL_MS
  const wait = startAt - Date.now()
  if (wait > 0) await sleep(wait)
}

const releaseSlot = (): void => {
  inflight--
  slotWaiters.shift()?.()
}

/** Throttled, 429-aware `fetch` against the Lexware API. `path` starts with `/v1/...`. */
export const lexFetch = async (path: string, init: RequestInit): Promise<Response> => {
  for (let attempt = 0; ; attempt++) {
    await acquireSlot()
    let response: Response
    try {
      response = await fetch(`${LEXOFFICE_API_URL}${path}`, init)
    } finally {
      releaseSlot()
    }
    const retryable = response.status === 429 || response.status === 503
    if (!retryable || attempt >= LEX_MAX_RETRIES) return response
    const retryAfter = Number(response.headers.get('Retry-After')) || 0
    const delay = retryAfter > 0 ? retryAfter * 1000 : 1500 * (attempt + 1)
    console.warn(`[lexoffice] HTTP ${response.status} on ${path} — retrying in ${delay} ms (attempt ${attempt + 1}/${LEX_MAX_RETRIES})`)
    await sleep(delay)
  }
}

/**
 * GET /v1/posting-categories — the list of booking/income-expense categories known to
 * this Lexoffice account (what the UI calls "Kategorie" when confirming a voucher).
 */
export const getPostingCategories = async (): Promise<any[]> => {
  const response = await lexFetch('/v1/posting-categories', {
    method: 'GET',
    headers: lexAuthHeaders(false),
  })
  if (!response.ok) {
    throw new Error(`Lexoffice posting-categories error (${response.status}): ${await response.text()}`)
  }
  const data = await response.json()
  return Array.isArray(data) ? data : []
}

/**
 * POST /v1/vouchers — create a correctly-typed, already-categorized voucher.
 *
 * Deliberately NOT the same path as the plain `POST /v1/files?type=voucher` upload:
 * that generic Files endpoint auto-creates its OWN draft voucher as a side effect,
 * defaulted to `type: "purchaseinvoice"` — and a voucher's `type` can never be changed
 * afterward (Lexoffice rejects the PUT with `vouchertype_changed`), permanently
 * mis-booking a sales invoice as a purchase. Creating the voucher ourselves first
 * (no `files` field yet) with the correct `type` avoids that trap; attach the PDF
 * afterward via `uploadFileToVoucher`.
 */
export const createVoucher = async (voucher: {
  type: 'salesinvoice' | 'salescreditnote' | 'purchaseinvoice' | 'purchasecreditnote'
  voucherStatus: 'open' | 'paid' | 'paidoff' | 'voided' | 'transferred' | 'sepadebit'
  voucherNumber?: string
  voucherDate: string
  contactId: string
  totalGrossAmount: number
  totalTaxAmount: number
  taxType: 'gross' | 'net'
  remark?: string
  voucherItems: Array<{ amount: number; taxAmount: number; taxRatePercent: number; categoryId: string }>
}): Promise<{ id: string }> => {
  const response = await lexFetch('/v1/vouchers', {
    method: 'POST',
    headers: lexAuthHeaders(true),
    body: JSON.stringify(voucher),
  })
  if (!response.ok) {
    throw new Error(`Lexoffice voucher creation failed (${response.status}): ${await response.text()}`)
  }
  return response.json()
}

/** POST /v1/vouchers/{voucherId}/files — attach a file directly to an existing voucher
 *  (the voucher-scoped upload; see the note on createVoucher for why this — not the
 *  generic Files endpoint — is used for the category/tax-rate-aware upload path).
 *  Returns 202 immediately; Lexware's OCR runs asynchronously afterwards — nothing to
 *  wait for on our side. */
export const uploadFileToVoucher = async (voucherId: string, fileBuffer: Buffer, fileName: string): Promise<any> => {
  const blob = new Blob([fileBuffer], { type: 'application/pdf' })
  const formData = new FormData()
  formData.append('file', blob, fileName)
  const response = await lexFetch(`/v1/vouchers/${voucherId}/files`, {
    method: 'POST',
    headers: lexAuthHeaders(false),
    body: formData,
  })
  if (!response.ok) {
    throw new Error(`Lexoffice voucher file upload failed (${response.status}): ${await response.text()}`)
  }
  return response.json()
}
