package commerce

import (
	"context"
	"crypto/md5"
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"math"
	"net/http"
	"net/url"
	"os"
	"sort"
	"strconv"
	"strings"
	"time"

	"gorm.io/gorm"
	"gorm.io/gorm/clause"

	"qycw/go-server/internal/plugins"
)

type RechargeCreateRequest struct {
	Amount        any     `json:"amount"`
	PaymentPlugin *string `json:"payment_plugin"`
	PaymentMethod *string `json:"payment_method"`
}

type RechargeNotifyRequest struct {
	PluginID string
	Method   string
	Headers  map[string]string
	Params   map[string]string
	Body     string
}

func (s *Service) RechargePaymentOptions(ctx context.Context) ([]map[string]any, error) {
	out := []map[string]any{}
	if s.plugins != nil {
		manifests, err := s.plugins.List(ctx, plugins.CapabilityPayment)
		if err != nil {
			return nil, err
		}
		selection, err := s.adminPluginSelection(ctx)
		if err != nil {
			return nil, err
		}
		for _, manifest := range manifests {
			if !selection.paymentEnabled[manifest.ID] {
				continue
			}
			out = append(out, map[string]any{
				"id":            manifest.ID,
				"name":          manifest.Name,
				"description":   manifest.Description,
				"method":        paymentMethodFromPluginID(manifest.ID),
				"method_label":  manifest.Name,
				"capabilities":  []string{"payurl", "qrcode"},
				"plugin":        true,
				"config_schema": manifest.ConfigSchema,
			})
		}
	}
	if epayConfigured() {
		out = append(out,
			map[string]any{"id": "epay_alipay", "name": "Alipay", "description": "Built-in Epay Alipay channel", "method": "alipay", "method_label": "Alipay", "capabilities": []string{"payurl", "qrcode"}, "plugin": false},
			map[string]any{"id": "epay_wxpay", "name": "WeChat Pay", "description": "Built-in Epay WeChat channel", "method": "wxpay", "method_label": "WeChat Pay", "capabilities": []string{"payurl", "qrcode"}, "plugin": false},
			map[string]any{"id": "epay_qqpay", "name": "QQ Wallet", "description": "Built-in Epay QQ wallet channel", "method": "qqpay", "method_label": "QQ Wallet", "capabilities": []string{"payurl", "qrcode"}, "plugin": false},
		)
	}
	return out, nil
}

func (s *Service) CreateRecharge(ctx context.Context, userID int64, req RechargeCreateRequest, ip string) (map[string]any, error) {
	if err := s.requireDB(); err != nil {
		return nil, err
	}
	amountCents, amountYuan, err := parseRechargeAmount(req.Amount)
	if err != nil {
		return nil, err
	}
	minCents := s.intSetting(ctx, "finance.recharge_min_cents", 1)
	maxCents := s.intSetting(ctx, "finance.recharge_max_cents", 100000000)
	if amountCents < minCents {
		return nil, BadRequest(fmt.Sprintf("Minimum recharge amount is %.2f", float64(minCents)/100))
	}
	if amountCents > maxCents {
		return nil, BadRequest(fmt.Sprintf("Maximum recharge amount is %.2f", float64(maxCents)/100))
	}

	method := "alipay"
	if req.PaymentMethod != nil && strings.TrimSpace(*req.PaymentMethod) != "" {
		method = strings.TrimSpace(*req.PaymentMethod)
	}
	plugin := "epay_" + method
	if req.PaymentPlugin != nil && strings.TrimSpace(*req.PaymentPlugin) != "" {
		plugin = strings.TrimSpace(*req.PaymentPlugin)
	}
	order := RechargeOrder{
		UserID:        userID,
		OutTradeNo:    generateRechargeNo(),
		PaymentPlugin: &plugin,
		PaymentMethod: &method,
		Channel:       channelFromMethod(method),
		Amount:        amountYuan,
		AmountCents:   amountCents,
		Status:        "pending",
		ClientIP:      stringPtr(ip),
	}
	if err := s.db.WithContext(ctx).Create(&order).Error; err != nil {
		return nil, err
	}

	payment := map[string]any{}
	if s.paymentPluginAvailable(ctx, plugin) {
		resp, err := s.plugins.CreatePayment(ctx, plugin, plugins.PaymentCreateRequest{
			OutTradeNo:  order.OutTradeNo,
			AmountCents: amountCents,
			Amount:      fmt.Sprintf("%.2f", amountYuan),
			Subject:     "Balance recharge",
			Method:      method,
			ClientIP:    ip,
			NotifyURL:   s.paymentURLSetting(ctx, "payment.notify_url", os.Getenv("EPAY_NOTIFY_URL")),
			ReturnURL:   s.paymentURLSetting(ctx, "payment.return_url", os.Getenv("EPAY_RETURN_URL")),
			UserID:      userID,
			Metadata:    map[string]any{"recharge_order_id": order.ID},
			Config:      s.paymentConfig(ctx, plugin),
		})
		if err != nil {
			return nil, ServiceUnavailable("payment plugin create failed: " + err.Error())
		}
		if !resp.OK {
			message := strings.TrimSpace(resp.Message)
			if message == "" {
				message = "payment plugin create failed"
			}
			return nil, ServiceUnavailable(message)
		}
		payment = paymentCreateMap(resp)
	} else if epayConfigured() {
		payment = createEpayPayment(order, method, ip)
	}
	if tradeNo := strings.TrimSpace(toString(payment["trade_no"])); tradeNo != "" {
		order.TradeNo = &tradeNo
		_ = s.db.WithContext(ctx).Save(&order).Error
	}
	return rechargeOrderMap(order, payment), nil
}

