package notify

import (
	"encoding/json"
	"net/url"

	commonLogic "github.com/perfect-panel/server/internal/logic/common"
	"github.com/perfect-panel/server/pkg/constant"

	"github.com/perfect-panel/server/pkg/xerr"
	"github.com/pkg/errors"

	"github.com/gin-gonic/gin"

	"github.com/hibiken/asynq"
	"github.com/perfect-panel/server/internal/model/order"
	"github.com/perfect-panel/server/internal/model/payment"
	"github.com/perfect-panel/server/internal/svc"
	"github.com/perfect-panel/server/internal/types"
	"github.com/perfect-panel/server/pkg/logger"
	"github.com/perfect-panel/server/pkg/payment/epay"

	queueType "github.com/perfect-panel/server/queue/types"
)

// epayNotifyTradeNotSuccess identifies the "buyer paid but trade not in TRADE_SUCCESS
// terminal state" branch in metrics / log search. EPay is told 200 here so it will
// not retry; we still want it visible for monitoring (HIF-135 / HIF-136).
const epayNotifyTradeNotSuccess = "epay_notify_trade_not_success"

// epayNotifyDecision encodes the post-parse decision so the branching logic can be
// unit-tested without spinning up a real OrderModel / Redis / asynq stack.
type epayNotifyDecision int

const (
	// epayNotifyDecisionRejectSign: signature is invalid and debug bypass is off.
	// Caller MUST return an error so the handler responds non-200 and EPay retries.
	epayNotifyDecisionRejectSign epayNotifyDecision = iota
	// epayNotifyDecisionAckTradeNotSuccess: trade_status != TRADE_SUCCESS
	// (user cancelled / failed at gateway). Acknowledge with 200 "success" and emit
	// a metric-named log for monitoring.
	epayNotifyDecisionAckTradeNotSuccess
	// epayNotifyDecisionAckIdempotent: order already at Finished (status=5).
	// Repeat callback — ack with 200 "success", do not re-enqueue activation.
	epayNotifyDecisionAckIdempotent
	// epayNotifyDecisionProcess: happy path. Write trade_no, flip status to Paid,
	// enqueue activation task.
	epayNotifyDecisionProcess
)

// evaluateEPayNotify is a pure decision function so each branch can be unit-tested
// without DB/Redis. Caller has already located the order; orderStatus is the
// current status from the DB row.
func evaluateEPayNotify(signValid, debugBypass bool, tradeStatus string, orderStatus uint8) epayNotifyDecision {
	if !signValid && !debugBypass {
		return epayNotifyDecisionRejectSign
	}
	if tradeStatus != "TRADE_SUCCESS" {
		return epayNotifyDecisionAckTradeNotSuccess
	}
	if orderStatus == 5 {
		return epayNotifyDecisionAckIdempotent
	}
	return epayNotifyDecisionProcess
}

type EPayNotifyLogic struct {
	logger.Logger
	ctx    *gin.Context
	svcCtx *svc.ServiceContext
}

// EPay notify
func NewEPayNotifyLogic(ctx *gin.Context, svcCtx *svc.ServiceContext) *EPayNotifyLogic {
	return &EPayNotifyLogic{
		Logger: logger.WithContext(ctx),
		ctx:    ctx,
		svcCtx: svcCtx,
	}
}

