package finance

// Helpers backing hardening.go (client feedback 2026-08-26 §5–§10).

import (
	"encoding/json"
	"errors"
	"log/slog"
	"net/http"
	"strconv"
	"time"

	"github.com/jackc/pgx/v5/pgtype"
	"github.com/shopspring/decimal"

	"github.com/TaruLab/keluargamart-backend/pkg/httpresponse"
	"github.com/TaruLab/keluargamart-backend/pkg/logger"
	"github.com/TaruLab/keluargamart-backend/pkg/middleware"
)

var ErrPaymentMethodInvalid = errors.New("payment_method must be cash, transfer, giro, cheque, qris or kartu")

const seqKeyVPC = "vpc"

func jsonNewDecoder(r *http.Request) *json.Decoder { return json.NewDecoder(r.Body) }

func loggerFrom(r *http.Request) *slog.Logger { return slog.Default() }

func pathInt64(r *http.Request) int64 {
	id := r.PathValue("id")
	n, _ := strconv.ParseInt(id, 10, 64)
	return n
}

func queryInt64Default(v string, def int64) int64 {
	if v == "" {
		return def
	}
	n, err := strconv.ParseInt(v, 10, 64)
	if err != nil || n <= 0 {
		return def
	}
	return n
}

func pgText(s string) pgtype.Text { return pgtype.Text{String: s, Valid: true} }

func pgTextV(s string) pgtype.Text { return pgtype.Text{String: s, Valid: s != ""} }

func pgInt8(id int64) pgtype.Int8 { return pgtype.Int8{Int64: id, Valid: id > 0} }

func respondNotFound(w http.ResponseWriter, r *http.Request) {
	httpresponse.Error(w, http.StatusNotFound, middleware.RequestIDFrom(r.Context()), "NOT_FOUND", "Not found", nil)
}

func respondFinanceErr(w http.ResponseWriter, r *http.Request, err error) {
	rid := middleware.RequestIDFrom(r.Context())
	msg := err.Error()
	switch {
	// A refused outlet scope is 403, checked BEFORE the string matching below:
	// the sentinel's message is about the scoping mechanism and must never reach
	// the wire, and the substring cases would otherwise not recognise it at all
	// and answer 500 for what is an authorization decision.
	case errors.Is(err, ErrForbidden), errors.Is(err, ErrNoStoreScope):
		httpresponse.Error(w, http.StatusForbidden, rid, httpresponse.CodeForbidden, "Forbidden", nil)
		return
	// These two answer 409 with a fixed message. They return rather than
	// assigning to status/code/msg because the tail below hands err to
	// SafeError, and a reassigned message would then be overwritten by the
	// classification.
	case containsStr(msg, "accounting_period_locked"):
		httpresponse.Error(w, http.StatusConflict, rid, httpresponse.CodeForbidden,
			"Periode akuntansi untuk transaksi ini sudah dikunci.", nil)
		return
	case containsStr(msg, "generated_cashflow_immutable"):
		httpresponse.Error(w, http.StatusConflict, rid, httpresponse.CodeForbidden,
			"Jurnal yang dihasilkan sistem tidak dapat diubah.", nil)
		return
	}
	// Nothing recognised it. A database error can still be sitting in err, and
	// its text names tables and constraints, so it goes through the boundary
	// rather than onto the wire.
	httpresponse.SafeError(w, rid, logger.FromContext(r.Context()),
		http.StatusInternalServerError, httpresponse.CodeInternal, err)
}

func containsStr(haystack, needle string) bool {
	for i := 0; i+len(needle) <= len(haystack); i++ {
		if haystack[i:i+len(needle)] == needle {
			return true
		}
	}
	return false
}

func decodeHard(w http.ResponseWriter, r *http.Request, dst any) error {
	if err := json.NewDecoder(r.Body).Decode(dst); err != nil {
		httpresponse.Error(w, http.StatusBadRequest, middleware.RequestIDFrom(r.Context()),
			httpresponse.CodeValidation, "invalid JSON body", nil)
		return err
	}
	return nil
}

func textPgPtr(p *string) pgtype.Text {
	if p == nil {
		return pgtype.Text{}
	}
	return pgtype.Text{String: *p, Valid: true}
}

func decimalZero() decimal.Decimal { return decimal.Zero }

func pgDate(t time.Time) pgtype.Date { return pgtype.Date{Time: t, Valid: true} }

func pgDateOrNilStr(s string) pgtype.Date {
	if s == "" {
		return pgtype.Date{}
	}
	t, err := time.Parse("2006-01-02", s)
	if err != nil {
		return pgtype.Date{}
	}
	return pgtype.Date{Time: t, Valid: true}
}

func pgBoolPtr(b *bool) pgtype.Bool {
	if b == nil {
		return pgtype.Bool{}
	}
	return pgtype.Bool{Bool: *b, Valid: true}
}