func (s *Service) PendingRecharge(ctx context.Context, userID int64) (any, error) {
	if err := s.requireDB(); err != nil {
		return nil, err
	}
	var order RechargeOrder
	err := s.db.WithContext(ctx).Where("user_id = ? AND status = ?", userID, "pending").Order("id DESC").First(&order).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	data := rechargeOrderMap(order, nil)
	data["expires_at"] = order.CreatedAt.Add(30 * time.Minute)
	return data, nil
}

func (s *Service) RechargeStatus(ctx context.Context, userID int64, outTradeNo string) (map[string]any, error) {
	if err := s.requireDB(); err != nil {
		return nil, err
	}
	var order RechargeOrder
	err := s.db.WithContext(ctx).Where("user_id = ? AND out_trade_no = ?", userID, outTradeNo).First(&order).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, NotFound("recharge order not found")
	}
	if err != nil {
		return nil, err
	}
	var user UserAccount
	_ = s.db.WithContext(ctx).Where("id = ?", userID).First(&user).Error
	return map[string]any{
		"out_trade_no": order.OutTradeNo,
		"status":       order.Status,
		"paid":         order.Status == "paid",
		"balance": func() any {
			if order.Status == "paid" {
				return user.Balance
			}
			return nil
		}(),
	}, nil
}

func (s *Service) ListRechargeOrders(ctx context.Context, userID int64, limit int, offset int) (map[string]any, error) {
	if err := s.requireDB(); err != nil {
		return nil, err
	}
	db := s.db.WithContext(ctx).Model(&RechargeOrder{}).Where("user_id = ?", userID)
	var total int64
	if err := db.Count(&total).Error; err != nil {
		return nil, err
	}
	var orders []RechargeOrder
	if err := db.Order("id DESC").Limit(limit).Offset(offset).Find(&orders).Error; err != nil {
		return nil, err
	}
	items := make([]map[string]any, 0, len(orders))
	for _, order := range orders {
		items = append(items, rechargeOrderItemMap(order))
	}
	return map[string]any{"items": items, "total": total}, nil
}

