import { Pool } from 'pg'
import refreshTokenHelper from '../../../utils/refreshTokenHelper'

/**
 * OPOS-Liste (Offene-Posten-Liste) — DATEV-style open items list.
 *
 * One row per open item (offener Posten): every completed invoice / credit memo
 * whose open amount is not zero, optionally plus unallocated payments
 * ("Zahlungen ohne Rechnungsbezug"). Direct Postgres like the other
 * `openso-*` / `open-fulfillment-invoices` routes, because the open amount is
 * computed by iDempiere's own SQL functions (`invoiceOpen`, `invoiceOpenToDate`,
 * `paymentTermDueDate`, `paymentAvailable`) which the REST API can't reach
 * (the RV_OpenItem view has no PK and is rejected by the REST layer).
 *
 * Query params:
 *   type            'ar' (Debitoren) | 'ap' (Kreditoren) | 'all'   (default 'all')
 *   orgId           restrict to one organisation (default: all orgs; limited roles are forced to their own org)
 *   asOf            OP-Stichtag YYYY-MM-DD — the list as it looked on that date
 *                   (invoices booked up to that date, allocations up to that date). Default: today.
 *   includePayments 'true' to add unallocated payments as (negative) open items.
 *
 * Sign convention (document perspective, like iDempiere's invoiceOpen):
 *   invoice  → positive open amount, credit memo / unallocated payment → negative.
 *   `sh` carries the DATEV Soll/Haben marker: Debitor invoice = S, Debitor credit/payment = H,
 *   Kreditor invoice = H, Kreditor credit/payment = S.
 *
 * AR/AP split follows the app's own convention (see server/api/invoices/so.get.ts):
 * `IsSOTrx='Y' OR C_DocTypeTarget_ID = 1000006` is AR — doctype 1000006 ("AP CreditMemo")
 * is used for customer credit notes and is economically a Debitoren credit.
 */

const CLIENT_ID = 1000000
const AR_SPECIAL_DOCTYPE = 1000006

const toIsoDate = (v: any): string | null => {
  if (!v) return null
  if (v instanceof Date) {
    return `${v.getFullYear()}-${String(v.getMonth() + 1).padStart(2, '0')}-${String(v.getDate()).padStart(2, '0')}`
  }
  const m = String(v).match(/^(\d{4})-(\d{2})-(\d{2})/)
  return m ? `${m[1]}-${m[2]}-${m[3]}` : null
}

const num = (v: any): number => {
  const n = typeof v === 'number' ? v : parseFloat(v)
  return Number.isFinite(n) ? n : 0
}

