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

// Dashboard "SameDay Versand" card: every open fulfillment order of a SameDay
// merchant (C_BPartner.isPrime = 'Y' on the partner linked to the order's
// AD_Org) shipped via DHL/DPD that must still be commissioned today.
//
// An order counts when either
//   a) it already has a (non-voided) customer shipment that is NOT commissioned, or
//   b) it has no shipment yet but open item lines that are fully fulfillable
//      from stock (same stock rule as openso-generation-queue.get.ts).
// Due orders without shipment that are NOT fulfillable (missing on-hand qty)
// are returned separately in `unfulfillable` and do not count. That list is
// limited to orders created within the last UNFULFILLABLE_MAX_AGE_DAYS so old
// stock-blocked leftovers do not pile up on the card forever.
// Every order is due at its merchant's business-day cut-off (Europe/Berlin, Mon–Fri):
// the cut-off is C_BPartner.CutOffTime of the org's partner (editable on the partner
// edit page, Fulfillment tab → Kommissionierung), DEFAULT_CUTOFF when unset. Created on
// a weekday until the cut-off → due that day, otherwise the next weekday's cut-off
// (Fri after cut-off / Sat / Sun → Monday).
// Each merchant has its own current deadline (today's cut-off on a weekday, Monday's
// on a weekend); orders created after it belong to the next day and are excluded.
// Rows whose own due time has passed are overdue (`isOverdue`). The card header
// counts down to the NEAREST upcoming deadline across the enabled merchants
// (`cutoffAt` / `cutoff`), or the latest one when all have passed; `cutoffs`
// lists every merchant's cut-off.
//
// iDempiere stores `created` as Berlin wall-clock (timestamp without tz), so the
// cutoff is built as a Berlin wall-clock value too. `isprime` / `cutofftime` are
// read through to_jsonb() so the query keeps working until the columns exist.
const DEFAULT_CUTOFF = '14:00'
const UNFULFILLABLE_MAX_AGE_DAYS = 4

const NOW_BERLIN = `(now() AT TIME ZONE 'Europe/Berlin')`

// Current deadline as Berlin wall-clock for a cut-off `timeExpr` (a time-typed SQL
// expression): today's cut-off on a weekday, the coming Monday's on Saturday
// (+2 days) / Sunday (+1 day).
const CURRENT_DEADLINE_SQL = (timeExpr: string) => `(
  date_trunc('day', ${NOW_BERLIN})
  + (CASE EXTRACT(ISODOW FROM ${NOW_BERLIN})::int WHEN 6 THEN 2 WHEN 7 THEN 1 ELSE 0 END) * interval '1 day'
  + ${timeExpr}
)`

// Due time of an order created at `col` (Berlin wall-clock): same day if created
// on a weekday until the cut-off, otherwise the next weekday's cut-off
// (Fri → +3, Sat → +2, Sun/Mon–Thu → +1 day).
const ORDER_DUE_SQL = (col: string, timeExpr: string) => `(
  CASE WHEN EXTRACT(ISODOW FROM ${col})::int <= 5 AND ${col}::time <= ${timeExpr}
       THEN date_trunc('day', ${col}) + ${timeExpr}
       ELSE date_trunc('day', ${col})
            + (CASE EXTRACT(ISODOW FROM ${col})::int WHEN 5 THEN 3 WHEN 6 THEN 2 ELSE 1 END) * interval '1 day'
            + ${timeExpr}
  END
)`