func (s *Service) HandleRechargeNotify(ctx context.Context, req RechargeNotifyRequest) (map[string]any, error) {
	if err := s.requireDB(); err != nil {
		return nil, err
	}
	params := req.Params
	if params == nil {
		params = map[string]string{}
	}
	pluginID := strings.TrimSpace(firstString(req.PluginID, params["payment_plugin"], params["plugin"]))
	if pluginID == "" {
		pluginID = s.paymentPluginForNotify(ctx, params)
	}
	if pluginID != "" && s.paymentPluginAvailable(ctx, pluginID) {
		resp, err := s.plugins.HandlePaymentNotify(ctx, pluginID, plugins.PaymentNotifyRequest{
			Method:  req.Method,
			Headers: req.Headers,
			Params:  params,
			Body:    req.Body,
			Config:  s.paymentConfig(ctx, pluginID),
		})
		if err != nil {
			return nil, BadRequest("invalid payment callback: " + err.Error())
		}
		if !resp.OK {
			message := strings.TrimSpace(resp.Message)
			if message == "" {
				message = "payment callback rejected"
			}
			return nil, BadRequest(message)
		}
		if resp.Ignored || !resp.Paid {
			return map[string]any{"ok": false, "status": "ignored"}, nil
		}
		if resp.AmountCents <= 0 {
			return nil, BadRequest("payment callback amount is required")
		}
		outTradeNo := strings.TrimSpace(resp.OutTradeNo)
		if outTradeNo == "" {
			return nil, BadRequest("missing out_trade_no")
		}
		normalized := map[string]string{}
		for key, value := range params {
			normalized[key] = value
		}
		if resp.TradeNo != "" {
			normalized["trade_no"] = resp.TradeNo
		}
		normalized["amount_cents"] = strconv.Itoa(resp.AmountCents)
		normalized["payment_plugin"] = pluginID
		normalized["payment_method"] = strings.TrimSpace(firstString(params["payment_method"], params["type"], paymentMethodFromPluginID(pluginID)))
		if resp.Raw != nil {
			if raw, err := json.Marshal(resp.Raw); err == nil {
				normalized["plugin_raw"] = string(raw)
			}
		}
		err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
			txService := &Service{db: tx, logger: s.logger, plugins: s.plugins}
			return txService.processRechargePaid(ctx, outTradeNo, normalized)
		})
		if err != nil {
			return nil, err
		}
		return map[string]any{"ok": true}, nil
	}
	if !verifyEpaySign(params, os.Getenv("EPAY_KEY")) {
		return nil, BadRequest("invalid payment callback signature")
	}
	if strings.TrimSpace(params["pid"]) == "" || strings.TrimSpace(params["pid"]) != strings.TrimSpace(os.Getenv("EPAY_PID")) {
		return nil, BadRequest("payment callback merchant mismatch")
	}
	outTradeNo := strings.TrimSpace(params["out_trade_no"])
	if outTradeNo == "" {
		return nil, BadRequest("missing out_trade_no")
	}
	tradeStatus := strings.ToUpper(strings.TrimSpace(firstString(params["trade_status"], params["status"])))
	if tradeStatus != "" && tradeStatus != "TRADE_SUCCESS" && tradeStatus != "SUCCESS" && tradeStatus != "PAID" {
		return map[string]any{"ok": false, "status": "ignored"}, nil
	}
	amountCents, err := parseDecimalCents(params["money"])
	if err != nil || amountCents <= 0 {
		return nil, BadRequest("invalid payment callback amount")
	}
	normalized := make(map[string]string, len(params)+4)
	for key, value := range params {
		normalized[key] = value
	}
	normalized["amount_cents"] = strconv.Itoa(amountCents)
	normalized["merchant_id"] = strings.TrimSpace(params["pid"])
	normalized["payment_method"] = strings.TrimSpace(params["type"])
	if normalized["payment_method"] != "" {
		normalized["payment_plugin"] = "epay_" + normalized["payment_method"]
	}
	err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		txService := &Service{db: tx, logger: s.logger, plugins: s.plugins}
		return txService.processRechargePaid(ctx, outTradeNo, normalized)
	})
	if err != nil {
		return nil, err
	}
	return map[string]any{"ok": true}, nil
}

