package order

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/perfect-panel/server/internal/model/log"
	modelorder "github.com/perfect-panel/server/internal/model/order"
	modeluser "github.com/perfect-panel/server/internal/model/user"
	"github.com/perfect-panel/server/internal/svc"
	"github.com/perfect-panel/server/internal/types"
	"github.com/perfect-panel/server/pkg/constant"
	"github.com/perfect-panel/server/pkg/logger"
	"github.com/perfect-panel/server/pkg/xerr"
	"github.com/pkg/errors"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

const (
	orderStatusRefunded = 6
)

type RefundOrderLogic struct {
	logger.Logger
	ctx    context.Context
	svcCtx *svc.ServiceContext
}

func NewRefundOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RefundOrderLogic {
	return &RefundOrderLogic{
		Logger: logger.WithContext(ctx),
		ctx:    ctx,
		svcCtx: svcCtx,
	}
}

func (l *RefundOrderLogic) RefundOrder(req *types.RefundOrderRequest) error {
	operator, ok := l.ctx.Value(constant.CtxKeyUser).(*modeluser.User)
	if !ok {
		return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
	}

	reason := strings.TrimSpace(req.Reason)
	var cachesToClear []*modeluser.Subscribe
	var planCacheIDs []int64
	var userCacheTargets []*modeluser.User

	err := l.svcCtx.DB.WithContext(l.ctx).Transaction(func(tx *gorm.DB) error {
		var orderInfo modelorder.Order
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
			Model(&modelorder.Order{}).
			Where("id = ?", req.Id).
			First(&orderInfo).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return errors.Wrapf(xerr.NewErrCode(xerr.OrderNotExist), "order %d not found", req.Id)
			}
			return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query order failed: %v", err)
		}

		if orderInfo.Status == orderStatusRefunded {
			return errors.Wrapf(xerr.NewErrCode(xerr.OrderAlreadyRefunded), "order %d already refunded", orderInfo.Id)
		}
		if orderInfo.Status != 2 && orderInfo.Status != 5 {
			return errors.Wrapf(xerr.NewErrCode(xerr.OrderStatusError), "order %d status %d is not refundable", orderInfo.Id, orderInfo.Status)
		}

		userSub, err := l.lockRefundTargetSubscription(tx, &orderInfo)
		if err != nil {
			return err
		}

		orderStatusBefore := orderInfo.Status
		subStatusBefore := userSub.Status
		subExpireBefore := userSub.ExpireTime
		now := time.Now()

		referer, commissionAmount, err := l.lockCommissionSource(tx, orderInfo.OrderNo, orderInfo.Commission)
		if err != nil {
			return err
		}

		var commissionBefore int64
		var commissionAfter int64
		if referer != nil {
			commissionBefore = referer.Commission
			commissionAfter = referer.Commission - commissionAmount
			if err := tx.Model(&modeluser.User{}).
				Where("id = ?", referer.Id).
				UpdateColumn("commission", gorm.Expr("commission - ?", commissionAmount)).Error; err != nil {
				return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "refund commission failed: %v", err)
			}

			commissionLog := log.Commission{
				Type:      log.CommissionTypeRefund,
				Amount:    -commissionAmount,
				OrderNo:   orderInfo.OrderNo,
				Timestamp: now.UnixMilli(),
			}
			content, marshalErr := commissionLog.Marshal()
			if marshalErr != nil {
				return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "marshal commission refund log failed: %v", marshalErr)
			}
			if err := tx.Model(&log.SystemLog{}).Create(&log.SystemLog{
				Type:      log.TypeCommission.Uint8(),
				Date:      now.Format(time.DateOnly),
				ObjectID:  referer.Id,
				Content:   string(content),
				CreatedAt: now,
			}).Error; err != nil {
				return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert commission refund log failed: %v", err)
			}
			referer.Commission = commissionAfter
			userCacheTargets = append(userCacheTargets, referer)
		}

		if err := tx.Model(&modelorder.Order{}).
			Where("id = ? AND status IN ?", orderInfo.Id, []int{2, 5}).
			Update("status", orderStatusRefunded).Error; err != nil {
			return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "refund order status update failed: %v", err)
		}
		orderInfo.Status = orderStatusRefunded

		userSub.Status = 3
		userSub.ExpireTime = now.Add(-time.Second)
		userSub.FinishedAt = &now
		if err := tx.Model(&modeluser.Subscribe{}).
			Where("id = ?", userSub.Id).
			Updates(map[string]interface{}{
				"status":      userSub.Status,
				"expire_time": userSub.ExpireTime,
				"finished_at": userSub.FinishedAt,
			}).Error; err != nil {
			return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update subscription failed: %v", err)
		}

		refundLog, err := l.buildRefundAuditLog(operator, &orderInfo, userSub, referer, commissionAmount, reason, orderStatusBefore, subStatusBefore, subExpireBefore, commissionBefore, commissionAfter, now)
		if err != nil {
			return err
		}
		logContent, err := refundLog.Marshal()
		if err != nil {
			return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "marshal refund audit log failed: %v", err)
		}
		if err := tx.Model(&log.SystemLog{}).Create(&log.SystemLog{
			Type:      log.TypeOrderRefund.Uint8(),
			Date:      now.Format(time.DateOnly),
			ObjectID:  orderInfo.Id,
			Content:   string(logContent),
			CreatedAt: now,
		}).Error; err != nil {
			return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert refund audit log failed: %v", err)
		}

		cachesToClear = append(cachesToClear, userSub)
		if userSub.SubscribeId > 0 {
			planCacheIDs = append(planCacheIDs, userSub.SubscribeId)
		}
		if orderInfo.UserId > 0 {
			orderUser := &modeluser.User{Id: orderInfo.UserId}
			userCacheTargets = append(userCacheTargets, orderUser)
		}
		return nil
	})
	if err != nil {
		l.Errorw("[RefundOrder] refund failed", logger.Field("error", err.Error()), logger.Field("order_id", req.Id))
		return err
	}

	if len(cachesToClear) > 0 {
		if clearErr := l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, cachesToClear...); clearErr != nil {
			l.Errorw("[RefundOrder] clear subscribe cache failed", logger.Field("error", clearErr.Error()), logger.Field("order_id", req.Id))
		}
	}
	for _, subscribeID := range planCacheIDs {
		if clearErr := l.svcCtx.SubscribeModel.ClearCache(l.ctx, subscribeID); clearErr != nil {
			l.Errorw("[RefundOrder] clear subscribe cache failed", logger.Field("error", clearErr.Error()), logger.Field("subscribe_id", subscribeID))
		}
	}
	if len(userCacheTargets) > 0 {
		if clearErr := l.svcCtx.UserModel.ClearUserCache(l.ctx, userCacheTargets...); clearErr != nil {
			l.Errorw("[RefundOrder] clear user cache failed", logger.Field("error", clearErr.Error()))
		}
	}
	return nil
}

