/**
 * iDempiere-backed store for the Incoming-Invoices inbox + per-vendor learning,
 * via the REST API (fetchHelper) against two custom tables:
 *   - CUST_IncomingInvoice      (staging: status + extraction JSON; PDF in Strapi)
 *   - CUST_VendorInvoiceProfile (learning: fingerprint, defaults, field hints)
 * Engine config is kept in AD_Preference. NO SQLite, NO direct pg — everything is
 * Postgres-backed through iDempiere's REST layer.
 *
 * All helpers take a ctx = { event, token, scope } (REST needs the auth token).
 * Fail-soft: callers catch errors so a missing table only disables this feature.
 * Write casing follows the iDempiere rule (lowercase-first scalars, PascalCase FK
 * objects with { id, tableName }); reads accept PascalCase responses.
 */
import { string } from 'alga-js'
import fetchHelper from '../fetchHelper'

export interface InboxScope { clientId: number; orgId: number }
export interface InboxCtx { event: any; token: string; scope: InboxScope }

const DOC_MODEL = 'cust_incominginvoice'
const DOC_TABLE = 'CUST_IncomingInvoice'
const PROFILE_MODEL = 'cust_vendorinvoiceprofile'
const PROFILE_TABLE = 'CUST_VendorInvoiceProfile'

const safeParse = (s: any) => { if (s == null) return null; if (typeof s === 'object') return s; try { return JSON.parse(s) } catch { return null } }
const fkId = (v: any) => (v && typeof v === 'object') ? (v.id ?? null) : (v ?? null)

const req = (ctx: InboxCtx, path: string, method: string, body: any = null) =>
  fetchHelper(ctx.event, path, method, ctx.token, body)

/* ---------------------------- mapping ---------------------------- */

const mapDoc = (r: any) => r ? ({
  id: r.id,
  status: r.InboxStatus ?? r.inboxStatus ?? 'new',
  filename: r.FileName ?? r.fileName ?? '',
  mimeType: r.MimeType ?? r.mimeType ?? 'application/pdf',
  byteSize: r.ByteSize ?? null,
  sourceTier: r.SourceTier ?? null,
  vendorFingerprint: r.VendorFingerprint ?? r.vendorFingerprint ?? '',
  extracted: safeParse(r.ExtractedJson),
  confidence: safeParse(r.ConfidenceJson),
  bbox: safeParse(r.BboxJson),
  cInvoiceId: fkId(r.C_Invoice_ID),
  strapiFileUrl: r.StrapiFileUrl ?? '',
  strapiAttachmentId: r.StrapiAttachmentId ?? null,
  error: r.ErrorMsg ?? '',
  uid: r[`${DOC_TABLE}_UU`] ?? r.UID ?? '',
  created: r.Created ?? null,
  updated: r.Updated ?? null
}) : null

const mapProfile = (r: any) => r ? ({
  id: r.id,
  c_bpartner_id: fkId(r.C_BPartner_ID),
  vendor_name: r.VendorName ?? null,
  vat_id: r.VatId ?? null,
  iban: r.IBAN ?? null,
  currency_id: fkId(r.C_Currency_ID),
  default_charge_id: fkId(r.C_Charge_ID),
  default_tax_id: fkId(r.C_Tax_ID),
  default_doctype_id: fkId(r.C_DocType_ID),
  is_tax_included: (r.IsTaxIncluded === true || r.IsTaxIncluded === 'Y'),
  field_hints: safeParse(r.FieldHints) || {},
  samples_count: Number(r.SamplesCount) || 0
}) : null

/* ------------------------ inbox documents ------------------------ */

export const createInboxDocument = async (ctx: InboxCtx, params: {
  filename: string; mimeType: string; byteSize?: number
}): Promise<number> => {
  // The uploader is captured by iDempiere's standard CreatedBy (the record is
  // created with the uploader's token) — no separate UploadedBy_ID column.
  // Only InboxStatus / FileName / MimeType are set at create; the "filled-in-later"
  // columns (VendorFingerprint, ExtractedJson, …) must be NON-mandatory in the
  // dictionary, since they're populated by setInboxParsed after OCR.
  const res: any = await req(ctx, `models/${DOC_MODEL}`, 'POST', {
    AD_Org_ID: { id: ctx.scope.orgId, tableName: 'AD_Org' },
    isActive: true,
    inboxStatus: 'processing',
    fileName: params.filename,
    mimeType: params.mimeType,
    ...(params.byteSize != null ? { byteSize: params.byteSize } : {}),
    tableName: DOC_TABLE
  })
  return Number(res?.id)
}

