package finance

import (
	"crypto/sha512"
	"crypto/subtle"
	"encoding/hex"
	"errors"
	"strings"
)

// Midtrans webhook settle seam. Store-independent pieces live here: payload
// shape, signature verification, and status/method mapping. The DB settlement
// itself is owned by the snap-token issuer (internal/sales) — it holds the
// invoice <-> snap_order_ref join — and calls these helpers before mutating
// state.

// Sentinel errors from the notification settle path, mapped to HTTP in the
// sales handler. Signature/amount/order failures are non-2xx so Midtrans
// retries; provider-noop outcomes are not errors.
var (
	ErrMidtransBadSignature   = errors.New("invalid midtrans signature")
	ErrMidtransAmountMismatch = errors.New("gross_amount does not match invoice total")
	ErrMidtransUnknownOrder   = errors.New("unknown snap order reference")
)

// Outcomes returned by the settle path — the handler logs them and answers 200.
const (
	MidtransOutcomeSettled        = "settled"         // capture/settlement, invoice now paid
	MidtransOutcomeAlreadySettled = "already-settled" // replay of a settled webhook, idempotent 200
	MidtransOutcomeIgnored        = "ignored"         // pending/unknown status, 200 ack no-op
	MidtransOutcomeFailedRecorded = "failed-recorded" // deny/cancel/expire/failure noted, invoice open
	// MidtransOutcomeWrittenOff is a capture for an invoice whose debt was
	// already written off (status 'cancelled'). The provider HAS the money; we
	// refuse to book it, because a written-off debt must stay written off.
	//
	// It is a 2xx outcome on purpose, and specifically NOT an error: the
	// conflict can never resolve, so a non-2xx would make Midtrans redeliver
	// the same capture forever. It is deliberately not `ignored` either — that
	// would make the capture vanish with no trace while the money sits at the
	// provider. Instead the settle path logs a warning naming the invoice, ref
	// and transaction, so an operator can refund or re-book the capture by hand.
	// The recovery poll reports the same constant for the same condition.
	MidtransOutcomeWrittenOff = "written-off"
)

// MidtransAction results.
const (
	MidtransActionSettled = "settled"
	MidtransActionFailed  = "failed"
	MidtransActionIgnored = "ignored"
)

// MidtransNotification is the raw Midtrans webhook payload. GrossAmount stays
// the exact notification string: it feeds both the signature check and the
// invoice-total reconciliation. Reformatting it before hashing is the classic
// signature bug (docs skill: hash the raw provider string).
type MidtransNotification struct {
	OrderID           string `json:"order_id"`
	TransactionID     string `json:"transaction_id"`
	TransactionStatus string `json:"transaction_status"`
	StatusCode        string `json:"status_code"`
	GrossAmount       string `json:"gross_amount"`
	PaymentType       string `json:"payment_type"`
	FraudStatus       string `json:"fraud_status"`
	SignatureKey      string `json:"signature_key"`
}

// MidtransAction maps transaction_status (and fraud_status for card
// "capture") to a settle decision. Rules per the Midtrans transaction cycle:
//   - settlement → settle; capture settles ONLY when fraud_status is accept
//     (challenge must stay pending — settling a challenged card payment
//     releases goods for a transaction Midtrans has not cleared);
//   - deny/cancel/expire/failure → failed (invoice stays open, buyer retries);
//   - everything else (pending, refund family) → ignored here. Refunds need
//     explicit merchant policy and are NOT auto-handled by this seam.
func MidtransAction(status, fraudStatus string) string {
	switch strings.ToLower(strings.TrimSpace(status)) {
	case "settlement":
		return "settled"
	case "capture":
		if strings.ToLower(strings.TrimSpace(fraudStatus)) == "accept" {
			return "settled"
		}
		return "ignored" // challenge / inspect: keep awaiting payment
	case "deny", "cancel", "expire", "failure":
		return "failed"
	default:
		return "ignored"
	}
}

// MidtransMethod maps payment_type onto the existing payment_method enum
// (migration 000095 reuses transfer/qris, untouched): qris stays qris,
// everything else (bank transfer, e-wallet, card, OTC) settles as transfer.
func MidtransMethod(paymentType string) string {
	if strings.ToLower(strings.TrimSpace(paymentType)) == "qris" {
		return "qris"
	}
	return "transfer"
}

// VerifyMidtransSignature checks SHA512(order_id+status_code+
// gross_amount+serverKey) hex == signature_key with a constant-time compare.
// Mismatch means the notification did not come from Midtrans: reject, no state
// change. The server key itself is never logged.
func VerifyMidtransSignature(orderID, statusCode, grossAmount, serverKey, signatureKey string) bool {
	sum := sha512.Sum512([]byte(orderID + statusCode + grossAmount + serverKey))
	got := hex.EncodeToString(sum[:])
	want := strings.ToLower(strings.TrimSpace(signatureKey))
	if len(got) != len(want) {
		return false
	}
	return subtle.ConstantTimeCompare([]byte(got), []byte(want)) == 1
}