func (l *EPayNotifyLogic) EPayNotify(req *types.EPayNotifyRequest) error {

	// Find payment config
	data, ok := l.ctx.Request.Context().Value(constant.CtxKeyPayment).(*payment.Payment)
	if !ok {
		l.Logger.Error("[EPayNotify] Payment not found in context")
		return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "payment config not found")
	}
	orderInfo, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OutTradeNo)
	if err != nil {
		// HIF-136 P02: order missing must propagate as error so EPay retries instead
		// of being silently lost (was previously masked by a `return nil` further down).
		l.Logger.Error("[EPayNotify] Find order failed", logger.Field("error", err.Error()), logger.Field("orderNo", req.OutTradeNo))
		return errors.Wrapf(xerr.NewErrCode(xerr.OrderNotExist), "order not exist: %v", req.OutTradeNo)
	}
	commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "payment_notify_received",
		"[SubscriptionFlow] epay notify received",
		append(commonLogic.OrderTraceFields(orderInfo),
			logger.Field("payment_platform", data.Platform),
			logger.Field("trade_status", req.TradeStatus),
		)...,
	)

	var config payment.EPayConfig
	if err := json.Unmarshal([]byte(data.Config), &config); err != nil {
		l.Logger.Errorw("[EPayNotify] Unmarshal config failed", logger.Field("error", err.Error()))
		return err
	}
	// Verify sign
	client := epay.NewClient(config.Pid, config.Url, config.Key, config.Type)
	signValid := client.VerifySign(urlParamsToMap(l.ctx.Request.URL.RawQuery))

	decision := evaluateEPayNotify(signValid, l.svcCtx.Config.Debug, req.TradeStatus, orderInfo.Status)
	switch decision {
	case epayNotifyDecisionRejectSign:
		// HIF-136 P01: previously `return nil` here let EPay see 200 and stop retrying;
		// caller still left order at status=1, which DeferCloseOrder then closed.
		// Return error so handler responds non-200 and EPay retries.
		l.Logger.Error("[EPayNotify] Verify sign failed",
			logger.Field("order_no", req.OutTradeNo),
			logger.Field("trade_no", req.TradeNo),
			logger.Field("raw_query", l.ctx.Request.URL.RawQuery),
		)
		return errors.Wrapf(xerr.NewErrCode(xerr.SignatureInvalid), "verify sign failed: %v", req.OutTradeNo)
	case epayNotifyDecisionAckTradeNotSuccess:
		// User cancelled / gateway-side failure: ack 200 to stop retries, but keep
		// the path observable under metric name `epay_notify_trade_not_success`.
		l.Logger.Info("[EPayNotify] Trade status not success",
			logger.Field("order_no", req.OutTradeNo),
			logger.Field("trade_status", req.TradeStatus),
			logger.Field("metric", epayNotifyTradeNotSuccess),
		)
		return nil
	case epayNotifyDecisionAckIdempotent:
		// Order already Finished — repeat callback is expected, ack with 200.
		return nil
	case epayNotifyDecisionProcess:
		// fall through
	}

	// HIF-136 P03: persist gateway trade_no BEFORE flipping status so post-mortems can
	// reverse-lookup EPay flows to ppanel orders. Raw gorm write is acceptable here —
	// the subsequent UpdateOrderStatus will invalidate the cache key (keys are by
	// order_no / id, both stable across this field write).
	if req.TradeNo != "" {
		if err := l.svcCtx.DB.WithContext(l.ctx).
			Model(&order.Order{}).
			Where("order_no = ?", req.OutTradeNo).
			Update("trade_no", req.TradeNo).Error; err != nil {
			l.Logger.Error("[EPayNotify] Update trade_no failed",
				logger.Field("error", err.Error()),
				logger.Field("order_no", req.OutTradeNo),
				logger.Field("trade_no", req.TradeNo),
			)
			return errors.Wrapf(err, "update trade_no failed: %v", req.OutTradeNo)
		}
	}

	// Update order status
	err = l.svcCtx.OrderModel.UpdateOrderStatus(l.ctx, req.OutTradeNo, 2)
	if err != nil {
		l.Logger.Error("[EPayNotify] Update order status failed", logger.Field("error", err.Error()), logger.Field("orderNo", req.OutTradeNo))
		return err
	}
	commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "payment_settled",
		"[SubscriptionFlow] epay notify marked order as paid",
		append(commonLogic.OrderTraceFields(orderInfo),
			logger.Field("payment_platform", data.Platform),
			logger.Field("trade_no", req.TradeNo),
		)...,
	)
	// Create activate order task
	payload := queueType.ForthwithActivateOrderPayload{
		OrderNo: req.OutTradeNo,
	}
	bytes, err := json.Marshal(&payload)
	if err != nil {
		l.Logger.Error("[EPayNotify] Marshal payload failed", logger.Field("error", err.Error()))
		return err
	}
	task := asynq.NewTask(queueType.ForthwithActivateOrder, bytes, asynq.MaxRetry(5))
	taskInfo, err := l.svcCtx.Queue.EnqueueContext(l.ctx, task)
	if err != nil {
		l.Logger.Error("[EPayNotify] Enqueue task failed", logger.Field("error", err.Error()))
		return err
	}
	commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "activation_task_enqueued",
		"[SubscriptionFlow] activation task enqueued from epay notify",
		append(commonLogic.OrderTraceFields(orderInfo),
			logger.Field("payment_platform", data.Platform),
			logger.Field("queue_task_id", taskInfo.ID),
		)...,
	)
	l.Logger.Info("[EPayNotify] Enqueue task success", logger.Field("taskInfo", taskInfo))
	return nil
}

func urlParamsToMap(query string) map[string]string {
	params := make(map[string]string)
	values, _ := url.ParseQuery(query)
	for k, v := range values {
		if len(v) > 0 {
			params[k] = v[0]
		}
	}
	return params
}