export const setInboxStrapiFile = async (ctx: InboxCtx, id: number, fileUrl: string, attachmentId?: number | null) => {
  await req(ctx, `models/${DOC_MODEL}/${id}`, 'PUT', {
    strapiFileUrl: fileUrl || '',
    ...(attachmentId != null ? { strapiAttachmentId: attachmentId } : {})
  })
}

export const setInboxParsed = async (ctx: InboxCtx, id: number, result: {
  sourceTier: number; vendorFingerprint?: string | null; extracted: any; confidence: any; bbox: any
}) => {
  await req(ctx, `models/${DOC_MODEL}/${id}`, 'PUT', {
    sourceTier: result.sourceTier,
    vendorFingerprint: result.vendorFingerprint || '',
    extractedJson: JSON.stringify(result.extracted ?? {}),
    confidenceJson: JSON.stringify(result.confidence ?? {}),
    bboxJson: JSON.stringify(result.bbox ?? {}),
    inboxStatus: 'parsed',
    errorMsg: ''
  })
}

export const setInboxError = async (ctx: InboxCtx, id: number, error: string) => {
  await req(ctx, `models/${DOC_MODEL}/${id}`, 'PUT', { inboxStatus: 'error', errorMsg: String(error).slice(0, 500) })
}

export const setInboxProcessing = async (ctx: InboxCtx, id: number) => {
  await req(ctx, `models/${DOC_MODEL}/${id}`, 'PUT', { inboxStatus: 'processing', errorMsg: '' })
}

export const markInboxLinked = async (ctx: InboxCtx, id: number, cInvoiceId: number, strapiAttachmentId?: number | null) => {
  await req(ctx, `models/${DOC_MODEL}/${id}`, 'PUT', {
    inboxStatus: 'linked',
    C_Invoice_ID: { id: cInvoiceId, tableName: 'C_Invoice' },
    ...(strapiAttachmentId != null ? { strapiAttachmentId } : {})
  })
}

export const rejectInboxDocument = async (ctx: InboxCtx, id: number) => {
  await req(ctx, `models/${DOC_MODEL}/${id}`, 'PUT', { inboxStatus: 'rejected', isActive: false })
}

export const getInboxDocument = async (ctx: InboxCtx, id: number) => {
  const res: any = await req(ctx, `models/${DOC_MODEL}/${id}`, 'GET', null)
  if (!res?.id) return null
  // Org guard (REST already client-scopes via token; enforce org here).
  if (fkId(res.AD_Org_ID) && Number(fkId(res.AD_Org_ID)) !== Number(ctx.scope.orgId)) return null
  return mapDoc(res)
}

export const listInboxDocuments = async (ctx: InboxCtx, opts: { status?: string; limit?: number } = {}) => {
  const limit = Math.min(Math.max(opts.limit ?? 200, 1), 1000)
  let filter = `AD_Org_ID eq ${ctx.scope.orgId}`
  if (opts.status) filter += ` AND InboxStatus eq '${opts.status}'`
  const res: any = await req(
    ctx,
    `models/${DOC_MODEL}?$filter=${string.urlEncode(filter)}&$orderby=${string.urlEncode('Created desc')}&$top=${limit}`,
    'GET', null
  )
  return (res?.records || []).map(mapDoc)
}

/* ------------------------ vendor profiles ------------------------ */

export const getVendorProfile = async (ctx: InboxCtx, fingerprint: string) => {
  if (!fingerprint) return null
  const filter = `AD_Org_ID eq ${ctx.scope.orgId} AND Fingerprint eq '${fingerprint.replace(/'/g, '')}' AND IsActive eq true`
  const res: any = await req(ctx, `models/${PROFILE_MODEL}?$filter=${string.urlEncode(filter)}`, 'GET', null)
  return mapProfile(res?.records?.[0])
}

