-- Backfill payment_allocations untuk data historis via FIFO simulation.
-- Seluruh migration ini berjalan dalam 1 transaction (golang-migrate default).
-- Jika ada party yang gagal di tengah loop, seluruh backfill ROLLBACK otomatis.

-- Fungsi inti: FIFO greedy untuk 1 party (supplier/customer).
-- Algoritma:
--   1. Hapus payment_allocations existing untuk party tsb (idempotent).
--   2. Kumpulkan eligible children sort by date ASC ke array (kind, id, amount, remaining).
--   3. Konsumsi creditCarry (SUM payment SALDO_AWAL) ke children tertua — TIDAK insert allocation row.
--   4. Loop payments (selain SALDO_AWAL) ORDER BY payment_date ASC: greedy alokasi ke child tertua dengan remaining > 0.
--   5. Sisa nominal payment tidak insert row (otomatis credit balance untuk dokumen baru).
CREATE OR REPLACE FUNCTION fn_fifo_backfill_party(
    p_party_type TEXT,
    p_party_id BIGINT
) RETURNS VOID AS $func$
DECLARE
    v_party_type TEXT := UPPER(p_party_type);
    v_payment RECORD;
    v_child RECORD;
    v_remaining NUMERIC(15, 3);
    v_used NUMERIC(15, 3);
    v_eps CONSTANT NUMERIC(15, 3) := 0.001;
