// Copyright 2025 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package object

import (
	"fmt"
	"strings"

	"github.com/beego/beego/v2/core/logs"
	"github.com/casdoor/casdoor/idp"
	"github.com/casdoor/casdoor/pp"
	"github.com/casdoor/casdoor/util"
)

func PlaceOrder(owner string, reqProductInfos []ProductInfo, user *User, couponCode string) (*Order, error) {
	if len(reqProductInfos) == 0 {
		return nil, fmt.Errorf("order has no products")
	}

	productNames := make([]string, 0, len(reqProductInfos))
	for _, reqInfo := range reqProductInfos {
		if reqInfo.Name == "" {
			return nil, fmt.Errorf("product name cannot be empty")
		}
		productNames = append(productNames, reqInfo.Name)
	}

	products, err := getOrderProducts(owner, productNames)
	if err != nil {
		return nil, err
	}
	productMap := make(map[string]Product, len(reqProductInfos))
	for _, product := range products {
		productMap[product.Name] = product
	}

	orderCurrency := products[0].Currency
	if orderCurrency == "" {
		orderCurrency = "USD"
	}

	if err := validateProductCurrencies(products, orderCurrency); err != nil {
		return nil, err
	}

	var productInfos []ProductInfo
	orderPrice := 0.0
	for _, productInfo := range reqProductInfos {
		product := productMap[productInfo.Name]

		var productPrice float64
		if product.IsRecharge {
			productPrice = productInfo.Price
			if productPrice <= 0 {
				return nil, fmt.Errorf("the custom price should be greater than zero")
			}
		} else {
			productPrice = product.Price
		}
		productInfos = append(productInfos, ProductInfo{
			Owner:       owner,
			Name:        product.Name,
			DisplayName: product.DisplayName,
			Image:       product.Image,
			Detail:      product.Detail,
			Price:       productPrice,
			Currency:    product.Currency,
			IsRecharge:  product.IsRecharge,
			Quantity:    productInfo.Quantity,
			PricingName: productInfo.PricingName,
			PlanName:    productInfo.PlanName,
		})

		orderPrice += productPrice * float64(productInfo.Quantity)
	}

	// Apply coupon discount if provided
	var couponName string
	var couponDiscount float64
	if couponCode != "" {
		coupon, err := ValidateCoupon(owner, couponCode, user.Name, productNames, orderPrice, orderCurrency)
		if err != nil {
			return nil, err
		}
		couponDiscount = CalculateDiscount(coupon, orderPrice)
		couponName = coupon.Name
		orderPrice -= couponDiscount
		if orderPrice < 0 {
			orderPrice = 0
		}
	}

	orderName := fmt.Sprintf("order_%v", util.GenerateTimeId())
	order := &Order{
		Owner:          owner,
		Name:           orderName,
		DisplayName:    orderName,
		CreatedTime:    util.GetCurrentTime(),
		Products:       productNames,
		ProductInfos:   productInfos,
		User:           user.Name,
		Payment:        "", // Payment will be set when user pays
		Price:          orderPrice,
		Currency:       orderCurrency,
		State:          "Created",
		Message:        "",
		UpdateTime:     "",
		CouponName:     couponName,
		CouponDiscount: couponDiscount,
	}

	affected, err := AddOrder(order)
	if err != nil {
		return nil, err
	}
	if !affected {
		return nil, fmt.Errorf("failed to add order: %s", util.StructToJson(order))
	}

	return order, nil
}