func (s *Service) processRechargePaid(ctx context.Context, outTradeNo string, params map[string]string) error {
	var order RechargeOrder
	if err := s.db.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("out_trade_no = ?", outTradeNo).First(&order).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return NotFound("recharge order not found")
		}
		return err
	}
	if order.Status == "paid" {
		return nil
	}
	if order.Status != "pending" {
		return BadRequest("recharge order status does not allow posting")
	}
	centsText := strings.TrimSpace(params["amount_cents"])
	cents, err := strconv.Atoi(centsText)
	if centsText == "" || err != nil || cents <= 0 || cents != order.AmountCents {
		return BadRequest("payment callback amount mismatch")
	}
	if order.PaymentPlugin != nil {
		callbackPlugin := strings.TrimSpace(params["payment_plugin"])
		if callbackPlugin == "" || callbackPlugin != strings.TrimSpace(*order.PaymentPlugin) {
			return BadRequest("payment callback plugin mismatch")
		}
	}
	if order.PaymentMethod != nil {
		callbackMethod := strings.TrimSpace(firstString(params["payment_method"], params["type"]))
		if callbackMethod == "" || callbackMethod != strings.TrimSpace(*order.PaymentMethod) {
			return BadRequest("payment callback method mismatch")
		}
	}

	var user UserAccount
	if err := s.db.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", order.UserID).First(&user).Error; err != nil {
		return err
	}
	balanceAfter := user.Balance + order.AmountCents
	if err := s.db.WithContext(ctx).Model(&UserAccount{}).Where("id = ?", order.UserID).Update("balance", balanceAfter).Error; err != nil {
		return err
	}

	raw, _ := json.Marshal(params)
	rawText := string(raw)
	tradeNo := strings.TrimSpace(params["trade_no"])
	order.Status = "paid"
	order.CallbackRaw = &rawText
	if tradeNo != "" {
		order.TradeNo = &tradeNo
	}
	if err := s.db.WithContext(ctx).Save(&order).Error; err != nil {
		return err
	}

	remark := fmt.Sprintf("User recharge %.2f", order.Amount)
	return s.db.WithContext(ctx).Create(&BalanceLog{
		UserID:       order.UserID,
		ChangeType:   "recharge",
		Delta:        order.AmountCents,
		BalanceAfter: balanceAfter,
		RefNo:        &order.OutTradeNo,
		Remark:       &remark,
	}).Error
}

func (s *Service) intSetting(ctx context.Context, key string, fallback int) int {
	if s.db == nil {
		return fallback
	}
	var setting SystemSetting
	err := s.db.WithContext(ctx).Where("`key` = ?", key).First(&setting).Error
	raw := strings.TrimSpace(string(setting.Value))
	if err != nil || raw == "" {
		return fallback
	}
	raw = strings.Trim(raw, `"`)
	value, err := strconv.Atoi(raw)
	if err != nil {
		return fallback
	}
	return value
}

func parseRechargeAmount(value any) (int, float64, error) {
	text := strings.TrimSpace(toString(value))
	amount, err := strconv.ParseFloat(text, 64)
	if err != nil || amount <= 0 {
		return 0, 0, BadRequest("recharge amount must be greater than 0")
	}
	cents := int(math.Round(amount * 100))
	return cents, float64(cents) / 100, nil
}

func generateRechargeNo() string {
	var raw [3]byte
	_, _ = rand.Read(raw[:])
	return fmt.Sprintf("RC%d%s", time.Now().UnixMilli(), strings.ToUpper(hex.EncodeToString(raw[:])))
}

func channelFromMethod(method string) string {
	switch method {
	case "wxpay", "qqpay":
		return method
	default:
		return "alipay"
	}
}

func paymentMethodFromPluginID(pluginID string) string {
	pluginID = strings.TrimSpace(pluginID)
	for _, prefix := range []string{"epay_", "payment_", "pay_"} {
		if strings.HasPrefix(pluginID, prefix) {
			return strings.TrimPrefix(pluginID, prefix)
		}
	}
	return pluginID
}

func (s *Service) paymentPluginAvailable(ctx context.Context, pluginID string) bool {
	if s.plugins == nil || strings.TrimSpace(pluginID) == "" {
		return false
	}
	manifest, err := s.plugins.Manifest(ctx, pluginID)
	if err != nil || !manifest.Enabled || !manifest.HasCapability(plugins.CapabilityPayment) {
		return false
	}
	selection, err := s.adminPluginSelection(ctx)
	return err == nil && selection.paymentEnabled[pluginID]
}

