package finance

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"strconv"
	"strings"

	"github.com/TaruLab/keluargamart-backend/internal/auth"
	"github.com/TaruLab/keluargamart-backend/internal/uploads"
	"github.com/TaruLab/keluargamart-backend/pkg/httpresponse"
	"github.com/TaruLab/keluargamart-backend/pkg/logger"
	"github.com/TaruLab/keluargamart-backend/pkg/middleware"
	"github.com/go-chi/chi/v5"
	"github.com/jackc/pgx/v5/pgconn"
)

// Handler binds the finance service to HTTP. Methods are exported so the
// router can attach RequirePermission per route in main.
type Handler struct {
	svc             *Service
	resolveCustomer CustomerResolver
	saveProof       ProofSaver
}

func NewHandler(svc *Service) *Handler {
	return &Handler{svc: svc}
}

// CustomerResolver maps an authenticated user to their linked customer.
// ok=false means the user has no linked customer (buyer views return empty).
type CustomerResolver func(ctx context.Context, userID int64) (customerID int64, ok bool, err error)

// WithCustomerResolver wires the user->customer seam used by the buyer-scoped
// /me views (Phase 6 item 5, B2B surface).
func (h *Handler) WithCustomerResolver(fn CustomerResolver) *Handler {
	h.resolveCustomer = fn
	return h
}

// ProofSaver stores a multipart `file` from r and returns its public URL.
// Backed by internal/uploads.SaveUpload so payment proofs obey the exact same
// size/type limits and key generation as POST /api/v1/uploads.
type ProofSaver func(ctx context.Context, r *http.Request) (url string, err error)

// WithProofSaver wires the file-storage seam used by the payment-proof upload.
func (h *Handler) WithProofSaver(fn ProofSaver) *Handler {
	h.saveProof = fn
	return h
}

// myCustomerID resolves the caller's customer, or 0 when they have none.
func (h *Handler) myCustomerID(r *http.Request) (int64, error) {
	if h.resolveCustomer == nil {
		return 0, ErrNotFound
	}
	u := auth.UserFrom(r.Context())
	if u == nil {
		return 0, ErrNotFound
	}
	id, ok, err := h.resolveCustomer(r.Context(), u.ID)
	if err != nil {
		return 0, err
	}
	if !ok {
		return 0, nil
	}
	return id, nil
}