func PayOrder(providerName, host, paymentEnv string, order *Order, lang string) (payment *Payment, attachInfo map[string]interface{}, err error) {
	if order.State != "Created" {
		return nil, nil, fmt.Errorf("cannot pay for order: %s, current state is %s", order.GetId(), order.State)
	}
	productNames := order.Products
	products, err := getOrderProducts(order.Owner, productNames)
	if err != nil {
		return nil, nil, err
	}
	if len(products) == 0 {
		return nil, nil, fmt.Errorf("order has no products")
	}

	orderCurrency := order.Currency
	if orderCurrency == "" {
		orderCurrency = "USD"
	}

	if err := validateProductCurrencies(products, orderCurrency); err != nil {
		return nil, nil, err
	}

	user, err := GetUser(util.GetId(order.Owner, order.User))
	if err != nil {
		return nil, nil, err
	}
	if user == nil {
		return nil, nil, fmt.Errorf("the user: %s does not exist", order.User)
	}

	// For multi-product orders, the payment provider is determined by the first product
	baseProduct := products[0]
	provider, err := baseProduct.getProvider(providerName)
	if err != nil {
		return nil, nil, err
	}

	pProvider, err := GetPaymentProvider(provider)
	if err != nil {
		return nil, nil, err
	}

	owner := baseProduct.Owner
	payerName := fmt.Sprintf("%s | %s", user.Name, user.DisplayName)
	paymentName := fmt.Sprintf("payment_%v", util.GenerateTimeId())

	originFrontend, originBackend := getOriginFromHost(host)
	returnUrl := fmt.Sprintf("%s/payments/%s/%s/result", originFrontend, owner, paymentName)
	notifyUrl := fmt.Sprintf("%s/api/notify-payment/%s/%s", originBackend, owner, paymentName)

	orderProductInfos := order.ProductInfos
	// Create a subscription when pricing and plan are provided
	// This allows both free users and paid users to subscribe to plans
	for i, productInfo := range orderProductInfos {
		if productInfo.PricingName == "" || productInfo.PlanName == "" {
			continue
		}

		plan, err := GetPlan(util.GetId(owner, productInfo.PlanName))
		if err != nil {
			return nil, nil, err
		}
		if plan == nil {
			return nil, nil, fmt.Errorf("the plan: %s does not exist", productInfo.PlanName)
		}

		// Check if plan restricts user to one subscription
		if plan.IsExclusive {
			hasSubscription, err := HasActiveSubscriptionForPlan(owner, user.Name, plan.Name)
			if err != nil {
				return nil, nil, err
			}
			if hasSubscription {
				return nil, nil, fmt.Errorf("user already has an active subscription for plan: %s", plan.Name)
			}
		}

		sub, err := NewSubscription(owner, user.Name, plan.Name, paymentName, plan.Period)
		if err != nil {
			return nil, nil, err
		}

		affected, err := AddSubscription(sub)
		if err != nil {
			return nil, nil, err
		}
		if !affected {
			return nil, nil, fmt.Errorf("failed to add subscription: %s", sub.Name)
		}

		if i == 0 {
			returnUrl = fmt.Sprintf("%s/buy-plan/%s/%s/result?subscription=%s", originFrontend, owner, productInfo.PricingName, sub.Name)
		}
	}

	if baseProduct.SuccessUrl != "" {
		returnUrl = fmt.Sprintf("%s?transactionOwner=%s&transactionName=%s", baseProduct.SuccessUrl, owner, paymentName)
	}

	displayNames := make([]string, len(products))
	descriptions := make([]string, len(products))
	for i, p := range products {
		displayNames[i] = p.DisplayName
		descriptions[i] = p.Description
	}
	reqProductName := strings.Join(productNames, ", ")
	reqProductDisplayName := strings.Join(displayNames, ", ")
	reqProductDescription := strings.Join(descriptions, ", ")

	payReq := &pp.PayReq{
		ProviderName:       providerName,
		ProductName:        reqProductName,
		PayerName:          payerName,
		PayerId:            user.Id,
		PayerEmail:         user.Email,
		PaymentName:        paymentName,
		ProductDisplayName: reqProductDisplayName,
		ProductDescription: reqProductDescription,
		ProductImage:       baseProduct.Image,
		Price:              order.Price,
		Currency:           order.Currency,
		ReturnUrl:          returnUrl,
		NotifyUrl:          notifyUrl,
		PaymentEnv:         paymentEnv,
	}

	if provider.Type == "WeChat Pay" {
		payReq.PayerId, err = getUserExtraProperty(user, "WeChat", idp.BuildWechatOpenIdKey(provider.ClientId2))
		if err != nil {
			return nil, nil, err
		}
	} else if provider.Type == "Balance" {
		payReq.PayerId = user.GetId()
	}

	payResp, err := pProvider.Pay(payReq)
	if err != nil {
		return nil, nil, err
	}

	payment = &Payment{
		Owner:       baseProduct.Owner,
		Name:        paymentName,
		CreatedTime: util.GetCurrentTime(),
		DisplayName: paymentName,

		Provider: provider.Name,
		Type:     provider.Type,

		Products:            productNames,
		ProductsDisplayName: reqProductDisplayName,
		Detail:              reqProductDescription,
		Currency:            order.Currency,
		Price:               order.Price,

		User:       user.Name,
		Order:      order.Name,
		PayUrl:     payResp.PayUrl,
		SuccessUrl: returnUrl,
		State:      pp.PaymentStateCreated,
		OutOrderId: payResp.OrderId,
	}

	if provider.Type == "Balance" {
		payment.State = pp.PaymentStatePaid
	}

	affected, err := AddPayment(payment)
	if err != nil {
		return nil, nil, err
	}

	if !affected {
		return nil, nil, fmt.Errorf("failed to add payment: %s", util.StructToJson(payment))
	}

	if provider.Type == "Balance" {
		transaction := &Transaction{
			Owner:       payment.Owner,
			CreatedTime: util.GetCurrentTime(),
			Application: user.SignupApplication,
			Amount:      -payment.Price,
			Currency:    order.Currency,
			Payment:     payment.Name,
			Category:    TransactionCategoryPurchase,
			Type:        provider.Category,
			Subtype:     provider.Type,
			Provider:    provider.Name,
			Tag:         "User",
			User:        payment.User,
			State:       string(pp.PaymentStatePaid),
		}

		affected, err = AddInternalPaymentTransaction(transaction, lang)
		if err != nil {
			return nil, nil, err
		}
		if !affected {
			return nil, nil, fmt.Errorf("failed to add transaction: %s", util.StructToJson(transaction))
		}

		hasRecharge := false
		rechargeAmount := 0.0
		for _, productInfo := range orderProductInfos {
			if productInfo.IsRecharge {
				hasRecharge = true
				rechargeAmount += productInfo.Price * float64(productInfo.Quantity)
			}
		}

		if hasRecharge {
			rechargeTransaction := &Transaction{
				Owner:       payment.Owner,
				CreatedTime: util.GetCurrentTime(),
				Application: user.SignupApplication,
				Amount:      rechargeAmount,
				Currency:    order.Currency,
				Payment:     payment.Name,
				Category:    TransactionCategoryRecharge,
				Type:        provider.Category,
				Subtype:     provider.Type,
				Provider:    provider.Name,
				Tag:         "User",
				User:        payment.User,
				State:       string(pp.PaymentStatePaid),
			}

			affected, err = AddInternalPaymentTransaction(rechargeTransaction, lang)
			if err != nil {
				return nil, nil, err
			}
			if !affected {
				return nil, nil, fmt.Errorf("failed to add recharge transaction: %s", util.StructToJson(rechargeTransaction))
			}
		}
	}

	order.Payment = payment.Name
	if provider.Type == "Balance" {
		order.State = "Paid"
		order.Message = "Payment successful"
		order.UpdateTime = util.GetCurrentTime()
	}

	// Update order state first to avoid inconsistency
	_, err = UpdateOrder(order.GetId(), order)
	if err != nil {
		return nil, nil, err
	}

	// Update product stock after order state is persisted (for instant payment methods)
	if provider.Type == "Balance" {
		err = UpdateProductStock(orderProductInfos)
		if err != nil {
			return nil, nil, err
		}

		// Record coupon usage after successful balance payment
		if order.CouponName != "" {
			if err = ApplyCoupon(order.Owner, order.CouponName, order.User, order.Name, order.CouponDiscount); err != nil {
				logs.Warning(fmt.Sprintf("PayOrder: failed to record coupon usage for order %s: %v", order.Name, err))
			}
		}
	}

	return payment, payResp.AttachInfo, nil
}

func CancelOrder(order *Order) (bool, error) {
	if order.State != "Created" {
		return false, fmt.Errorf("cannot cancel order in state: %s", order.State)
	}

	order.State = "Canceled"
	order.Message = "Canceled by user"
	order.UpdateTime = util.GetCurrentTime()
	return UpdateOrder(order.GetId(), order)
}