func (s *Service) paymentPluginForNotify(ctx context.Context, params map[string]string) string {
	outTradeNo := strings.TrimSpace(params["out_trade_no"])
	if outTradeNo == "" || s.db == nil {
		return ""
	}
	var order RechargeOrder
	if err := s.db.WithContext(ctx).Where("out_trade_no = ?", outTradeNo).First(&order).Error; err != nil {
		return ""
	}
	if order.PaymentPlugin == nil {
		return ""
	}
	return strings.TrimSpace(*order.PaymentPlugin)
}

func (s *Service) paymentURLSetting(ctx context.Context, key string, fallback string) string {
	if s.db == nil {
		return fallback
	}
	var item SystemSetting
	if err := s.db.WithContext(ctx).Where("`key` = ?", key).First(&item).Error; err != nil {
		return fallback
	}
	text := settingStringCommerce(item.Value, fallback)
	if strings.TrimSpace(text) == "" {
		return fallback
	}
	return text
}

func (s *Service) paymentConfig(ctx context.Context, pluginID string) map[string]any {
	config := map[string]any{
		"plugin_id":  pluginID,
		"notify_url": s.paymentURLSetting(ctx, "payment.notify_url", os.Getenv("EPAY_NOTIFY_URL")),
		"return_url": s.paymentURLSetting(ctx, "payment.return_url", os.Getenv("EPAY_RETURN_URL")),
	}
	if strings.HasPrefix(pluginID, "epay_") {
		config["api_url"] = os.Getenv("EPAY_API_URL")
		config["pid"] = os.Getenv("EPAY_PID")
		config["key"] = os.Getenv("EPAY_KEY")
	}
	if s.db == nil {
		return config
	}
	prefix := "payment." + pluginID + "."
	var settings []SystemSetting
	if err := s.db.WithContext(ctx).Where("`key` LIKE ?", prefix+"%").Find(&settings).Error; err != nil {
		return config
	}
	for _, item := range settings {
		key := strings.TrimPrefix(item.Key, prefix)
		var value any
		if err := json.Unmarshal(item.Value, &value); err == nil {
			config[key] = value
		} else {
			config[key] = strings.Trim(strings.TrimSpace(string(item.Value)), `"`)
		}
	}
	if s.plugins != nil {
		config = s.plugins.MergeConfig(ctx, pluginID, config)
	}
	return config
}

func paymentCreateMap(resp plugins.PaymentCreateResponse) map[string]any {
	out := map[string]any{}
	if resp.TradeNo != "" {
		out["trade_no"] = resp.TradeNo
	}
	if resp.PayURL != "" {
		out["payurl"] = resp.PayURL
	}
	if resp.QRCode != "" {
		out["qrcode"] = resp.QRCode
	}
	if resp.URLScheme != "" {
		out["urlscheme"] = resp.URLScheme
	}
	if resp.Raw != nil {
		for key, value := range resp.Raw {
			if out[key] == nil {
				out[key] = value
			}
		}
	}
	return out
}

func epayConfigured() bool {
	return os.Getenv("EPAY_API_URL") != "" && os.Getenv("EPAY_PID") != "" && os.Getenv("EPAY_KEY") != ""
}

func createEpayPayment(order RechargeOrder, method string, ip string) map[string]any {
	params := map[string]string{
		"pid":          os.Getenv("EPAY_PID"),
		"type":         method,
		"out_trade_no": order.OutTradeNo,
		"notify_url":   os.Getenv("EPAY_NOTIFY_URL"),
		"return_url":   os.Getenv("EPAY_RETURN_URL"),
		"name":         "Balance recharge",
		"money":        fmt.Sprintf("%.2f", order.Amount),
		"clientip":     ip,
		"device":       "pc",
	}
	params["sign"] = epaySign(params, os.Getenv("EPAY_KEY"))
	params["sign_type"] = "MD5"
	resp, err := http.PostForm(strings.TrimRight(os.Getenv("EPAY_API_URL"), "/")+"/xpay/epay/mapi.php", mapToValues(params))
	if err != nil {
		return map[string]any{"error": err.Error()}
	}
	defer resp.Body.Close()
	raw, _ := io.ReadAll(resp.Body)
	var data map[string]any
	if err := json.Unmarshal(raw, &data); err != nil {
		return map[string]any{"raw": string(raw)}
	}
	return data
}