BEGIN
    -- Acquire advisory lock untuk anti-race (1-arg form: hashtext returns int4, cast ke bigint)
    PERFORM pg_advisory_xact_lock(hashtext('payment_alloc:' || v_party_type || ':' || p_party_id::text)::bigint);

    -- Hapus allocations existing untuk party tsb (idempotent ulang-jalan)
    DELETE FROM payment_allocations pa
    USING payments p
    WHERE pa.payment_id = p.id
      AND p.party_type = v_party_type
      AND p.party_id = p_party_id;

    -- TEMP table untuk antrian children (sort sudah ada di INSERT...SELECT ORDER BY)
    CREATE TEMP TABLE IF NOT EXISTS _children_queue (
        seq BIGSERIAL PRIMARY KEY,
        kind TEXT NOT NULL,           -- 'PURCHASE_ITEM' / 'MDP' / 'EXPENSE_REALIZATION'
        child_id BIGINT NOT NULL,
        amount NUMERIC(15, 3) NOT NULL,
        remaining NUMERIC(15, 3) NOT NULL
    ) ON COMMIT DROP;
    TRUNCATE _children_queue;

    IF v_party_type = 'SUPPLIER' THEN
        -- purchase_items eligible: received_date IS NOT NULL, approval latest step >= 4 (Receiving), action != REJECTED
        INSERT INTO _children_queue (kind, child_id, amount, remaining)
        SELECT 'PURCHASE_ITEM', pi.id, pi.total_price, pi.total_price
        FROM purchase_items pi
        JOIN purchases p ON p.id = pi.purchase_id
        JOIN LATERAL (
            SELECT a.step_number, a.action
            FROM approvals a
            WHERE a.approvable_type = 'PURCHASES' AND a.approvable_id = p.id
            ORDER BY a.action_at DESC, a.id DESC
            LIMIT 1
        ) la ON true
        WHERE p.supplier_id = p_party_id
          AND p.deleted_at IS NULL
          AND pi.received_date IS NOT NULL
          AND la.step_number >= 4
          AND (la.action IS NULL OR la.action <> 'REJECTED')
          AND pi.total_price > 0
        ORDER BY pi.received_date ASC, pi.id ASC;

        -- expense_realizations eligible: parent expense approval latest step >= 5 (Realisasi), action != REJECTED.
        -- Sort pakai e.transaction_date supaya FIFO konsisten dengan tanggal yang di-display di report.
        INSERT INTO _children_queue (kind, child_id, amount, remaining)
        SELECT 'EXPENSE_REALIZATION', er.id, (er.qty * er.price), (er.qty * er.price)
        FROM expense_realizations er
        JOIN expense_nonstocks en ON en.id = er.expense_nonstock_id
        JOIN expenses e ON e.id = en.expense_id
        JOIN LATERAL (
            SELECT a.step_number, a.action
            FROM approvals a
            WHERE a.approvable_type = 'EXPENSES' AND a.approvable_id = e.id
            ORDER BY a.action_at DESC, a.id DESC
            LIMIT 1
        ) la ON true
        WHERE e.supplier_id = p_party_id
          AND e.deleted_at IS NULL
          AND la.step_number >= 5
          AND (la.action IS NULL OR la.action <> 'REJECTED')
          AND (er.qty * er.price) > 0
        ORDER BY e.transaction_date ASC, e.id ASC, er.id ASC;

    ELSIF v_party_type = 'CUSTOMER' THEN
        -- marketing_delivery_products eligible: delivery_date IS NOT NULL (match current report behavior, tidak filter approval)
        INSERT INTO _children_queue (kind, child_id, amount, remaining)
        SELECT 'MDP', mdp.id, mdp.total_price, mdp.total_price
        FROM marketing_delivery_products mdp
        JOIN marketing_products mp ON mp.id = mdp.marketing_product_id
        JOIN marketings m ON m.id = mp.marketing_id
        WHERE m.customer_id = p_party_id
          AND m.deleted_at IS NULL
          AND mdp.delivery_date IS NOT NULL
          AND mdp.total_price > 0
        ORDER BY mdp.delivery_date ASC, mdp.id ASC;
    ELSE
        RETURN;
    END IF;

    -- Skip jika tidak ada children eligible
    IF NOT EXISTS (SELECT 1 FROM _children_queue) THEN
        RETURN;
    END IF;

    -- Loop SEMUA payments termasuk SALDO_AWAL ORDER BY payment_date ASC, id ASC.
    -- SALDO_AWAL diperlakukan sebagai payment tertua sehingga opening credit otomatis
    -- consume oldest debts via FIFO. Tanpa allocation row, debt yang ter-cover SaldoAwal
    -- akan tampak "Belum Lunas" di report.
    FOR v_payment IN
        SELECT id, nominal
        FROM payments
        WHERE party_type = v_party_type
          AND party_id = p_party_id
          AND deleted_at IS NULL
          AND nominal > v_eps
        ORDER BY payment_date ASC, id ASC
    LOOP
        v_remaining := v_payment.nominal;

        -- Greedy alokasi ke children tertua dengan remaining > 0
        FOR v_child IN
            SELECT seq, kind, child_id, remaining
            FROM _children_queue
            WHERE remaining > v_eps
            ORDER BY seq ASC
        LOOP
            EXIT WHEN v_remaining <= v_eps;

            -- v_child.remaining is snapshot at cursor open; re-fetch latest to avoid drift in same payment iter
            SELECT remaining INTO v_used FROM _children_queue WHERE seq = v_child.seq;
            IF v_used <= v_eps THEN
                CONTINUE;
            END IF;

            v_used := LEAST(v_remaining, v_used);
            UPDATE _children_queue SET remaining = remaining - v_used WHERE seq = v_child.seq;
            v_remaining := v_remaining - v_used;

            IF v_child.kind = 'PURCHASE_ITEM' THEN
                INSERT INTO payment_allocations (payment_id, purchase_item_id, amount, allocated_at)
                VALUES (v_payment.id, v_child.child_id, v_used, NOW());
            ELSIF v_child.kind = 'MDP' THEN
                INSERT INTO payment_allocations (payment_id, marketing_delivery_product_id, amount, allocated_at)
                VALUES (v_payment.id, v_child.child_id, v_used, NOW());
            ELSIF v_child.kind = 'EXPENSE_REALIZATION' THEN
                INSERT INTO payment_allocations (payment_id, expense_realization_id, amount, allocated_at)
                VALUES (v_payment.id, v_child.child_id, v_used, NOW());
            END IF;
        END LOOP;
    END LOOP;
END;
$func$ LANGUAGE plpgsql;

-- Invoke per-party. Gagal di satu party → entire transaction ROLLBACK.
DO $do$
DECLARE
    r RECORD;
BEGIN
    FOR r IN
        SELECT DISTINCT party_type, party_id
        FROM payments
        WHERE deleted_at IS NULL
          AND party_id IS NOT NULL
    LOOP
        PERFORM fn_fifo_backfill_party(r.party_type, r.party_id);
    END LOOP;
END;
$do$;