func (h *Handler) ListAccountingPeriods(w http.ResponseWriter, r *http.Request) {
	v, err := h.svc.ListAccountingPeriods(r.Context())
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), v)
}
func periodParams(r *http.Request) (int16, int16, error) {
	y, e := strconv.Atoi(chi.URLParam(r, "year"))
	if e != nil {
		return 0, 0, ErrAccountingPeriodInvalid
	}
	m, e := strconv.Atoi(chi.URLParam(r, "month"))
	if e != nil {
		return 0, 0, ErrAccountingPeriodInvalid
	}
	return int16(y), int16(m), nil
}
func (h *Handler) GetAccountingPeriod(w http.ResponseWriter, r *http.Request) {
	y, m, e := periodParams(r)
	if e != nil {
		respondErr(w, r, e)
		return
	}
	v, e := h.svc.GetAccountingPeriod(r.Context(), y, m)
	if e != nil {
		respondErr(w, r, e)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), v)
}
func (h *Handler) PreviewAccountingPeriod(w http.ResponseWriter, r *http.Request) {
	y, m, e := periodParams(r)
	if e != nil {
		respondErr(w, r, e)
		return
	}
	v, e := h.svc.PreviewAccountingPeriod(r.Context(), y, m)
	if e != nil {
		respondErr(w, r, e)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), v)
}
func (h *Handler) ValidateAccountingPeriod(w http.ResponseWriter, r *http.Request) {
	y, m, e := periodParams(r)
	if e != nil {
		respondErr(w, r, e)
		return
	}
	v, e := h.svc.ValidateAccountingPeriod(r.Context(), y, m)
	if e != nil {
		respondErr(w, r, e)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), v)
}
func (h *Handler) CloseAccountingPeriod(w http.ResponseWriter, r *http.Request) {
	y, m, e := periodParams(r)
	if e != nil {
		respondErr(w, r, e)
		return
	}
	u := auth.UserFrom(r.Context())
	if u == nil {
		respondErr(w, r, ErrForbidden)
		return
	}
	v, e := h.svc.CloseAccountingPeriod(r.Context(), y, m, u.ID)
	if e != nil {
		respondErr(w, r, e)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), v)
}
func (h *Handler) ReopenAccountingPeriod(w http.ResponseWriter, r *http.Request) {
	y, m, e := periodParams(r)
	if e != nil {
		respondErr(w, r, e)
		return
	}
	u := auth.UserFrom(r.Context())
	if u == nil {
		respondErr(w, r, ErrForbidden)
		return
	}
	var req struct {
		Reason string `json:"reason"`
	}
	_ = json.NewDecoder(r.Body).Decode(&req)
	if strings.TrimSpace(req.Reason) == "" {
		httpresponse.Error(w, http.StatusBadRequest, middleware.RequestIDFrom(r.Context()),
			httpresponse.CodeValidation, "Alasan pembukaan kembali periode wajib diisi", nil)
		return
	}
	v, e := h.svc.ReopenAccountingPeriod(r.Context(), y, m, u.ID, strings.TrimSpace(req.Reason))
	if e != nil {
		respondErr(w, r, e)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), v)
}
func (h *Handler) AccountingPeriodHistory(w http.ResponseWriter, r *http.Request) {
	y, m, e := periodParams(r)
	if e != nil {
		respondErr(w, r, e)
		return
	}
	logs, e := h.svc.AccountingPeriodHistory(r.Context(), y, m)
	if e != nil {
		respondErr(w, r, e)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), logs)
}
func (h *Handler) ListInvoices(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	f := InvoiceFilter{
		Page:       queryInt(q.Get("page")),
		PerPage:    queryInt(q.Get("per_page")),
		Search:     q.Get("search"),
		Status:     q.Get("status"),
		CustomerID: parseInt64(q.Get("customer_id")),
		StoreID:    parseInt64(q.Get("store_id")),
		DateFrom:   q.Get("date_from"),
		DateTo:     q.Get("date_to"),
		SortBy:     q.Get("sort_by"),
		SortDir:    q.Get("sort_dir"),
	}
	list, total, err := h.svc.ListInvoices(r.Context(), f)
	if err != nil {
		respondServiceErr(w, r, err)
		return
	}
	httpresponse.OKPaginated(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), list,
		normOr(f.Page, 1), perPageOrDefault(f.PerPage), total)
}

func (h *Handler) GetInvoice(w http.ResponseWriter, r *http.Request) {
	inv, err := h.svc.GetInvoice(r.Context(), pathID(r))
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), inv)
}

// PrintInvoice serves the printable invoice document — the same aggregate as
// GetInvoice, exposed as a stable contract for the formal print view.
func (h *Handler) PrintInvoice(w http.ResponseWriter, r *http.Request) {
	inv, err := h.svc.PrintInvoice(r.Context(), pathID(r))
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), inv)
}

func (h *Handler) CreateInvoice(w http.ResponseWriter, r *http.Request) {
	var in struct {
		SalesOrderID int64 `json:"sales_order_id"`
	}
	if !decode(w, r, &in) {
		return
	}
	inv, err := h.svc.CreateInvoice(r.Context(), in.SalesOrderID)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusCreated, middleware.RequestIDFrom(r.Context()), inv)
}

func (h *Handler) PayInvoice(w http.ResponseWriter, r *http.Request) {
	var in PaymentInput
	if !decode(w, r, &in) {
		return
	}
	if in.ReferenceNumber == nil || strings.TrimSpace(*in.ReferenceNumber) == "" {
		httpresponse.Error(w, http.StatusUnprocessableEntity, middleware.RequestIDFrom(r.Context()), httpresponse.CodeValidation, "reference_number is required", nil)
		return
	}
	inv, err := h.svc.PayInvoice(r.Context(), pathID(r), in)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), inv)
}