const handleFunc = async (event: any) => {
  const config = useRuntimeConfig()
  const dbConfig = {
    host: config.pgHost,
    port: parseInt(config.pgPort || '5432'),
    database: config.pgDatabase,
    user: config.pgUser,
    password: config.pgPassword,
  }

  if (!dbConfig.host || !dbConfig.database || !dbConfig.user || !dbConfig.password) {
    throw createError({
      statusCode: 500,
      message: 'PostgreSQL credentials not configured. Please set PG_HOST, PG_DATABASE, PG_USER, PG_PASSWORD in .env'
    })
  }

  const query = getQuery(event)
  const type = String(query?.type || 'all').toLowerCase()
  const includePayments = String(query?.includePayments || '') === 'true'
  const asOfParam = toIsoDate(query?.asOf)
  const today = toIsoDate(new Date()) as string
  // A Stichtag in the future is meaningless — clamp to today (= "current" mode).
  const asOf: string | null = asOfParam && asOfParam < today ? asOfParam : null
  const referenceDate = asOf || today

  // Limited roles (FrontendMenu == 'c') only ever see their own organisation.
  const roleCookie = getCookie(event, 'logship_role')
  let menuType: string = 'Menu'
  try {
    const role = roleCookie ? JSON.parse(roleCookie) : null
    menuType = role?.FrontendMenu?.id || role?.FrontendMenu || 'Menu'
  } catch {}
  const ownOrgCookie = getCookie(event, 'logship_organization_id')
  const ownOrgId = ownOrgCookie ? parseInt(ownOrgCookie) : NaN
  const requestedOrg = query?.orgId != null && String(query.orgId) !== '' ? parseInt(String(query.orgId)) : NaN
  let orgFilter: number | null = Number.isFinite(requestedOrg) ? requestedOrg : null
  if (menuType === 'c') orgFilter = Number.isFinite(ownOrgId) ? ownOrgId : -1

  const sideFilter = type === 'ar' ? 'AR' : type === 'ap' ? 'AP' : null

  const pool = new Pool(dbConfig)

  try {
    // ---- Invoices / credit memos ------------------------------------------------
    // $1 org (int|null), $2 asOf (date|null), $3 side ('AR'|'AP'|null), $4 reference date
    const invoiceSql = `
      WITH inv AS (
        SELECT
          i.c_invoice_id,
          i.documentno,
          i.dateinvoiced::date              AS dateinvoiced,
          i.dateacct::date                  AS dateacct,
          i.issotrx,
          i.docstatus,
          i.ispaid,
          i.isindispute,
          i.poreference,
          i.description,
          i.c_order_id,
          i.grandtotal,
          i.c_currency_id,
          i.c_bpartner_id,
          i.ad_org_id,
          i.c_paymentterm_id,
          i.c_payment_id,
          dt.docbasetype,
          dt.name                           AS doctype_name,
          CASE WHEN dt.docbasetype IN ('ARC','APC') THEN -1 ELSE 1 END AS multiplier,
          CASE WHEN i.issotrx = 'Y' OR i.c_doctypetarget_id = ${AR_SPECIAL_DOCTYPE} THEN 'AR' ELSE 'AP' END AS side,
          paymenttermduedate(i.c_paymentterm_id, i.dateinvoiced)::date AS duedate,
          CASE WHEN $2::date IS NULL
               THEN invoiceopen(i.c_invoice_id, 0)
               ELSE invoiceopentodate(i.c_invoice_id, 0, $2::date)
          END                               AS openamt_alloc
        FROM c_invoice i
        JOIN c_doctype dt ON dt.c_doctype_id = COALESCE(NULLIF(i.c_doctype_id, 0), i.c_doctypetarget_id)
        WHERE i.ad_client_id = ${CLIENT_ID}
          AND i.isactive  = 'Y'
          AND i.docstatus = 'CO'
          AND ($1::int  IS NULL OR i.ad_org_id = $1)
          AND ($2::date IS NULL OR i.dateacct::date <= $2::date)
          AND (
            i.ispaid = 'N'
            OR ($2::date IS NOT NULL AND EXISTS (
                 SELECT 1
                 FROM c_allocationline al
                 JOIN c_allocationhdr ah ON ah.c_allocationhdr_id = al.c_allocationhdr_id
                 WHERE al.c_invoice_id = i.c_invoice_id
                   AND ah.isactive = 'Y'
                   AND ah.dateacct::date > $2::date
            ))
          )
      )
      SELECT
        inv.*,
        bp.value                            AS partner_value,
        bp.name                             AS partner_name,
        bp.iscustomer,
        bp.isvendor,
        org.name                            AS org_name,
        cur.iso_code                        AS currency,
        pt.name                             AS paymentterm,
        COALESCE(pt.netdays, 0)             AS netdays,
        COALESCE(pt.discountdays, 0)        AS discountdays,
        COALESCE(pt.discount, 0)            AS discount,
        o.documentno                        AS order_documentno,
        al.n                                AS alloc_count,
        al.last_date                        AS last_payment_date,
        COALESCE(al.discount_sum, 0)        AS alloc_discount,
        COALESCE(al.writeoff_sum, 0)        AS alloc_writeoff,
        lp.payamt                           AS linked_payamt,
        lp.documentno                       AS linked_payment_documentno,
        lp.datetrx                          AS linked_payment_date,
        COALESCE(dun.n, 0)                  AS dunning_count,
        dun.last_date                       AS last_dunning_date,
        dun.level_name                      AS dunning_level,
        ($4::date - inv.duedate)            AS days_due
      FROM inv
      JOIN c_bpartner bp     ON bp.c_bpartner_id   = inv.c_bpartner_id
      LEFT JOIN ad_org org   ON org.ad_org_id      = inv.ad_org_id
      LEFT JOIN c_currency cur ON cur.c_currency_id = inv.c_currency_id
      LEFT JOIN c_paymentterm pt ON pt.c_paymentterm_id = inv.c_paymentterm_id
      LEFT JOIN c_order o    ON o.c_order_id       = inv.c_order_id
      LEFT JOIN LATERAL (
        SELECT COUNT(*)                                        AS n,
               MAX(ah.dateacct)::date                          AS last_date,
               SUM(al.discountamt)                             AS discount_sum,
               SUM(al.writeoffamt)                             AS writeoff_sum
        FROM c_allocationline al
        JOIN c_allocationhdr ah ON ah.c_allocationhdr_id = al.c_allocationhdr_id
        WHERE al.c_invoice_id = inv.c_invoice_id
          AND ah.isactive = 'Y'
          AND ($2::date IS NULL OR ah.dateacct::date <= $2::date)
      ) al ON TRUE
      LEFT JOIN LATERAL (
        -- App convention: an invoice may carry a direct C_Payment_ID link without an
        -- allocation (see /sales/invoices "TotalPaidAmt || C_Payment_ID.PayAmt").
        SELECT p.payamt, p.documentno, p.datetrx::date AS datetrx
        FROM c_payment p
        WHERE p.c_payment_id = inv.c_payment_id
          AND p.docstatus IN ('CO','CL')
          AND ($2::date IS NULL OR p.dateacct::date <= $2::date)
      ) lp ON TRUE
      LEFT JOIN LATERAL (
        SELECT COUNT(*)                 AS n,
               MAX(dr.dunningdate)::date AS last_date,
               (ARRAY_AGG(dl.name ORDER BY dr.dunningdate DESC))[1] AS level_name
        FROM c_dunningrunline drl
        JOIN c_dunningrunentry dre ON dre.c_dunningrunentry_id = drl.c_dunningrunentry_id
        JOIN c_dunningrun dr       ON dr.c_dunningrun_id       = dre.c_dunningrun_id
        LEFT JOIN c_dunninglevel dl ON dl.c_dunninglevel_id     = dr.c_dunninglevel_id
        WHERE drl.c_invoice_id = inv.c_invoice_id
          AND drl.processed = 'Y'
          AND ($2::date IS NULL OR dr.dunningdate::date <= $2::date)
      ) dun ON TRUE
      WHERE ($3::text IS NULL OR inv.side = $3::text)
      ORDER BY inv.side, bp.value, bp.name, inv.duedate, inv.dateinvoiced, inv.c_invoice_id
    `

    const params = [orgFilter, asOf, sideFilter, referenceDate]
    const invRes = await pool.query(invoiceSql, params)

    const records: any[] = []
    for (const r of invRes.rows) {
      const multiplier = num(r.multiplier) || 1
      const grandTotal = num(r.grandtotal)
      const amount = grandTotal * multiplier                   // signed document amount
      let openAmt = num(r.openamt_alloc)
      let paidVia: 'allocation' | 'link' | null = num(r.alloc_count) > 0 ? 'allocation' : null
      // Direct-link fallback only when nothing is allocated at all (same rule as /sales/invoices).
      if (num(r.alloc_count) === 0 && r.linked_payamt != null && multiplier === 1) {
        openAmt = openAmt - num(r.linked_payamt)
        paidVia = 'link'
      }
      openAmt = Math.round(openAmt * 100) / 100
      if (Math.abs(openAmt) < 0.005) continue
      const paidAmt = Math.round((amount - openAmt) * 100) / 100
      // Allocations that net to zero (allocation + its reversal) are not a payment.
      if (paidVia === 'allocation' && paidAmt === 0) paidVia = null

      const side: 'AR' | 'AP' = r.side === 'AP' ? 'AP' : 'AR'
      const isCredit = multiplier < 0
      const discountPct = num(r.discount)
      const discountDays = num(r.discountdays)
      const dateInvoiced = toIsoDate(r.dateinvoiced)
      let discountDate: string | null = null
      if (discountPct > 0 && dateInvoiced) {
        const d = new Date(dateInvoiced + 'T00:00:00')
        d.setDate(d.getDate() + discountDays)
        discountDate = toIsoDate(d)
      }
      const daysDue = r.days_due == null ? 0 : parseInt(String(r.days_due))

      records.push({
        kind: 'invoice',
        id: Number(r.c_invoice_id),
        documentNo: r.documentno,
        side,
        docBaseType: r.docbasetype || '',
        docTypeName: r.doctype_name || '',
        isCredit,
        // DATEV Soll/Haben marker, from the sign of the open amount:
        // Debitor: positive = S (Forderung), negative = H; Kreditor: positive = H (Verbindlichkeit), negative = S.
        sh: (side === 'AR') === (openAmt > 0) ? 'S' : 'H',
        dateDoc: dateInvoiced,
        dateAcct: toIsoDate(r.dateacct),
        dueDate: toIsoDate(r.duedate),
        daysDue,
        isOverdue: daysDue > 0,
        partnerId: Number(r.c_bpartner_id) || 0,
        partnerValue: r.partner_value || '',
        partnerName: r.partner_name || '',
        isCustomer: r.iscustomer === 'Y',
        isVendor: r.isvendor === 'Y',
        orgId: Number(r.ad_org_id) || 0,
        orgName: r.org_name || '',
        currency: r.currency || 'EUR',
        amount: Math.round(amount * 100) / 100,
        paidAmt,
        allocDiscount: num(r.alloc_discount),
        allocWriteOff: num(r.alloc_writeoff),
        openAmt,
        paidVia,
        paymentTerm: r.paymentterm || '',
        netDays: num(r.netdays),
        discountDays,
        discountPct,
        discountDate,
        discountAmt: discountPct > 0 ? Math.round(grandTotal * discountPct) / 100 : 0,
        discountStillPossible: !!(discountDate && discountDate >= referenceDate),
        dunningCount: num(r.dunning_count),
        dunningLevel: r.dunning_level || '',
        lastDunningDate: toIsoDate(r.last_dunning_date),
        lastPaymentDate: toIsoDate(r.last_payment_date) || (paidVia === 'link' ? toIsoDate(r.linked_payment_date) : null),
        linkedPaymentDocumentNo: paidVia === 'link' ? (r.linked_payment_documentno || '') : '',
        description: r.description || '',
        poReference: r.poreference || '',
        orderId: r.c_order_id ? Number(r.c_order_id) : null,
        orderDocumentNo: r.order_documentno || '',
        isInDispute: r.isindispute === 'Y',
        isPaidFlag: r.ispaid === 'Y'
      })
    }

    // ---- Unallocated payments (Zahlungen ohne Rechnungsbezug) --------------------
    if (includePayments) {
      const paymentSql = `
        WITH pay AS (
          SELECT
            p.c_payment_id, p.documentno, p.datetrx::date AS datetrx, p.dateacct::date AS dateacct,
            p.payamt, p.isreceipt, p.c_bpartner_id, p.c_currency_id, p.ad_org_id, p.description,
            p.c_invoice_id, p.c_order_id, p.tendertype, p.checkno,
            CASE WHEN p.isreceipt = 'Y' THEN 'AR' ELSE 'AP' END AS side,
            CASE WHEN $2::date IS NULL
                 THEN paymentavailable(p.c_payment_id)
                 ELSE p.payamt - COALESCE((
                        SELECT SUM(al.amount) * (CASE WHEN p.isreceipt = 'Y' THEN 1 ELSE -1 END)
                        FROM c_allocationline al
                        JOIN c_allocationhdr ah ON ah.c_allocationhdr_id = al.c_allocationhdr_id
                        WHERE al.c_payment_id = p.c_payment_id
                          AND ah.isactive = 'Y'
                          AND ah.dateacct::date <= $2::date
                      ), 0)
            END AS available
          FROM c_payment p
          WHERE p.ad_client_id = ${CLIENT_ID}
            AND p.isactive  = 'Y'
            AND p.docstatus IN ('CO','CL')
            AND p.c_charge_id IS NULL
            AND ($1::int  IS NULL OR p.ad_org_id = $1)
            AND ($2::date IS NULL OR p.dateacct::date <= $2::date)
            AND (
              p.isallocated = 'N'
              OR ($2::date IS NOT NULL AND EXISTS (
                   SELECT 1
                   FROM c_allocationline al
                   JOIN c_allocationhdr ah ON ah.c_allocationhdr_id = al.c_allocationhdr_id
                   WHERE al.c_payment_id = p.c_payment_id
                     AND ah.isactive = 'Y'
                     AND ah.dateacct::date > $2::date
              ))
            )
            -- payments "used" through the direct invoice link are not open
            AND NOT EXISTS (
              SELECT 1 FROM c_invoice x
              WHERE x.c_payment_id = p.c_payment_id AND x.docstatus = 'CO' AND x.isactive = 'Y'
            )
        )
        SELECT pay.*,
               bp.value AS partner_value, bp.name AS partner_name, bp.iscustomer, bp.isvendor,
               org.name AS org_name, cur.iso_code AS currency,
               inv.documentno AS invoice_documentno,
               o.documentno AS order_documentno
        FROM pay
        JOIN c_bpartner bp      ON bp.c_bpartner_id = pay.c_bpartner_id
        LEFT JOIN ad_org org    ON org.ad_org_id = pay.ad_org_id
        LEFT JOIN c_currency cur ON cur.c_currency_id = pay.c_currency_id
        LEFT JOIN c_invoice inv ON inv.c_invoice_id = pay.c_invoice_id
        LEFT JOIN c_order o     ON o.c_order_id = pay.c_order_id
        WHERE ($3::text IS NULL OR pay.side = $3::text)
        ORDER BY pay.side, bp.value, bp.name, pay.datetrx, pay.c_payment_id
      `
      const payRes = await pool.query(paymentSql, [orgFilter, asOf, sideFilter])
      for (const r of payRes.rows) {
        const available = Math.round(num(r.available) * 100) / 100
        if (Math.abs(available) < 0.005) continue
        const side: 'AR' | 'AP' = r.side === 'AP' ? 'AP' : 'AR'
        const dateTrx = toIsoDate(r.datetrx)
        const daysDue = dateTrx ? Math.round((new Date(referenceDate + 'T00:00:00').getTime() - new Date(dateTrx + 'T00:00:00').getTime()) / 86400000) : 0
        const openAmt = -available   // a received payment is a credit (H) on the debtor account
        records.push({
          kind: 'payment',
          id: Number(r.c_payment_id),
          documentNo: r.documentno,
          side,
          docBaseType: side === 'AR' ? 'ARR' : 'APP',
          docTypeName: side === 'AR' ? 'Zahlungseingang' : 'Zahlungsausgang',
          isCredit: true,
          sh: (side === 'AR') === (openAmt > 0) ? 'S' : 'H',
          dateDoc: dateTrx,
          dateAcct: toIsoDate(r.dateacct),
          dueDate: dateTrx,
          daysDue,
          isOverdue: false,
          partnerId: Number(r.c_bpartner_id) || 0,
          partnerValue: r.partner_value || '',
          partnerName: r.partner_name || '',
          isCustomer: r.iscustomer === 'Y',
          isVendor: r.isvendor === 'Y',
          orgId: Number(r.ad_org_id) || 0,
          orgName: r.org_name || '',
          currency: r.currency || 'EUR',
          amount: -Math.round(num(r.payamt) * 100) / 100,
          paidAmt: -Math.round((num(r.payamt) - available) * 100) / 100,
          allocDiscount: 0,
          allocWriteOff: 0,
          openAmt,
          paidVia: null,
          paymentTerm: '',
          netDays: 0,
          discountDays: 0,
          discountPct: 0,
          discountDate: null,
          discountAmt: 0,
          discountStillPossible: false,
          dunningCount: 0,
          dunningLevel: '',
          lastDunningDate: null,
          lastPaymentDate: dateTrx,
          linkedPaymentDocumentNo: '',
          description: r.description || '',
          poReference: r.checkno || '',
          orderId: r.c_order_id ? Number(r.c_order_id) : null,
          orderDocumentNo: r.order_documentno || '',
          invoiceId: r.c_invoice_id ? Number(r.c_invoice_id) : null,
          invoiceDocumentNo: r.invoice_documentno || '',
          isInDispute: false,
          isPaidFlag: false
        })
      }
    }

    const sum = (rows: any[], f: (r: any) => number) => Math.round(rows.reduce((s, r) => s + f(r), 0) * 100) / 100
    const ar = records.filter(r => r.side === 'AR')
    const ap = records.filter(r => r.side === 'AP')

    return {
      records,
      count: records.length,
      asOf: referenceDate,
      isHistorical: !!asOf,
      orgId: orgFilter,
      type,
      includePayments,
      summary: {
        arOpen: sum(ar, r => r.openAmt),
        arOverdue: sum(ar.filter(r => r.kind === 'invoice' && r.daysDue > 0 && r.openAmt > 0), r => r.openAmt),
        arCount: ar.length,
        apOpen: sum(ap, r => r.openAmt),
        apOverdue: sum(ap.filter(r => r.kind === 'invoice' && r.daysDue > 0 && r.openAmt > 0), r => r.openAmt),
        apCount: ap.length
      }
    }
  } catch (err: any) {
    console.error('PostgreSQL query error (accounting/opos):', err)
    throw createError({ statusCode: 500, message: `Database error: ${err.message}` })
  } finally {
    await pool.end()
  }
}

export default defineEventHandler(async (event) => {
  try {
    return await handleFunc(event)
  } catch (err: any) {
    try {
      await refreshTokenHelper(event)
      return await handleFunc(event)
    } catch (error: any) {
      console.error('Fatal error in accounting/opos:', error)
      throw error
    }
  }
})
