-- =============================================================================
-- Two-way sync: public.bills <-> accounting.bills
--
-- Forward:  public.bills changes -> upsert accounting.bills (linked via
--           external_source='acmacc_bill', external_id=public.bills.id::text).
--           Approvals reach accounting indirectly: the existing
--           recompute_bill_status_from_approvals trigger flips
--           public.bills.status, which then fires the forward trigger below.
--
-- Reverse:  accounting.bills.status='void' -> public.bills.status='cancelled'.
--           accounting.bills paid sync (paid_amount >= total) is already
--           handled by accounting.sync_accounting_bill_paid; left untouched.
--
-- Note: the forward sync function and trigger added here are dropped by the
-- next migration (drop_redundant_bills_forward_sync) because the existing
-- accounting.sync_public_bill / trg_acct_sync_public_bill chain already does
-- the forward direction more thoroughly (vendor auto-create, bill_items,
-- GL posting). The reverse trigger and index stay.
-- =============================================================================

-- Fast lookups for the external link used by both sync directions.
CREATE INDEX IF NOT EXISTS accounting_bills_external_lookup_idx
  ON accounting.bills (external_source, external_id);

-- ---------------------------------------------------------------------------
-- Forward sync function
-- ---------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION accounting.sync_public_bill_to_accounting(_public_bill_id uuid)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'accounting'
AS $$
DECLARE
  pb           public.bills%ROWTYPE;
  v_company_id uuid;
  v_vendor_id  uuid;
  v_status     accounting.bill_status;
  v_existing   accounting.bills%ROWTYPE;
  v_number     text;
BEGIN
  SELECT * INTO pb FROM public.bills WHERE id = _public_bill_id;
  IF NOT FOUND THEN
    RETURN;
  END IF;

  -- Resolve accounting company for the association (try ensure_ then fallback)
  BEGIN
    v_company_id := accounting.ensure_company_for_association(pb.association_id);
  EXCEPTION WHEN OTHERS THEN
    BEGIN
      v_company_id := accounting.company_id_for_association(pb.association_id);
    EXCEPTION WHEN OTHERS THEN
      RAISE WARNING 'accounting sync: cannot resolve company for association % (bill %): %',
        pb.association_id, pb.id, SQLERRM;
      RETURN;
    END;
  END;

  IF v_company_id IS NULL THEN
    RAISE WARNING 'accounting sync: no company resolved for association % (bill %)',
      pb.association_id, pb.id;
    RETURN;
  END IF;

  -- Status mapping
  v_status := CASE pb.status
    WHEN 'approved'  THEN 'open'::accounting.bill_status
    WHEN 'paid'      THEN 'paid'::accounting.bill_status
    WHEN 'denied'    THEN 'void'::accounting.bill_status
    WHEN 'cancelled' THEN 'void'::accounting.bill_status
    WHEN 'pending'   THEN 'draft'::accounting.bill_status
    WHEN 'draft'     THEN 'draft'::accounting.bill_status
    ELSE                  'draft'::accounting.bill_status
  END;

  SELECT * INTO v_existing
  FROM accounting.bills
  WHERE external_source = 'acmacc_bill' AND external_id = pb.id::text;

  -- Look up matching accounting vendor (NULL if none — column is nullable)
  IF pb.vendor_id IS NOT NULL THEN
    SELECT id INTO v_vendor_id
    FROM accounting.vendors
    WHERE company_id      = v_company_id
      AND external_source = 'acmacc_vendor'
      AND external_id     = pb.vendor_id::text
    LIMIT 1;
  END IF;

  v_number := COALESCE(NULLIF(pb.invoice_number, ''), 'BILL-' || substr(pb.id::text, 1, 8));

  IF v_existing.id IS NULL THEN
    -- Only materialize the accounting row once the public bill has reached
    -- a state worth posting; skip pure-draft creations.
    IF pb.status NOT IN ('approved', 'paid', 'denied', 'cancelled') THEN
      RETURN;
    END IF;

    INSERT INTO accounting.bills (
      company_id, vendor_id, number, issue_date, due_date,
      status, subtotal, tax, total, paid_amount, notes,
      external_source, external_id
    ) VALUES (
      v_company_id, v_vendor_id, v_number, pb.bill_date, pb.due_date,
      v_status, pb.amount, 0, pb.amount, pb.amount_paid, pb.notes,
      'acmacc_bill', pb.id::text
    );
  ELSE
    UPDATE accounting.bills SET
      vendor_id   = COALESCE(v_vendor_id, vendor_id),
      number      = v_number,
      issue_date  = pb.bill_date,
      due_date    = pb.due_date,
      status      = v_status,
      subtotal    = pb.amount,
      total       = pb.amount,
      paid_amount = pb.amount_paid,
      notes       = pb.notes,
      updated_at  = now()
    WHERE id = v_existing.id
      AND (
           v_existing.vendor_id   IS DISTINCT FROM COALESCE(v_vendor_id, v_existing.vendor_id)
        OR v_existing.number      IS DISTINCT FROM v_number
        OR v_existing.issue_date  IS DISTINCT FROM pb.bill_date
        OR v_existing.due_date    IS DISTINCT FROM pb.due_date
        OR v_existing.status      IS DISTINCT FROM v_status
        OR v_existing.subtotal    IS DISTINCT FROM pb.amount
        OR v_existing.total       IS DISTINCT FROM pb.amount
        OR v_existing.paid_amount IS DISTINCT FROM pb.amount_paid
        OR v_existing.notes       IS DISTINCT FROM pb.notes
      );
  END IF;