func (h *Handler) DeleteInvoice(w http.ResponseWriter, r *http.Request) {
	if err := h.svc.DeleteInvoice(r.Context(), pathID(r)); err != nil {
		respondErr(w, r, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) ListReceivables(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	f := ReceivableFilter{
		Page:       queryInt(q.Get("page")),
		PerPage:    queryInt(q.Get("per_page")),
		CustomerID: parseInt64(q.Get("customer_id")),
		StoreID:    parseInt64(q.Get("store_id")),
		Status:     q.Get("status"),
	}
	rows, count, total, err := h.svc.ListReceivables(r.Context(), f)
	if err != nil {
		httpresponse.Internal(w, middleware.RequestIDFrom(r.Context()), logger.FromContext(r.Context()), err)
		return
	}
	httpresponse.OKPaginatedWithMeta(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), rows,
		normOr(f.Page, 1), perPageOrDefault(f.PerPage), count, map[string]any{"total_amount": total})
}

func (h *Handler) ListPayments(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	f := InvoiceFilter{
		Page: queryInt(q.Get("page")), PerPage: queryInt(q.Get("per_page")), Search: q.Get("search"),
		DateFrom: q.Get("date_from"), DateTo: q.Get("date_to"),
		CustomerID: parseInt64(q.Get("customer_id")),
		StoreID:    parseInt64(q.Get("store_id")),
		PayableID:  parseInt64(q.Get("payable_id")), PayableType: q.Get("payable_type"),
	}
	list, total, err := h.svc.ListPayments(r.Context(), f)
	if err != nil {
		respondServiceErr(w, r, err)
		return
	}
	httpresponse.OKPaginated(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), list,
		normOr(f.Page, 1), perPageOrDefault(f.PerPage), total)
}

// VerifyPayment approves or rejects a pending payment (admin action).
//
// RESPONSE SHAPE CHANGE (bug sweep 2026-09): `status` no longer carries the
// fabricated "paid"/"failed" string. The payments table has no status column
// (db/schema.sql) — money state belongs to PayInvoice, which creates payments
// already settled — so acknowledging "paid" here told the client something the
// database never recorded.
//
//	{"id": 1, "status": "paid"|"failed",        // before (fabricated, not persisted)
//	 "verdict": "verified"|"rejected",           // NEW: what was actually persisted
//	 "notes": "[Verified] pembayaran diverifikasi oleh #9 (Budi)",  // NEW: persisted text
//	 "reviewed_by": 9}                           // NEW: reviewer identity (omitted when anonymous)
//
// "status" is kept as a derived alias of "verdict" so existing clients reading
// `.status === 'paid'` keep working for the approve case; `verdict` is the
// field to read for reject. "notes" is the literal payments.notes value
// after the write, and "reviewed_by" is null when the request carried no user.
//
// The verdict note is appended to any existing note (see SetPaymentReview in
// db/query/finance.sql); re-verify overwrites the previous verdict.
func (h *Handler) VerifyPayment(w http.ResponseWriter, r *http.Request) {
	var in struct {
		Approved bool `json:"approved"`
	}
	if !decode(w, r, &in) {
		return
	}
	reqID := middleware.RequestIDFrom(r.Context())
	id := pathID(r)
	// Reviewer identity from ctx — same actor-from-ctx pattern as
	// RecordPartialPayment. The route is permission-gated, so a nil user means
	// an internal/system review, not a missing audit.
	var reviewerID *int64
	var reviewerName string
	if u := auth.UserFrom(r.Context()); u != nil {
		reviewerID = &u.ID
		reviewerName = u.Name
	}
	verdict, err := h.svc.UpdatePaymentStatus(r.Context(), id, in.Approved, reviewerID, reviewerName)
	if err != nil {
		respondServiceErr(w, r, err)
		return
	}
	verdictState := PaymentVerdictRejected
	if in.Approved {
		verdictState = PaymentVerdictVerified
	}
	// "status" is a compatibility alias only: approve keeps the legacy "paid"
	// value, reject keeps "failed". It is NOT read back from the database.
	status := "failed"
	if in.Approved {
		status = "paid"
	}
	body := map[string]any{
		"id":      id,
		"status":  status,
		"verdict": verdictState,
		"notes":   verdict,
	}
	if reviewerID != nil {
		body["reviewed_by"] = *reviewerID
	}
	httpresponse.OK(w, http.StatusOK, reqID, body)
}