func (l *RefundOrderLogic) lockRefundTargetSubscription(tx *gorm.DB, orderInfo *modelorder.Order) (*modeluser.Subscribe, error) {
	var userSub modeluser.Subscribe
	err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
		Model(&modeluser.Subscribe{}).
		Where("order_id = ?", orderInfo.Id).
		First(&userSub).Error
	if err == nil {
		return &userSub, nil
	}
	if !errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query refund subscription failed: %v", err)
	}

	if orderInfo.Type != 2 {
		return nil, errors.Wrapf(xerr.NewErrCode(xerr.OrderRefundNoSubscription), "order %d has no linked subscription", orderInfo.Id)
	}

	if orderInfo.ParentId == 0 {
		return nil, errors.Wrapf(xerr.NewErrCode(xerr.OrderRefundNoSubscription), "renewal order %d has no parent order", orderInfo.Id)
	}

	err = tx.Clauses(clause.Locking{Strength: "UPDATE"}).
		Model(&modeluser.Subscribe{}).
		Where("order_id = ?", orderInfo.ParentId).
		First(&userSub).Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, errors.Wrapf(xerr.NewErrCode(xerr.OrderRefundNoSubscription), "renewal order %d parent subscription not found", orderInfo.Id)
		}
		return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query renewal subscription failed: %v", err)
	}
	return &userSub, nil
}

func (l *RefundOrderLogic) lockCommissionSource(tx *gorm.DB, orderNo string, orderCommission int64) (*modeluser.User, int64, error) {
	var commissionLogs []log.SystemLog
	if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
		Model(&log.SystemLog{}).
		Where("type = ? AND content LIKE ?", log.TypeCommission.Uint8(), fmt.Sprintf("%%\"order_no\":\"%s\"%%", orderNo)).
		Order("id DESC").
		Find(&commissionLogs).Error; err != nil {
		return nil, 0, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query commission log failed: %v", err)
	}

	for _, item := range commissionLogs {
		var content log.Commission
		if err := content.Unmarshal([]byte(item.Content)); err != nil {
			continue
		}
		if content.Type != log.CommissionTypePurchase && content.Type != log.CommissionTypeRenewal {
			continue
		}

		var referer modeluser.User
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
			Model(&modeluser.User{}).
			Where("id = ?", item.ObjectID).
			First(&referer).Error; err != nil {
			return nil, 0, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query commission owner failed: %v", err)
		}
		return &referer, content.Amount, nil
	}

	if orderCommission > 0 {
		return nil, 0, errors.Wrapf(xerr.NewErrCode(xerr.OrderRefundCommissionMismatch), "order %s commission owner not found", orderNo)
	}
	return nil, 0, nil
}

func (l *RefundOrderLogic) buildRefundAuditLog(
	operator *modeluser.User,
	orderInfo *modelorder.Order,
	userSub *modeluser.Subscribe,
	referer *modeluser.User,
	commissionAmount int64,
	reason string,
	orderStatusBefore uint8,
	subStatusBefore uint8,
	subExpireBefore time.Time,
	commissionBefore int64,
	commissionAfter int64,
	now time.Time,
) (*log.OrderRefund, error) {
	refundLog := &log.OrderRefund{
		OrderId:               orderInfo.Id,
		OrderNo:               orderInfo.OrderNo,
		OperatorUserId:        operator.Id,
		TargetUserId:          userSub.UserId,
		UserSubscribeId:       userSub.Id,
		CommissionAmount:      commissionAmount,
		Reason:                reason,
		OrderStatusBefore:     orderStatusBefore,
		OrderStatusAfter:      orderInfo.Status,
		SubscribeStatusBefore: subStatusBefore,
		SubscribeStatusAfter:  userSub.Status,
		SubscribeExpireBefore: subExpireBefore.UnixMilli(),
		SubscribeExpireAfter:  userSub.ExpireTime.UnixMilli(),
		CommissionBefore:      commissionBefore,
		CommissionAfter:       commissionAfter,
		Timestamp:             now.UnixMilli(),
	}
	if referer != nil {
		refundLog.RefererUserId = referer.Id
	}
	if len(operator.AuthMethods) > 0 {
		refundLog.OperatorAuthIdentifier = operator.AuthMethods[0].AuthIdentifier
	}
	return refundLog, nil
}