END;
$$;

-- ---------------------------------------------------------------------------
-- Forward trigger
-- ---------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION public.tg_bills_sync_to_accounting()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public'
AS $$
BEGIN
  BEGIN
    PERFORM accounting.sync_public_bill_to_accounting(NEW.id);
  EXCEPTION WHEN OTHERS THEN
    -- Never break the originating bill write because of a sync failure.
    RAISE WARNING 'public.bills -> accounting.bills sync failed for %: %', NEW.id, SQLERRM;
  END;
  RETURN NEW;
END;
$$;

DROP TRIGGER IF EXISTS trg_public_bills_sync_accounting ON public.bills;
CREATE TRIGGER trg_public_bills_sync_accounting
AFTER INSERT OR UPDATE OF
  status, amount, amount_paid, bill_date, due_date, vendor_id, invoice_number, notes
ON public.bills
FOR EACH ROW
EXECUTE FUNCTION public.tg_bills_sync_to_accounting();

-- ---------------------------------------------------------------------------
-- Reverse trigger: accounting.bills.status='void' -> public.bills.status='cancelled'
-- (paid back-sync is already done by accounting.sync_accounting_bill_paid)
-- ---------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION accounting.tg_accounting_bill_status_back_sync()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'accounting'
AS $$
DECLARE
  _public_id uuid;
BEGIN
  IF NEW.external_source IS DISTINCT FROM 'acmacc_bill' OR NEW.external_id IS NULL THEN
    RETURN NEW;
  END IF;
  IF OLD.status IS NOT DISTINCT FROM NEW.status THEN
    RETURN NEW;
  END IF;

  BEGIN
    _public_id := NEW.external_id::uuid;
  EXCEPTION WHEN OTHERS THEN
    RETURN NEW;
  END;

  IF NEW.status = 'void' THEN
    UPDATE public.bills
       SET status = 'cancelled', updated_at = now()
     WHERE id = _public_id
       AND status NOT IN ('paid', 'cancelled');

    UPDATE public.bill_approvals
       SET status = 'cancelled', updated_at = now()
     WHERE bill_id = _public_id
       AND status IS DISTINCT FROM 'cancelled';
  END IF;

  RETURN NEW;
END;
$$;

DROP TRIGGER IF EXISTS trg_acct_bill_status_back_sync ON accounting.bills;
CREATE TRIGGER trg_acct_bill_status_back_sync
AFTER UPDATE OF status ON accounting.bills
FOR EACH ROW
EXECUTE FUNCTION accounting.tg_accounting_bill_status_back_sync();