// UploadPaymentProof stores a proof image for a payment and persists its URL
// on the payment row. Authorization is enforced here rather than by route
// middleware: the owning buyer (payment -> invoice -> customers.user_id) may
// upload a proof for their own payment, and staff holding
// fakturisasi.payments.verify may upload for any payment.
func (h *Handler) UploadPaymentProof(w http.ResponseWriter, r *http.Request) {
	reqID := middleware.RequestIDFrom(r.Context())
	u := auth.UserFrom(r.Context())
	if u == nil {
		httpresponse.Error(w, http.StatusUnauthorized, reqID, httpresponse.CodeAuthentication, "Unauthenticated", nil)
		return
	}
	id := pathID(r)
	// PaymentOwnerUser reports ok=false (and 0) when the payment's customer has
	// no linked user, so ownerUserID==0 denies the owner branch inside the
	// shared predicate.
	ownerID, _, err := h.svc.PaymentOwnerUser(r.Context(), id)
	if err != nil {
		respondServiceErr(w, r, err) // ErrNotFound -> 404
		return
	}
	// Same predicate as the read path (CanAccessPaymentProof): owner OR
	// ProofVerifyPermission.
	if !CanAccessPaymentProof(u, ownerID) {
		httpresponse.Error(w, http.StatusForbidden, reqID, httpresponse.CodeForbidden, "Forbidden", nil)
		return
	}
	if h.saveProof == nil {
		httpresponse.Internal(w, reqID, logger.FromContext(r.Context()), errors.New("proof saver not wired"))
		return
	}
	url, err := h.saveProof(r.Context(), r)
	if err != nil {
		switch {
		case errors.Is(err, uploads.ErrInvalidForm), errors.Is(err, uploads.ErrMissingFile),
			errors.Is(err, uploads.ErrUnsupportedType):
			httpresponse.Error(w, http.StatusBadRequest, reqID, httpresponse.CodeValidation, err.Error(), nil)
		default:
			httpresponse.Internal(w, reqID, logger.FromContext(r.Context()), err)
		}
		return
	}
	if err := h.svc.SetPaymentProof(r.Context(), id, url); err != nil {
		respondServiceErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusCreated, reqID, map[string]any{"id": id, "proof_url": url})
}

// RecordPartialPayment creates an additional payment against the same payable
// as an existing payment (§5.2), reusing PayInvoice's recording logic.
func (h *Handler) RecordPartialPayment(w http.ResponseWriter, r *http.Request) {
	var in PartialPaymentInput
	if !decode(w, r, &in) {
		return
	}
	var createdBy *int64
	if u := auth.UserFrom(r.Context()); u != nil {
		createdBy = &u.ID
	}
	p, err := h.svc.RecordPartialPayment(r.Context(), pathID(r), createdBy, in)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusCreated, middleware.RequestIDFrom(r.Context()), p)
}

func (h *Handler) ListCashflows(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	f := CashflowFilter{
		Page:     queryInt(q.Get("page")),
		PerPage:  queryInt(q.Get("per_page")),
		Type:     q.Get("type"),
		Category: q.Get("category"),
		DateFrom: q.Get("date_from"),
		DateTo:   q.Get("date_to"),
	}
	list, total, sum, err := h.svc.ListCashflows(r.Context(), f)
	if err != nil {
		if errors.Is(err, ErrInvalidDate) {
			respondErr(w, r, err)
			return
		}
		httpresponse.Internal(w, middleware.RequestIDFrom(r.Context()), logger.FromContext(r.Context()), err)
		return
	}
	httpresponse.OKPaginatedWithMeta(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), list,
		normOr(f.Page, 1), perPageOrDefault(f.PerPage), total, map[string]any{"summary": sum})
}

func (h *Handler) GetCashflow(w http.ResponseWriter, r *http.Request) {
	c, err := h.svc.GetCashflow(r.Context(), pathID(r))
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), c)
}

func (h *Handler) CreateCashflow(w http.ResponseWriter, r *http.Request) {
	var in CashflowInput
	if !decode(w, r, &in) {
		return
	}
	c, err := h.svc.CreateCashflow(r.Context(), in)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusCreated, middleware.RequestIDFrom(r.Context()), c)
}

func (h *Handler) UpdateCashflow(w http.ResponseWriter, r *http.Request) {
	var in CashflowInput
	if !decode(w, r, &in) {
		return
	}
	c, err := h.svc.UpdateCashflow(r.Context(), pathID(r), in)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), c)
}

