package proxy

import (
	"context"
	"time"

	"apps/backend/internal/svc"

	"github.com/zeromicro/go-zero/core/logx"
)

// proxyCharge tracks one reserved quota unit for a proxied upstream call.
//
// The key mode is resolved once by the caller and reused for both the reservation and the audit
// event, because resolving separately reads member settings twice and a member editing their
// keys in between would reserve against one mode and bill the other.
type proxyCharge struct {
	svcCtx  *svc.ServiceContext
	uid     int64
	meter   string
	mode    string
	label   string
	source  string
	settled bool
}

// reserveQuota holds quota for an upcoming call. Pair it with `defer charge.Settle(ctx, &err)`
// on a named error return so that failures refund instead of charging for nothing.
func reserveQuota(ctx context.Context, svcCtx *svc.ServiceContext, uid int64, meter, mode, label, source string) (*proxyCharge, error) {
	if err := svcCtx.Usage.PrepareCallWithMode(ctx, uid, meter, mode); err != nil {
		return nil, err
	}
	return &proxyCharge{
		svcCtx: svcCtx, uid: uid, meter: meter, mode: mode, label: label, source: source,
	}, nil
}

// Settle commits the charge on success and refunds it on failure.
func (c *proxyCharge) Settle(ctx context.Context, errp *error) {
	if errp != nil && *errp != nil {
		c.release(ctx)
		return
	}
	c.commit(ctx)
}

// commit writes the audit event. The upstream call already succeeded, so a failure here is
// logged rather than returned: the member has their result and charging them is correct, but
// losing the event must not be silent either.
func (c *proxyCharge) commit(ctx context.Context) {
	if c == nil || c.settled {
		return
	}
	c.settled = true
	if _, err := c.svcCtx.Usage.RecordCall(ctx, c.uid, c.meter, c.mode, c.label, c.source); err != nil {
		logx.Errorf("usage record uid=%d meter=%s source=%s: %v", c.uid, c.meter, c.source, err)
	}
}

func (c *proxyCharge) release(ctx context.Context) {
	if c == nil || c.settled {
		return
	}
	c.settled = true
	// The request context is often already cancelled on the path being compensated for.
	rctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second)
	defer cancel()
	if err := c.svcCtx.Usage.ReleaseCall(rctx, c.uid, c.meter, c.mode); err != nil {
		logx.Errorf("usage release uid=%d meter=%s source=%s: %v", c.uid, c.meter, c.source, err)
	}
}