func epaySign(params map[string]string, key string) string {
	keys := make([]string, 0, len(params))
	for k, v := range params {
		if v == "" || k == "sign" || k == "sign_type" {
			continue
		}
		keys = append(keys, k)
	}
	sort.Strings(keys)
	parts := make([]string, 0, len(keys))
	for _, k := range keys {
		parts = append(parts, k+"="+params[k])
	}
	sum := md5.Sum([]byte(strings.Join(parts, "&") + key))
	return hex.EncodeToString(sum[:])
}

func verifyEpaySign(params map[string]string, key string) bool {
	if strings.TrimSpace(key) == "" {
		return false
	}
	return strings.EqualFold(params["sign"], epaySign(params, key))
}

func parseDecimalCents(value string) (int, error) {
	value = strings.TrimSpace(value)
	if value == "" {
		return 0, fmt.Errorf("amount is empty")
	}
	if value == "" || strings.HasPrefix(value, "+") || strings.HasPrefix(value, "-") {
		return 0, fmt.Errorf("amount must be positive")
	}
	parts := strings.Split(value, ".")
	if len(parts) > 2 || parts[0] == "" || len(parts) == 2 && (len(parts[1]) == 0 || len(parts[1]) > 2) {
		return 0, fmt.Errorf("amount precision is invalid")
	}
	for _, part := range parts {
		for _, char := range part {
			if char < '0' || char > '9' {
				return 0, fmt.Errorf("amount contains non-digits")
			}
		}
	}
	yuan, err := strconv.ParseInt(parts[0], 10, 64)
	if err != nil {
		return 0, err
	}
	fraction := int64(0)
	if len(parts) == 2 && parts[1] != "" {
		fraction, err = strconv.ParseInt(parts[1], 10, 64)
		if err != nil {
			return 0, err
		}
		if len(parts[1]) == 1 {
			fraction *= 10
		}
	}
	if yuan > int64(^uint(0)>>1)/100 {
		return 0, fmt.Errorf("amount is too large")
	}
	cents := yuan*100 + fraction
	if cents > int64(^uint(0)>>1) {
		return 0, fmt.Errorf("amount is too large")
	}
	if cents <= 0 {
		return 0, fmt.Errorf("amount must be positive")
	}
	return int(cents), nil
}

func mapToValues(params map[string]string) url.Values {
	values := url.Values{}
	for key, value := range params {
		values.Set(key, value)
	}
	return values
}

func rechargeOrderMap(order RechargeOrder, payment map[string]any) map[string]any {
	out := rechargeOrderItemMap(order)
	out["payment_plugin"] = order.PaymentPlugin
	out["payment_method"] = order.PaymentMethod
	out["payment_name"] = "Epay"
	for _, key := range []string{"payurl", "qrcode", "urlscheme", "trade_no"} {
		if payment != nil && payment[key] != nil {
			out[key] = payment[key]
		}
	}
	return out
}

func rechargeOrderItemMap(order RechargeOrder) map[string]any {
	return map[string]any{
		"id":             order.ID,
		"user_id":        order.UserID,
		"out_trade_no":   order.OutTradeNo,
		"trade_no":       order.TradeNo,
		"channel":        order.Channel,
		"payment_plugin": order.PaymentPlugin,
		"payment_method": order.PaymentMethod,
		"amount":         fmt.Sprintf("%.2f", order.Amount),
		"amount_cents":   order.AmountCents,
		"status":         order.Status,
		"remark":         order.Remark,
		"created_at":     order.CreatedAt,
		"updated_at":     order.UpdatedAt,
	}
}

func toString(value any) string {
	switch v := value.(type) {
	case nil:
		return ""
	case string:
		return v
	case fmt.Stringer:
		return v.String()
	default:
		return fmt.Sprint(v)
	}
}