func (h *Handler) DeleteCashflow(w http.ResponseWriter, r *http.Request) {
	if err := h.svc.DeleteCashflow(r.Context(), pathID(r)); err != nil {
		respondErr(w, r, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// ReverseCashflow records a correction entry for a generated cashflow (§18).
func (h *Handler) ReverseCashflow(w http.ResponseWriter, r *http.Request) {
	var in CashflowReverseInput
	if !decode(w, r, &in) {
		return
	}
	c, err := h.svc.ReverseCashflow(r.Context(), pathID(r), in)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusCreated, middleware.RequestIDFrom(r.Context()), c)
}

// --- helpers

func pathID(r *http.Request) int64 {
	id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
	return id
}

func decode(w http.ResponseWriter, r *http.Request, dst any) bool {
	return httpresponse.DecodeJSON(w, r, middleware.RequestIDFrom(r.Context()), dst)
}

func respondErr(w http.ResponseWriter, r *http.Request, err error) {
	var constraint *pgconn.PgError
	if errors.As(err, &constraint) && constraint.ConstraintName == "cashier_shift_open" {
		httpresponse.Error(w, http.StatusConflict, middleware.RequestIDFrom(r.Context()), httpresponse.CodeBusinessViolation, "Cashier shift is closed; payment cannot be recorded", nil)
		return
	}
	rid := middleware.RequestIDFrom(r.Context())
	switch {
	case errors.Is(err, ErrNotFound):
		httpresponse.Error(w, http.StatusNotFound, rid, httpresponse.CodeNotFound, "Not found", nil)
	case errors.Is(err, ErrForbidden), errors.Is(err, ErrNoStoreScope):
		// 403, and the message is NOT err.Error(): these name the scoping
		// mechanism, which is not the caller's business. The caller is
		// authenticated and this class of resource is known to it, so a 404 would
		// be a different lie.
		httpresponse.Error(w, http.StatusForbidden, rid, httpresponse.CodeForbidden, "Forbidden", nil)
	case errors.Is(err, ErrPaymentTooLarge), errors.Is(err, ErrUnknownPayable):
		httpresponse.Error(w, http.StatusConflict, rid, httpresponse.CodeBusinessViolation, err.Error(), nil)
	case errors.Is(err, ErrCannotInvoice):
		httpresponse.Error(w, http.StatusConflict, rid, httpresponse.CodeBusinessViolation, err.Error(), nil)
	case errors.Is(err, ErrInvoiceUnpaid), errors.Is(err, ErrInvoiceHasPayment),
		errors.Is(err, ErrPeriodCloseBlocked),
		errors.Is(err, ErrInvoiceCreationHistoryRequired), errors.Is(err, ErrAccountingPeriodLocked),
		errors.Is(err, ErrGeneratedCashflowImmutable):
		httpresponse.Error(w, http.StatusConflict, rid, httpresponse.CodeBusinessViolation, err.Error(), nil)
	case errors.Is(err, ErrNotGenerated):
		httpresponse.Error(w, http.StatusConflict, rid, httpresponse.CodeBusinessViolation, err.Error(), nil)
	case err != nil:
		httpresponse.SafeError(w, rid, logger.FromContext(r.Context()), http.StatusBadRequest, httpresponse.CodeValidation, err)
	}
}

func queryInt(s string) int {
	n, _ := strconv.Atoi(s)
	return n
}

func parseInt64(s string) int64 {
	n, _ := strconv.ParseInt(s, 10, 64)
	return n
}

func normOr(n, def int) int {
	if n < 1 {
		return def
	}
	return n
}

func perPageOrDefault(n int) int {
	// Clamp instead of falling back: a client asking for 200 used to receive
	// the 20-row default — asking for MORE quietly returned LESS, and every
	// summary computed off that page under-reported (4 Nov flow audit: the
	// receivables page's "Total Pembayaran Masuk" counted only 20 of 135
	// payments). Oversized requests now get the maximum page size.
	if n < 1 {
		return 20
	}
	if n > 100 {
		return 100
	}
	return n
}

func respondServiceErr(w http.ResponseWriter, r *http.Request, err error) {
	if errors.Is(err, ErrInvalidDate) || errors.Is(err, ErrNotFound) {
		respondErr(w, r, err)
		return
	}
	httpresponse.Internal(w, middleware.RequestIDFrom(r.Context()), logger.FromContext(r.Context()), err)
}

// --- buyer-scoped views (Phase 6 item 5, B2B surface) ------------------------

// MyInvoices lists the authenticated buyer's own invoices.
func (h *Handler) MyInvoices(w http.ResponseWriter, r *http.Request) {
	customerID, err := h.myCustomerID(r)
	if err != nil {
		httpresponse.Internal(w, middleware.RequestIDFrom(r.Context()), logger.FromContext(r.Context()), err)
		return
	}
	q := r.URL.Query()
	f := InvoiceFilter{
		Page:       queryInt(q.Get("page")),
		PerPage:    queryInt(q.Get("per_page")),
		Status:     q.Get("status"),
		CustomerID: customerID,
		DateFrom:   q.Get("date_from"),
		DateTo:     q.Get("date_to"),
		SortBy:     q.Get("sort_by"),
		SortDir:    q.Get("sort_dir"),
	}
	// customerID 0 means no linked customer; the list SQL treats <= 0 as
	// "no filter" and would leak the whole company invoice list (mirrors
	// MyPayments' service guard).
	if customerID <= 0 {
		httpresponse.OKPaginated(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), []Invoice{},
			normOr(f.Page, 1), perPageOrDefault(f.PerPage), 0)
		return
	}
	list, total, err := h.svc.ListInvoices(r.Context(), f)
	if err != nil {
		respondServiceErr(w, r, err)
		return
	}
	httpresponse.OKPaginated(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), list,
		normOr(f.Page, 1), perPageOrDefault(f.PerPage), total)
}