// Merchant cut-off: C_BPartner.CutOffTime (iDempiere Time column = timestamp
// '1970-01-01 HH:MM:SS') of the org's partner, else the default ($2 = 'HH:MM').
const ORG_CUTOFF_SQL = `COALESCE((to_jsonb(bp) ->> 'cutofftime')::timestamp::time, $2::time)`

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' })
  }

  // Limited roles (FrontendMenu == 'c') only see their own organization.
  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 orgIdCookie = getCookie(event, 'logship_organization_id')
  const orgIdNum = orgIdCookie ? parseInt(orgIdCookie) : NaN
  const orgFilter: number | null = (menuType === 'c' && Number.isFinite(orgIdNum)) ? orgIdNum : null

  const pool = new Pool(dbConfig)
  try {
    const sql = `
      WITH prime_orgs AS (
        SELECT org.ad_org_id, org.name,
               ${ORG_CUTOFF_SQL} AS cutoff,
               ${CURRENT_DEADLINE_SQL(ORG_CUTOFF_SQL)} AS deadline
        FROM ad_org org
        JOIN c_bpartner bp ON bp.c_bpartner_id = org.c_bpartner_id
        WHERE org.ad_client_id = 1000000
          AND to_jsonb(bp) ->> 'isprime' = 'Y'
          AND ($1::int IS NULL OR org.ad_org_id = $1)
      ),
      stock AS (
        SELECT s.m_product_id,
               l.m_warehouse_id,
               SUM(s.qtyonhand) AS qtyonhand
        FROM m_storage s
        JOIN m_locator l ON l.m_locator_id = s.m_locator_id
        LEFT JOIN m_locatortype lt ON lt.m_locatortype_id = l.m_locatortype_id
        WHERE lt.m_locatortype_id IS NULL
           OR lt.isavailableforshipping = 'Y'
           OR lt.isavailableforreplenishment = 'Y'
           OR lt.isavailableforreservation = 'Y'
        GROUP BY s.m_product_id, l.m_warehouse_id
      ),
      -- latest uncommissioned customer shipment per order
      open_ship AS (
        SELECT DISTINCT ON (io.c_order_id) io.c_order_id, io.documentno, io.m_inout_id
        FROM m_inout io
        WHERE io.ad_client_id   = 1000000
          AND io.issotrx        = 'Y'
          AND io.movementtype   = 'C-'
          AND io.docstatus NOT IN ('VO','RE')
          AND io.iscommissioned = 'N'
          AND io.ad_org_id IN (SELECT ad_org_id FROM prime_orgs)
        ORDER BY io.c_order_id, io.created DESC
      ),
      candidates AS (
        SELECT o.c_order_id, o.documentno, o.created, o.m_warehouse_id,
               o.ad_org_id, o.c_bpartner_id,
               po.name AS org_name,
               po.cutoff, po.deadline,
               sh.m_shipper_id AS shipper_id,
               sh.name AS shipper_name,
               COALESCE(bp.name, '') AS partner_name,
               os.documentno AS open_shipment_no,
               os.m_inout_id AS open_shipment_id
        FROM c_order o
        JOIN prime_orgs po ON po.ad_org_id = o.ad_org_id
        JOIN m_shipper sh ON sh.m_shipper_id = o.m_shipper_id
        LEFT JOIN c_bpartner bp ON bp.c_bpartner_id = o.c_bpartner_id
        LEFT JOIN open_ship os ON os.c_order_id = o.c_order_id
        WHERE o.issotrx = 'Y'
          AND o.docstatus IN ('CO','CL')
          AND o.ad_client_id = 1000000
          AND o.isfulfillmentorder = 'Y'
          AND o.created <= po.deadline
          AND ${ORDER_DUE_SQL('o.created', 'po.cutoff')} <= po.deadline
          AND sh.name ~* '^(DHL|DPD)'
          AND sh.name !~* 'FBA'
          AND (
                os.c_order_id IS NOT NULL
                OR (
                  -- no shipment yet, but still open item lines
                  NOT EXISTS (
                    SELECT 1 FROM m_inout io
                    WHERE io.c_order_id = o.c_order_id
                      AND io.issotrx = 'Y'
                      AND io.docstatus NOT IN ('VO','RE')
                  )
                  AND EXISTS (
                    SELECT 1
                    FROM c_orderline ol
                    JOIN m_product p ON p.m_product_id = ol.m_product_id
                    WHERE ol.c_order_id = o.c_order_id
                      AND p.producttype = 'I'
                      AND (ol.qtyordered - COALESCE(ol.qtydelivered, 0)) > 0
                  )
                )
              )
      )
      SELECT cd.c_order_id AS id,
             cd.documentno,
             to_char(cd.created, 'YYYY-MM-DD"T"HH24:MI:SS') AS created,
             to_char(${ORDER_DUE_SQL('cd.created', 'cd.cutoff')}, 'YYYY-MM-DD"T"HH24:MI:SS') AS due_at,
             ${NOW_BERLIN} > ${ORDER_DUE_SQL('cd.created', 'cd.cutoff')} AS is_overdue,
             to_char(cd.cutoff, 'HH24:MI') AS cutoff,
             to_char(cd.deadline, 'YYYY-MM-DD"T"HH24:MI:SS') AS deadline_at,
             cd.org_name,
             cd.ad_org_id,
             cd.c_bpartner_id,
             cd.m_warehouse_id,
             cd.partner_name,
             cd.shipper_id,
             cd.shipper_name,
             cd.open_shipment_no,
             cd.open_shipment_id,
             -- orders without shipment must be fully fulfillable from stock;
             -- the others are returned separately as "missing stock"
             (cd.open_shipment_no IS NOT NULL
              OR NOT EXISTS (
              SELECT 1
              FROM c_orderline ol
              JOIN m_product p ON p.m_product_id = ol.m_product_id
              LEFT JOIN stock st
                ON st.m_product_id   = ol.m_product_id
               AND st.m_warehouse_id = cd.m_warehouse_id
              WHERE ol.c_order_id = cd.c_order_id
                AND p.producttype = 'I'
                AND (ol.qtyordered - COALESCE(ol.qtydelivered, 0)) > 0
              GROUP BY ol.m_product_id
              HAVING SUM(ol.qtyordered - COALESCE(ol.qtydelivered, 0)) > COALESCE(MAX(st.qtyonhand), 0)
            )) AS is_fulfillable,
             cd.created >= ${NOW_BERLIN} - ($3::int * interval '1 day') AS is_recent
      FROM candidates cd
      ORDER BY cd.created ASC, cd.c_order_id ASC
    `
    // One row per enabled merchant: its cut-off and current deadline (+ the Berlin clock).
    const clockSql = `
      SELECT org.ad_org_id, org.name AS org_name,
             to_char(${ORG_CUTOFF_SQL}, 'HH24:MI') AS cutoff,
             (to_jsonb(bp) ->> 'cutofftime') IS NOT NULL AS is_custom,
             to_char(${CURRENT_DEADLINE_SQL(ORG_CUTOFF_SQL)}, 'YYYY-MM-DD"T"HH24:MI:SS') AS deadline_at,
             ${NOW_BERLIN} > ${CURRENT_DEADLINE_SQL(ORG_CUTOFF_SQL)} AS past_cutoff,
             to_char(${NOW_BERLIN}, 'YYYY-MM-DD"T"HH24:MI:SS') AS now_local
      FROM ad_org org
      JOIN c_bpartner bp ON bp.c_bpartner_id = org.c_bpartner_id
      WHERE org.ad_client_id = 1000000
        AND to_jsonb(bp) ->> 'isprime' = 'Y'
        AND ($1::int IS NULL OR org.ad_org_id = $1)
      ORDER BY deadline_at ASC, org.name ASC
    `

    const [result, clock] = await Promise.all([
      pool.query(sql, [orgFilter, DEFAULT_CUTOFF, UNFULFILLABLE_MAX_AGE_DAYS]),
      pool.query(clockSql, [orgFilter, DEFAULT_CUTOFF])
    ])

    const cutoffs = clock.rows.map((r: any) => ({
      orgId: Number(r.ad_org_id),
      org: r.org_name || '',
      cutoff: r.cutoff,
      isCustom: !!r.is_custom,
      deadlineAt: r.deadline_at,
      isPast: !!r.past_cutoff
    }))
    // Header target: the nearest deadline still ahead, else the latest one that passed.
    const upcoming = cutoffs.filter(c => !c.isPast)
    const target = upcoming[0] ?? cutoffs[cutoffs.length - 1] ?? null
    // Merchants sharing the targeted deadline (null when it is everyone's deadline)
    const targetOrgs = target ? cutoffs.filter(c => c.deadlineAt === target.deadlineAt).map(c => c.org) : []
    const cutoffOrg = target && targetOrgs.length < cutoffs.length ? targetOrgs.join(', ') : null
    const nowLocal = clock.rows[0]?.now_local || null

    const rows = result.rows.map((r: any) => ({
      id: Number(r.id),
      documentNo: r.documentno,
      created: r.created,
      dueAt: r.due_at,
      isOverdue: !!r.is_overdue,
      cutoff: r.cutoff,
      deadlineAt: r.deadline_at,
      orgName: r.org_name || '',
      // Ids for the card's batch actions (generate per partner + warehouse, print, picklist).
      orgId: r.ad_org_id != null ? Number(r.ad_org_id) : null,
      partnerId: r.c_bpartner_id != null ? Number(r.c_bpartner_id) : null,
      warehouseId: r.m_warehouse_id != null ? Number(r.m_warehouse_id) : null,
      partnerName: r.partner_name || '',
      shipperId: r.shipper_id != null ? Number(r.shipper_id) : null,
      shipperName: r.shipper_name || '',
      shipmentNo: r.open_shipment_no || null,
      shipmentId: r.open_shipment_id != null ? Number(r.open_shipment_id) : null,
      isFulfillable: !!r.is_fulfillable,
      isRecent: !!r.is_recent
    }))
    const records = rows.filter(r => r.isFulfillable)
    // Due today but blocked by missing on-hand stock (no shipment yet) –
    // only orders created within the last UNFULFILLABLE_MAX_AGE_DAYS.
    const unfulfillable = rows.filter(r => !r.isFulfillable && r.isRecent)

    const byOrgMap: Record<string, { count: number; cutoff: string | null }> = {}
    records.forEach(r => {
      const e = byOrgMap[r.orgName] ?? (byOrgMap[r.orgName] = { count: 0, cutoff: r.cutoff ?? null })
      e.count++
    })
    const byOrg = Object.entries(byOrgMap)
      .map(([org, v]) => ({ org, count: v.count, cutoff: v.cutoff }))
      .sort((a, b) => b.count - a.count)

    return {
      defaultCutoff: DEFAULT_CUTOFF,
      cutoff: target?.cutoff ?? DEFAULT_CUTOFF,
      cutoffOrg,
      cutoffs,
      // true when the enabled merchants do not all share one cut-off
      hasMixedCutoffs: new Set(cutoffs.map(c => c.cutoff)).size > 1,
      nowLocal,
      cutoffAt: target?.deadlineAt ?? null,
      isPastCutoff: target ? target.isPast : false,
      count: records.length,
      withShipment: records.filter(r => r.shipmentNo).length,
      overdueCount: records.filter(r => r.isOverdue).length,
      byOrg,
      records,
      unfulfillable
    }
  } catch (err: any) {
    console.error('PostgreSQL query error:', 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 prime-due-today:', error)
      throw error
    }
  }
})