/** All active vendor profiles for the org (for fuzzy layout matching). */
export const listVendorProfiles = async (ctx: InboxCtx) => {
  const filter = `AD_Org_ID eq ${ctx.scope.orgId} AND IsActive eq true`
  const res: any = await req(ctx, `models/${PROFILE_MODEL}?$filter=${string.urlEncode(filter)}&$top=500`, 'GET', null)
  return (res?.records || []).map(mapProfile)
}

export const upsertVendorProfile = async (ctx: InboxCtx, fingerprint: string, patch: {
  cBpartnerId?: number | null; vendorName?: string | null; vatId?: string | null; iban?: string | null
  currencyId?: number | null; defaultChargeId?: number | null; defaultTaxId?: number | null
  defaultDoctypeId?: number | null; isTaxIncluded?: boolean; fieldHints?: any
}) => {
  if (!fingerprint) return
  const existing = await getVendorProfile(ctx, fingerprint)
  const body: any = { isActive: true }
  if (patch.vendorName != null) body.vendorName = patch.vendorName
  if (patch.vatId != null) body.vatId = patch.vatId
  if (patch.iban != null) body.iBAN = patch.iban
  if (patch.isTaxIncluded != null) body.isTaxIncluded = !!patch.isTaxIncluded
  if (patch.fieldHints != null) body.fieldHints = JSON.stringify(patch.fieldHints)
  if (patch.cBpartnerId) body.C_BPartner_ID = { id: patch.cBpartnerId, tableName: 'C_BPartner' }
  if (patch.currencyId) body.C_Currency_ID = { id: patch.currencyId, tableName: 'C_Currency' }
  if (patch.defaultChargeId) body.C_Charge_ID = { id: patch.defaultChargeId, tableName: 'C_Charge' }
  if (patch.defaultTaxId) body.C_Tax_ID = { id: patch.defaultTaxId, tableName: 'C_Tax' }
  if (patch.defaultDoctypeId) body.C_DocType_ID = { id: patch.defaultDoctypeId, tableName: 'C_DocType' }

  if (existing) {
    body.samplesCount = (existing.samples_count || 0) + 1
    await req(ctx, `models/${PROFILE_MODEL}/${existing.id}`, 'PUT', body)
  } else {
    body.AD_Org_ID = { id: ctx.scope.orgId, tableName: 'AD_Org' }
    body.fingerprint = fingerprint
    body.samplesCount = 1
    body.tableName = PROFILE_TABLE
    await req(ctx, `models/${PROFILE_MODEL}`, 'POST', body)
  }
}

/* ------------------------ engine config (AD_Preference) ---------- */

const prefAttribute = (key: string) => `ap_ocr_${key}`

export const getInboxConfig = async (ctx: InboxCtx, key: string): Promise<any> => {
  const filter = `Attribute eq '${prefAttribute(key)}' AND AD_Org_ID eq ${ctx.scope.orgId}`
  const res: any = await req(ctx, `models/ad_preference?$filter=${string.urlEncode(filter)}`, 'GET', null)
  return safeParse(res?.records?.[0]?.Value)
}

export const setInboxConfig = async (ctx: InboxCtx, key: string, value: any) => {
  const attr = prefAttribute(key)
  const filter = `Attribute eq '${attr}' AND AD_Org_ID eq ${ctx.scope.orgId}`
  const found: any = await req(ctx, `models/ad_preference?$filter=${string.urlEncode(filter)}`, 'GET', null)
  const existingId = found?.records?.[0]?.id
  const payload: any = { attribute: attr, value: JSON.stringify(value ?? null), isActive: true }
  if (existingId) {
    await req(ctx, `models/ad_preference/${existingId}`, 'PUT', payload)
  } else {
    await req(ctx, 'models/ad_preference', 'POST', {
      ...payload, AD_Org_ID: { id: ctx.scope.orgId, tableName: 'AD_Org' }, tableName: 'AD_Preference'
    })
  }
}