// MyPrintInvoice serves the printable invoice document for the authenticated
// buyer's own invoice — the same aggregate as PrintInvoice, ownership-checked
// via myCustomerID. Foreign or missing ids answer 404 so buyer ids are not
// enumerable (mirrors sales loadOwnedOrder).
func (h *Handler) MyPrintInvoice(w http.ResponseWriter, r *http.Request) {
	customerID, err := h.myCustomerID(r)
	if err != nil {
		httpresponse.Internal(w, middleware.RequestIDFrom(r.Context()), logger.FromContext(r.Context()), err)
		return
	}
	inv, err := h.svc.PrintInvoice(r.Context(), pathID(r))
	if err != nil {
		respondErr(w, r, err)
		return
	}
	if customerID == 0 || inv.CustomerID != customerID {
		respondErr(w, r, ErrNotFound)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), inv)
}

// MyReceivables lists the authenticated buyer's own outstanding receivables.
func (h *Handler) MyReceivables(w http.ResponseWriter, r *http.Request) {
	customerID, err := h.myCustomerID(r)
	if err != nil {
		httpresponse.Internal(w, middleware.RequestIDFrom(r.Context()), logger.FromContext(r.Context()), err)
		return
	}
	q := r.URL.Query()
	f := ReceivableFilter{
		Page:       queryInt(q.Get("page")),
		PerPage:    queryInt(q.Get("per_page")),
		CustomerID: customerID,
		Status:     q.Get("status"),
	}
	// customerID 0 means no linked customer; the SQL treats <= 0 as "no
	// filter" and would leak the whole company receivables list.
	if customerID <= 0 {
		httpresponse.OKPaginatedWithMeta(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), []ReceivableRow{},
			normOr(f.Page, 1), perPageOrDefault(f.PerPage), 0, map[string]any{"total_amount": 0})
		return
	}
	rows, count, total, err := h.svc.ListReceivables(r.Context(), f)
	if err != nil {
		httpresponse.Internal(w, middleware.RequestIDFrom(r.Context()), logger.FromContext(r.Context()), err)
		return
	}
	httpresponse.OKPaginatedWithMeta(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), rows,
		normOr(f.Page, 1), perPageOrDefault(f.PerPage), count, map[string]any{"total_amount": total})
}

// MyPayments lists the authenticated buyer's own payments.
func (h *Handler) MyPayments(w http.ResponseWriter, r *http.Request) {
	customerID, err := h.myCustomerID(r)
	if err != nil {
		httpresponse.Internal(w, middleware.RequestIDFrom(r.Context()), logger.FromContext(r.Context()), err)
		return
	}
	q := r.URL.Query()
	f := InvoiceFilter{
		Page:       queryInt(q.Get("page")),
		PerPage:    queryInt(q.Get("per_page")),
		CustomerID: customerID,
		DateFrom:   q.Get("date_from"),
		DateTo:     q.Get("date_to"),
		SortBy:     q.Get("sort_by"),
		SortDir:    q.Get("sort_dir"),
	}
	list, total, err := h.svc.MyPayments(r.Context(), f)
	if err != nil {
		respondServiceErr(w, r, err)
		return
	}
	httpresponse.OKPaginated(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), list,
		normOr(f.Page, 1), perPageOrDefault(f.PerPage), total)
}
