package jobs

import (
	"context"
	"crypto/hmac"
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"log/slog"
	"math/big"
	"os"
	"strconv"
	"strings"
	"time"

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

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

const genericBlankPluginID = "generic_blank"

var errProvisionPending = errors.New("provision pending")

type ProvisionWorker struct {
	db          *gorm.DB
	logger      *slog.Logger
	interval    time.Duration
	batchSize   int
	maxAttempts int
	plugins     *plugins.Service
	license     workerLicenseGate
}

func NewProvisionWorker(db *gorm.DB, logger *slog.Logger, interval time.Duration, batchSize int, maxAttempts int, pluginSvc ...*plugins.Service) *ProvisionWorker {
	if batchSize <= 0 {
		batchSize = 20
	}
	if interval <= 0 {
		interval = 30 * time.Second
	}
	if maxAttempts <= 0 {
		maxAttempts = 3
	}
	var pluginsService *plugins.Service
	if len(pluginSvc) > 0 {
		pluginsService = pluginSvc[0]
	}
	return &ProvisionWorker{db: db, logger: logger, interval: interval, batchSize: batchSize, maxAttempts: maxAttempts, plugins: pluginsService}
}

func (w *ProvisionWorker) Run(ctx context.Context) error {
	if w.db == nil {
		w.logger.Warn("provision worker disabled: database is not configured")
		<-ctx.Done()
		return nil
	}

	w.logger.Info("provision worker started", "interval", w.interval, "batch_size", w.batchSize)
	if err := w.ProcessOnce(ctx); err != nil {
		w.logger.Error("provision worker initial scan failed", "error", err)
	}

	ticker := time.NewTicker(w.interval)
	defer ticker.Stop()
	for {
		select {
		case <-ctx.Done():
			w.logger.Info("provision worker stopped")
			return nil
		case <-ticker.C:
			if err := w.ProcessOnce(ctx); err != nil {
				w.logger.Error("provision worker scan failed", "error", err)
			}
		}
	}
}

func (w *ProvisionWorker) ProcessOnce(ctx context.Context) error {
	if err := w.license.check(); err != nil {
		return err
	}
	cutoff := time.Now().UTC().Add(-w.interval)
	var instances []commerce.Instance
	if err := w.db.WithContext(ctx).
		Where("status = ?", "provisioning").
		Where("(last_provision_at IS NULL OR last_provision_at < ?)", cutoff).
		Order("id ASC").
		Limit(w.batchSize).
		Find(&instances).Error; err != nil {
		return err
	}
	for _, instance := range instances {
		if err := w.processInstance(ctx, instance.ID); err != nil {
			w.logger.Error("provision instance failed", "instance_id", instance.ID, "error", err)
		}
	}
	return nil
}

func (w *ProvisionWorker) processInstance(ctx context.Context, instanceID int64) error {
	return w.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var instance commerce.Instance
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
			Where("id = ?", instanceID).
			First(&instance).Error; err != nil {
			return err
		}
		if instance.Status != "provisioning" {
			return nil
		}

		now := time.Now().UTC()
		instance.ProvisionAttempts++
		instance.LastProvisionAt = &now

		var product commerce.Product
		if err := tx.Where("id = ?", instance.ProductID).First(&product).Error; err != nil {
			message := fmt.Sprintf("Go provision worker: product %d not found: %v", instance.ProductID, err)
			instance.Status = "provision_failed"
			instance.ProvisionLog = &message
			if err := tx.Save(&instance).Error; err != nil {
				return err
			}
			return commerce.NewService(tx, nil, w.logger, w.plugins).CompensateProvisionFailureTx(ctx, instance.ID, message)
		}

		if !product.AutoProvision {
			message := "Go provision worker: product auto_provision=false, waiting for manual provisioning"
			instance.ProvisionLog = &message
			return tx.Save(&instance).Error
		}

		apiType := strings.TrimSpace(product.APIType)
		switch apiType {
		case "plugin":
			pluginID := ""
			if product.APIIdentifier != nil {
				pluginID = strings.TrimSpace(*product.APIIdentifier)
			}
			if pluginID == genericBlankPluginID {
				applyGenericBlankProvision(&instance, product, now)
				if err := tx.Save(&instance).Error; err != nil {
					return err
				}
				return w.afterProvisionSuccess(ctx, tx, &instance)
			}
			result, err := w.callProvisionPlugin(ctx, tx, instance, product, pluginID)
			if err != nil {
				return w.applyProvisionError(tx, &instance, err.Error(), true)
			}
			if !result.OK {
				return w.applyProvisionError(tx, &instance, result.Message, result.Retryable)
			}
			if err := applyPluginProvisionResult(&instance, pluginID, result.Result, now); err != nil {
				return err
			}
			if err := tx.Save(&instance).Error; err != nil {
				return err
			}
			return w.afterProvisionSuccess(ctx, tx, &instance)
		case "", "none":
			message := "Go provision worker: product api_type has no automatic provider; waiting for manual provisioning"
			instance.ProvisionLog = &message
			return tx.Save(&instance).Error
		case "dcim_cloud_image", "dcim_cloud_rid":
			if err := w.provisionDCIM(ctx, tx, &instance, product, now); err != nil {
				return w.applyProvisionError(tx, &instance, err.Error(), true)
			}
			if err := tx.Save(&instance).Error; err != nil {
				return err
			}
			return w.afterProvisionSuccess(ctx, tx, &instance)
		case "zjmf_finance":
			if err := w.provisionZJMF(ctx, tx, &instance, product, now); err != nil {
				if errors.Is(err, errProvisionPending) {
					return tx.Save(&instance).Error
				}
				return w.applyProvisionError(tx, &instance, err.Error(), true)
			}
			if err := tx.Save(&instance).Error; err != nil {
				return err
			}
			return w.afterProvisionSuccess(ctx, tx, &instance)
		default:
			result, pluginID, err := w.callProvisionPluginFallback(ctx, tx, instance, product, apiType)
			if err != nil {
				return w.applyProvisionError(tx, &instance, err.Error(), false)
			}
			if !result.OK {
				return w.applyProvisionError(tx, &instance, result.Message, result.Retryable)
			}
			if err := applyPluginProvisionResult(&instance, pluginID, result.Result, now); err != nil {
				return err
			}
			if err := tx.Save(&instance).Error; err != nil {
				return err
			}
			return w.afterProvisionSuccess(ctx, tx, &instance)
		}
	})
}

func (w *ProvisionWorker) provisionDCIM(ctx context.Context, tx *gorm.DB, instance *commerce.Instance, product commerce.Product, now time.Time) error {
	if instance.CloudServerID == nil {
		return fmt.Errorf("DCIM 开通失败：实例未绑定云节点")
	}
	var server commerce.CloudServer
	if err := tx.WithContext(ctx).Where("id = ?", *instance.CloudServerID).First(&server).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return fmt.Errorf("DCIM 开通失败：云节点不存在")
		}
		return err
	}
	if server.Disabled {
		return fmt.Errorf("DCIM 开通失败：云节点已禁用")
	}
	if server.ServerType == "zjmf_finance" {
		return fmt.Errorf("DCIM 开通失败：云节点类型为 ZJMF")
	}

	var order commerce.Order
	if err := tx.WithContext(ctx).Where("id = ?", instance.OrderID).First(&order).Error; err != nil {
		return err
	}
	snapshot := snapshotForInstance(order.ItemsSnapshot, *instance)

	if instance.UpstreamID == nil {
		params := buildDCIMProvisionParams(*instance, product, server, snapshot)
		if err := applyDCIMProductGroupLocation(ctx, tx, product, params); err != nil {
			return err
		}
		result, err := commerce.CallDCIMPath(ctx, server, "/clouds", "POST", params, 45*time.Second)
		if err != nil {
			return fmt.Errorf("调用魔方云开通失败：%w", err)
		}
		if result.StatusCode < 200 || result.StatusCode >= 300 {
			return fmt.Errorf("魔方云开通返回异常（HTTP %d）：%s", result.StatusCode, trimJobText(result.Text, 200))
		}
		upstreamID, ok := int64Value(firstNonNilValue(result.Payload["id"], result.Body["id"]))
		if !ok || upstreamID <= 0 {
			return fmt.Errorf("魔方云未返回实例 ID：%s", trimJobText(result.Text, 200))
		}
		instance.UpstreamID = &upstreamID
		if result.TaskID != nil {
			instance.UpstreamTaskID = result.TaskID
		}
		instance.UpstreamPayload = jsonRaw(map[string]any{
			"provision_provider": "dcim_cloud",
			"create_result":      result.Body,
			"params":             params,
			"created_at":         now.Format(time.RFC3339),
		})
		if err := tx.WithContext(ctx).Save(instance).Error; err != nil {
			return err
		}
	}

	if instance.UpstreamTaskID != nil {
		if err := w.pollDCIMTask(ctx, server, *instance.UpstreamTaskID); err != nil {
			return err
		}
	}
	if instance.UpstreamID == nil {
		return fmt.Errorf("DCIM 开通失败：缺少上游实例 ID")
	}
	result, err := commerce.CallDCIMPath(ctx, server, fmt.Sprintf("/clouds/%d", *instance.UpstreamID), "GET", nil, 20*time.Second)
	if err != nil {
		return fmt.Errorf("获取魔方云实例详情失败：%w", err)
	}
	if result.StatusCode < 200 || result.StatusCode >= 300 {
		return fmt.Errorf("获取魔方云实例详情异常（HTTP %d）：%s", result.StatusCode, trimJobText(result.Text, 200))
	}
	detail := result.Payload
	if len(detail) == 0 {
		detail = result.Body
	}
	applyDCIMProvisionDetail(instance, detail, now)
	message := fmt.Sprintf("开通成功 upstream_id=%d", *instance.UpstreamID)
	instance.ProvisionLog = &message
	instance.Status = "active"
	instance.UpstreamPayload = jsonRaw(map[string]any{
		"provision_provider": "dcim_cloud",
		"upstream_id":        *instance.UpstreamID,
		"task_id":            instance.UpstreamTaskID,
		"detail":             detail,
		"completed_at":       now.Format(time.RFC3339),
	})
	return nil
}

func (w *ProvisionWorker) pollDCIMTask(ctx context.Context, server commerce.CloudServer, taskID int64) error {
	deadline := time.Now().Add(120 * time.Second)
	for time.Now().Before(deadline) {
		select {
		case <-ctx.Done():
			return ctx.Err()
		case <-time.After(3 * time.Second):
		}
		result, err := commerce.CallDCIMPath(ctx, server, fmt.Sprintf("/tasks/%d", taskID), "GET", nil, 8*time.Second)
		if err != nil {
			if w.logger != nil {
				w.logger.Warn("poll dcim task failed", "task_id", taskID, "error", err)
			}
			continue
		}
		if result.StatusCode < 200 || result.StatusCode >= 300 {
			continue
		}
		status := dcimTaskStatus(result.Payload)
		switch status {
		case "success":
			return nil
		case "failed":
			return fmt.Errorf("魔方云实例创建任务失败：%s", firstNonEmptyString(result.Payload["msg"], result.Payload["message"], result.Text))
		}
	}
	return fmt.Errorf("魔方云实例创建超时")
}

func buildDCIMProvisionParams(instance commerce.Instance, product commerce.Product, server commerce.CloudServer, snapshot map[string]any) map[string]any {
	params := map[string]any{}
	sliderValues, _ := snapshot["slider_values"].(map[string]any)
	if sliderValues == nil {
		sliderValues = map[string]any{}
	}
	for _, raw := range anySlice(snapshot["provision_config"]) {
		field, ok := raw.(map[string]any)
		if !ok {
			continue
		}
		key := configFieldKey(field["def"])
		if key == "" {
			continue
		}
		fieldType := strings.TrimSpace(fmt.Sprint(firstNonNilValue(field["type"], "fixed")))
		var value any
		switch fieldType {
		case "fixed":
			value = field["value"]
		case "slider", "user_select":
			if selected, ok := sliderValues[key]; ok {
				value = selected
			} else {
				value = firstNonNilValue(field["value"], field["slider_min"])
			}
		case "input":
			if selected, ok := sliderValues[key]; ok {
				value = selected
			} else if key == "hostname" && instance.Hostname != nil {
				value = *instance.Hostname
			} else if key != "rootpass" {
				value = field["value"]
			}
		default:
			value = field["value"]
		}
		if normalized, ok := normalizeUpstreamValue(value); ok {
			params[key] = normalized
		}
	}
	if product.APIIdentifier != nil {
		if normalized, ok := normalizeUpstreamValue(*product.APIIdentifier); ok {
			switch product.APIType {
			case "dcim_cloud_image":
				if _, exists := params["os"]; !exists {
					params["os"] = normalized
				}
			case "dcim_cloud_rid":
				if _, exists := params["rid"]; !exists {
					params["rid"] = normalized
				}
			}
		}
	}
	if instance.OSImage != nil {
		if osValue, ok := normalizeUpstreamValue(*instance.OSImage); ok {
			params["os"] = osValue
		}
	}
	if server.ClientID != nil {
		params["client"] = *server.ClientID
	}
	delete(params, "client_name")
	setParamFromIntPtr(params, "cpu", instance.CPU)
	setParamFromIntPtr(params, "memory", instance.Memory)
	setParamFromIntPtr(params, "system_disk_size", instance.SystemDiskSize)
	setParamFromIntPtr(params, "data_disk_size", instance.DataDiskSize)
	setParamFromIntPtr(params, "in_bw", instance.InBW)
	setParamFromIntPtr(params, "out_bw", instance.OutBW)
	setParamFromIntPtr(params, "ip_num", instance.IPNum)
	setParamFromIntPtr(params, "nat_acl_limit", instance.NatACLLimit)
	setParamFromIntPtr(params, "traffic_quota", instance.TrafficQuota)
	setParamFromIntPtr(params, "traffic_type", instance.TrafficType)
	if _, exists := params["bw"]; !exists && instance.OutBW != nil {
		params["bw"] = *instance.OutBW
	}
	applyDCIMSnapshotLocation(params, snapshot)
	return params
}

func applyDCIMProductGroupLocation(ctx context.Context, tx *gorm.DB, product commerce.Product, params map[string]any) error {
	if hasDCIMLocation(params) {
		return nil
	}
	var group commerce.ProductGroup
	if err := tx.WithContext(ctx).Select("area", "node").Where("id = ?", product.GroupID).First(&group).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil
		}
		return err
	}
	if group.Node != nil && *group.Node != 0 {
		params["node"] = *group.Node
	} else if group.Area != nil && *group.Area != 0 {
		params["area"] = *group.Area
	}
	return nil
}

func applyDCIMSnapshotLocation(params map[string]any, snapshot map[string]any) {
	if hasDCIMLocation(params) {
		return
	}
	if value, ok := normalizeUpstreamValue(snapshot["product_group_node"]); ok {
		params["node"] = value
		return
	}
	if value, ok := normalizeUpstreamValue(snapshot["product_group_area"]); ok {
		params["area"] = value
	}
}

func hasDCIMLocation(params map[string]any) bool {
	for _, key := range []string{"node", "area", "node_group", "node_group_id", "ip_group", "ip_group_id", "ip", "ip_id"} {
		if locationValuePresent(params[key]) {
			return true
		}
	}
	return false
}

func applyDCIMProvisionDetail(instance *commerce.Instance, detail map[string]any, now time.Time) {
	if mainIP := dcimMainIP(detail); mainIP != "" {
		instance.MainIP = &mainIP
	}
	if port, ok := int64Value(firstNonNilValue(detail["port"], detail["ssh_port"])); ok && port > 0 && port <= 65535 {
		sshPort := int(port)
		instance.SSHPort = &sshPort
	}
	if hostname, ok := stringValue(firstNonNilValue(detail["hostname"], detail["domain"])); ok {
		instance.Hostname = &hostname
	}
	if rawPassword, ok := stringValue(firstNonNilValue(detail["rootpassword"], detail["password"])); ok {
		encrypted, err := commerce.EncryptStoredSecret(rawPassword)
		if err == nil {
			instance.RootPassword = &encrypted
		}
	}
	if instance.ExpiresAt == nil {
		expires := now.AddDate(0, 0, cycleDays(instance.Cycle))
		instance.ExpiresAt = &expires
	}
}

func dcimMainIP(detail map[string]any) string {
	for _, key := range []string{"mainip", "main_ip", "dedicatedip"} {
		if value, ok := stringValue(detail[key]); ok {
			return value
		}
	}
	for _, raw := range anySlice(detail["ip"]) {
		if item, ok := raw.(map[string]any); ok {
			if value, ok := stringValue(item["ip"]); ok {
				return value
			}
			continue
		}
		if value, ok := stringValue(raw); ok {
			return value
		}
	}
	return ""
}

func dcimTaskStatus(task map[string]any) string {
	if text, ok := stringValue(task["status"]); ok {
		switch strings.ToLower(text) {
		case "success", "finished", "completed", "done":
			return "success"
		case "failed", "failure", "error", "cancelled", "canceled":
			return "failed"
		default:
			return "running"
		}
	}
	if numeric, ok := int64Value(task["status"]); ok {
		switch numeric {
		case 2:
			return "success"
		case 3, 4, 5, -1:
			return "failed"
		}
	}
	if progress, ok := int64Value(task["progress"]); ok && progress >= 100 {
		return "success"
	}
	return "running"
}

func (w *ProvisionWorker) provisionZJMF(ctx context.Context, tx *gorm.DB, instance *commerce.Instance, product commerce.Product, now time.Time) error {
	if instance.CloudServerID == nil {
		return fmt.Errorf("魔方财务开通失败：实例未绑定云节点")
	}
	if product.APIIdentifier == nil || strings.TrimSpace(*product.APIIdentifier) == "" {
		return fmt.Errorf("魔方财务开通失败：产品未绑定上游产品 ID")
	}
	var server commerce.CloudServer
	if err := tx.WithContext(ctx).Where("id = ?", *instance.CloudServerID).First(&server).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return fmt.Errorf("魔方财务开通失败：云节点不存在")
		}
		return err
	}
	if server.ServerType != "zjmf_finance" {
		return fmt.Errorf("魔方财务开通失败：绑定的云节点不是魔方财务")
	}

	var order commerce.Order
	if err := tx.WithContext(ctx).Where("id = ?", instance.OrderID).First(&order).Error; err != nil {
		return err
	}
	snapshot := snapshotForInstance(order.ItemsSnapshot, *instance)
	pid := strings.TrimSpace(*product.APIIdentifier)

	configResult, err := commerce.CallZJMFAPI(ctx, server, "/cart/get_product_config", "GET", map[string]any{"pid": pid}, 30*time.Second)
	if err != nil {
		return fmt.Errorf("魔方财务获取产品配置失败：%w", err)
	}
	if err := ensureZJMFBodyOK(configResult, 200); err != nil {
		return fmt.Errorf("魔方财务获取产品配置失败：%w", err)
	}
	rawConfig := configResult.Payload
	cartData := buildZJMFCartData(product, instance, snapshot, rawConfig)
	downstream, err := w.zjmfDownstreamPayload(ctx, tx, instance)
	if err != nil {
		return err
	}
	mergeInstancePayload(instance, "zjmf_cart", map[string]any{
		"pid":               cartData["pid"],
		"billingcycle":      cartData["billingcycle"],
		"configoptions":     cartData["configoptions"],
		"os":                cartData["os"],
		"host":              cartData["host"],
		"password_provided": valuePresent(cartData["password"]),
	})

	paid := zjmfPayloadStatus(upstreamPayloadBlock(instance, "zjmf_credit")) == 1001
	hostIDs := []int64{}
	if instance.UpstreamID != nil {
		hostIDs = append(hostIDs, *instance.UpstreamID)
	}
	var invoiceID *int64
	if instance.UpstreamInvoiceID != nil {
		invoiceID = instance.UpstreamInvoiceID
	}

	if invoiceID == nil && len(hostIDs) == 0 {
		checkBody, err := callZJMFChecked(ctx, server, "/cart/clear", "POST", mergeStringAny(map[string]any{"is_api": 1}, downstream), 30*time.Second, 200, 400)
		if err != nil {
			return fmt.Errorf("魔方财务检查重复订单失败：%w", err)
		}
		checkInvoiceID := extractZJMFInvoiceID(checkBody)
		checkHostIDs := extractZJMFHostIDs(checkBody)
		checkStatus := zjmfPayloadStatus(checkBody)
		if checkStatus == 400 && len(checkHostIDs) == 0 {
			return fmt.Errorf("魔方财务检查重复订单失败：%s", firstNonEmptyString(checkBody["msg"], checkStatus))
		}
		if checkInvoiceID != nil || len(checkHostIDs) > 0 {
			mergeInstancePayload(instance, "zjmf_downstream_check", checkBody)
			invoiceID = checkInvoiceID
			hostIDs = checkHostIDs
			if invoiceID != nil {
				instance.UpstreamInvoiceID = invoiceID
			}
			if len(hostIDs) > 0 {
				instance.UpstreamID = &hostIDs[0]
			}
			paid = checkStatus == 400 && len(hostIDs) > 0 && invoiceID == nil
			if err := tx.WithContext(ctx).Save(instance).Error; err != nil {
				return err
			}
		}
	}

	if invoiceID == nil && len(hostIDs) == 0 {
		payload := mergeStringAny(map[string]any{"payment": "", "checkout": 1, "cart_data": cartData}, downstream)
		settleBody, err := callZJMFChecked(ctx, server, "/cart/settle", "POST", payload, 45*time.Second, 200, 1001)
		if err != nil {
			return fmt.Errorf("魔方财务创建订单失败：%w", err)
		}
		mergeInstancePayload(instance, "zjmf_settle", settleBody)
		invoiceID = extractZJMFInvoiceID(settleBody)
		hostIDs = extractZJMFHostIDs(settleBody)
		if invoiceID != nil {
			instance.UpstreamInvoiceID = invoiceID
		}
		if len(hostIDs) > 0 {
			instance.UpstreamID = &hostIDs[0]
		}
		paid = zjmfPayloadStatus(settleBody) == 1001
		if err := tx.WithContext(ctx).Save(instance).Error; err != nil {
			return err
		}
	}

	if invoiceID != nil && !paid {
		invoiceDetail, err := callZJMFChecked(ctx, server, "/invoice_page", "POST", map[string]any{"invoiceid": strconv.FormatInt(*invoiceID, 10)}, 30*time.Second, 200)
		if err != nil {
			return fmt.Errorf("魔方财务账单校验失败：%w", err)
		}
		if err := validateZJMFInvoiceAmount(invoiceDetail, instance.UnitPrice); err != nil {
			return err
		}
		mergeInstancePayload(instance, "zjmf_invoice", invoiceDetail)
		creditPayload := mergeStringAny(map[string]any{
			"invoiceid":  strconv.FormatInt(*invoiceID, 10),
			"use_credit": 1,
			"enough":     1,
			"is_api":     1,
		}, downstream)
		creditBody, err := callZJMFChecked(ctx, server, "/apply_credit", "POST", creditPayload, 45*time.Second, 200, 1001)
		if err != nil {
			return fmt.Errorf("魔方财务余额支付失败：%w", err)
		}
		mergeInstancePayload(instance, "zjmf_credit", creditBody)
		if ids := extractZJMFHostIDs(creditBody); len(ids) > 0 {
			hostIDs = ids
			instance.UpstreamID = &hostIDs[0]
		}
		paid = zjmfPayloadStatus(creditBody) == 1001
		if err := tx.WithContext(ctx).Save(instance).Error; err != nil {
			return err
		}
	}

	if !paid {
		return fmt.Errorf("魔方财务订单已创建，但上游账单未完成支付")
	}
	if len(hostIDs) == 0 && instance.UpstreamID != nil {
		hostIDs = append(hostIDs, *instance.UpstreamID)
	}
	if len(hostIDs) == 0 {
		return fmt.Errorf("魔方财务支付成功，但未返回上游产品 ID")
	}
	hostID := hostIDs[0]
	instance.UpstreamID = &hostID
	detail, ready := w.pollZJMFHostDetail(ctx, server, hostID)
	if len(detail) > 0 {
		applyZJMFHostDetail(instance, detail)
		mergeInstancePayload(instance, "zjmf_host", sanitizeZJMFHostPayload(detail))
	}
	if !ready {
		message := fmt.Sprintf("魔方财务主机仍在开通中 zjmf_host_id=%d", hostID)
		instance.ProvisionLog = &message
		return errProvisionPending
	}
	if instance.ExpiresAt == nil {
		expires := now.AddDate(0, 0, cycleDays(instance.Cycle))
		instance.ExpiresAt = &expires
	}
	message := fmt.Sprintf("开通成功 zjmf_host_id=%d", hostID)
	instance.ProvisionLog = &message
	instance.Status = "active"
	return nil
}

func callZJMFChecked(ctx context.Context, server commerce.CloudServer, path string, method string, payload map[string]any, timeout time.Duration, allowedStatuses ...int) (map[string]any, error) {
	result, err := commerce.CallZJMFAPI(ctx, server, path, method, payload, timeout)
	if err != nil {
		return nil, err
	}
	if err := ensureZJMFBodyOK(result, allowedStatuses...); err != nil {
		return nil, err
	}
	return result.Body, nil
}

func ensureZJMFBodyOK(result interface {
}, allowedStatuses ...int) error {
	type zjmfResult interface {
	}
	_ = zjmfResult(nil)
	bodyResult := any(result)
	statusCode := 0
	text := ""
	body := map[string]any{}
	switch typed := bodyResult.(type) {
	case struct {
		StatusCode int
		Text       string
		Body       map[string]any
		Payload    map[string]any
		TaskID     *int64
	}:
		statusCode = typed.StatusCode
		text = typed.Text
		body = typed.Body
	default:
		raw, _ := json.Marshal(typed)
		var decoded struct {
			StatusCode int            `json:"StatusCode"`
			Text       string         `json:"Text"`
			Body       map[string]any `json:"Body"`
		}
		_ = json.Unmarshal(raw, &decoded)
		statusCode = decoded.StatusCode
		text = decoded.Text
		body = decoded.Body
	}
	if statusCode < 200 || statusCode >= 300 {
		return fmt.Errorf("HTTP %d：%s", statusCode, trimJobText(text, 200))
	}
	status := zjmfPayloadStatus(body)
	for _, allowed := range allowedStatuses {
		if status == allowed {
			return nil
		}
	}
	return fmt.Errorf("%s", firstNonEmptyString(body["msg"], body["message"], body["status"], text))
}

func buildZJMFCartData(product commerce.Product, instance *commerce.Instance, snapshot map[string]any, rawConfig map[string]any) map[string]any {
	configOptions, osPayload := buildZJMFConfigOptions(rawConfig, *instance, snapshot)
	hostname, password := ensureZJMFLoginFields(instance, rawConfig)
	return map[string]any{
		"pid":           strings.TrimSpace(derefString(product.APIIdentifier)),
		"billingcycle":  zjmfBillingCycle(firstNonEmptyString(instance.Cycle, snapshot["cycle"], "monthly")),
		"serverid":      0,
		"configoptions": configOptions,
		"customfield":   map[string]any{},
		"currencyid":    0,
		"qty":           1,
		"os":            osPayload,
		"host":          hostname,
		"password":      password,
		"hostid":        0,
	}
}

func buildZJMFConfigOptions(rawConfig map[string]any, instance commerce.Instance, snapshot map[string]any) (map[string]any, map[string]string) {
	values := snapshotConfigValues(instance, snapshot)
	osValue, osLabel := selectedOS(instance)
	configOptions := map[string]any{}
	osPayload := map[string]string{}
	for _, rawGroup := range anySlice(rawConfig["config_groups"]) {
		group, ok := rawGroup.(map[string]any)
		if !ok {
			continue
		}
		for _, rawOption := range anySlice(group["options"]) {
			option, ok := rawOption.(map[string]any)
			if !ok {
				continue
			}
			configID := firstNonEmptyString(option["id"])
			if configID == "" {
				continue
			}
			optionKey := zjmfOptionKey(option)
			var desired any
			if optionKey == "os" {
				desired = osValue
			} else {
				for _, candidate := range candidateConfigValues(optionKey, values) {
					desired = candidate
					break
				}
			}
			if isZJMFQuantityOption(option["option_type"]) {
				if qty, ok := quantityValueForOption(option, desired); ok {
					configOptions[configID] = qty
				}
				continue
			}
			subID, subLabel := matchZJMFSubID(option, desired)
			if subID == "" {
				continue
			}
			configOptions[configID] = subID
			if optionKey == "os" {
				if osLabel != "" {
					osPayload[subID] = osLabel
				} else if subLabel != "" {
					osPayload[subID] = subLabel
				} else {
					osPayload[subID] = subID
				}
			}
		}
	}
	return configOptions, osPayload
}

func ensureZJMFLoginFields(instance *commerce.Instance, rawConfig map[string]any) (string, string) {
	hostname := ""
	if instance.Hostname != nil {
		hostname = strings.TrimSpace(*instance.Hostname)
	}
	if hostname == "" && instance.Alias != nil {
		hostname = strings.TrimSpace(*instance.Alias)
	}
	if hostname == "" {
		hostname = fmt.Sprintf("server-%d", instance.ID)
	}
	instance.Hostname = &hostname
	password := randomZJMFPassword(rawConfig)
	if password != "" {
		if encrypted, err := commerce.EncryptStoredSecret(password); err == nil {
			instance.RootPassword = &encrypted
		}
	}
	return hostname, password
}

func randomZJMFPassword(rawConfig map[string]any) string {
	length := 12
	product := zjmfProductFromConfig(rawConfig)
	if passwordRule, ok := parseJSONObject(product["password"]); ok {
		if rule, ok := parseJSONObject(passwordRule["rule"]); ok {
			if n, ok := int64Value(rule["len_num"]); ok && n > 0 {
				length = int(n)
			}
		}
	}
	if length < 6 {
		length = 6
	}
	if length > 20 {
		length = 20
	}
	groups := []string{"ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz", "0123456789"}
	required := make([]byte, 0, length)
	for _, group := range groups {
		required = append(required, randomChar(group))
	}
	alphabet := strings.Join(groups, "")
	for len(required) < length {
		required = append(required, randomChar(alphabet))
	}
	for i := len(required) - 1; i > 0; i-- {
		j := int(randomInt(int64(i + 1)))
		required[i], required[j] = required[j], required[i]
	}
	return string(required)
}

func (w *ProvisionWorker) zjmfDownstreamPayload(ctx context.Context, tx *gorm.DB, instance *commerce.Instance) (map[string]any, error) {
	if instance.DownstreamToken == nil || strings.TrimSpace(*instance.DownstreamToken) == "" {
		token := stableZJMFDownstreamToken(*instance)
		instance.DownstreamToken = &token
	}
	backendURL := settingStringFromDB(ctx, tx, "platform.backend_url")
	if backendURL == "" {
		backendURL = settingStringFromDB(ctx, tx, "platform.frontend_url")
	}
	backendURL = normalizeZJMFDownstreamBaseURL(backendURL)
	return map[string]any{
		"is_api":           1,
		"downstream_url":   backendURL,
		"downstream_token": *instance.DownstreamToken,
		"downstream_id":    instance.ID,
	}, nil
}

func stableZJMFDownstreamToken(instance commerce.Instance) string {
	secret := []byte(os.Getenv("JWT_SECRET_KEY"))
	mac := hmac.New(sha256.New, secret)
	_, _ = mac.Write([]byte(fmt.Sprintf("zjmf:%d:%d:%d", instance.ID, instance.OrderID, instance.ProductID)))
	return hex.EncodeToString(mac.Sum(nil))[:32]
}

func settingStringFromDB(ctx context.Context, tx *gorm.DB, key string) string {
	var item commerce.SystemSetting
	if err := tx.WithContext(ctx).Where("`key` = ?", key).First(&item).Error; err != nil {
		return ""
	}
	var value any
	if err := json.Unmarshal(item.Value, &value); err != nil {
		return strings.Trim(strings.TrimSpace(string(item.Value)), `"`)
	}
	return firstNonEmptyString(value)
}

func normalizeZJMFDownstreamBaseURL(value string) string {
	text := strings.TrimRight(strings.TrimSpace(value), "/")
	if !strings.HasPrefix(text, "http://") && !strings.HasPrefix(text, "https://") {
		return "http://127.0.0.1"
	}
	for _, suffix := range []string{"/api/host/sync", "/api"} {
		if strings.HasSuffix(strings.ToLower(text), suffix) {
			text = strings.TrimRight(text[:len(text)-len(suffix)], "/")
		}
	}
	if text == "" {
		return "http://127.0.0.1"
	}
	return text
}

func (w *ProvisionWorker) pollZJMFHostDetail(ctx context.Context, server commerce.CloudServer, hostID int64) (map[string]any, bool) {
	deadline := time.Now().Add(120 * time.Second)
	var last map[string]any
	for time.Now().Before(deadline) {
		result, err := commerce.CallZJMFAPI(ctx, server, "/host/header", "GET", map[string]any{"host_id": strconv.FormatInt(hostID, 10)}, 30*time.Second)
		if err == nil && result.StatusCode >= 200 && result.StatusCode < 300 && zjmfPayloadStatus(result.Body) == 200 {
			last = result.Payload
			if zjmfHostReady(last) {
				return last, true
			}
		} else if err != nil && w.logger != nil {
			w.logger.Warn("poll zjmf host failed", "host_id", hostID, "error", err)
		}
		select {
		case <-ctx.Done():
			return last, false
		case <-time.After(3 * time.Second):
		}
	}
	return last, zjmfHostReady(last)
}

func applyZJMFHostDetail(instance *commerce.Instance, detail map[string]any) {
	hostData := detail
	if nested, ok := detail["host_data"].(map[string]any); ok {
		hostData = nested
	}
	if mainIP := firstNonEmptyString(hostData["dedicatedip"], hostData["mainip"], hostData["main_ip"], hostData["ip"]); mainIP != "" {
		instance.MainIP = &mainIP
	} else if assigned := anySlice(hostData["assignedips"]); len(assigned) > 0 {
		if value := firstNonEmptyString(assigned[0]); value != "" {
			instance.MainIP = &value
		}
	}
	if port, ok := int64Value(hostData["port"]); ok && port > 0 && port <= 65535 {
		sshPort := int(port)
		instance.SSHPort = &sshPort
	}
	if hostname := firstNonEmptyString(hostData["domain"], hostData["hostname"]); hostname != "" {
		instance.Hostname = &hostname
	}
	if rawPassword := firstNonEmptyString(hostData["password"], hostData["rootpassword"]); rawPassword != "" {
		if encrypted, err := commerce.EncryptStoredSecret(rawPassword); err == nil {
			instance.RootPassword = &encrypted
		}
	}
	if due, ok := int64Value(firstNonNilValue(hostData["nextduedate"], hostData["next_due_date"])); ok && due > 0 {
		expires := time.Unix(due, 0).UTC()
		instance.ExpiresAt = &expires
	}
}

func zjmfHostReady(detail map[string]any) bool {
	if len(detail) == 0 {
		return false
	}
	hostData := detail
	if nested, ok := detail["host_data"].(map[string]any); ok {
		hostData = nested
	}
	status := strings.ToLower(firstNonEmptyString(detail["domainstatus"], detail["status"], hostData["domainstatus"], hostData["status"]))
	if status == "active" {
		return true
	}
	return firstNonEmptyString(detail["mainip"], detail["main_ip"], hostData["dedicatedip"], hostData["mainip"], hostData["main_ip"], hostData["ip"]) != ""
}

func sanitizeZJMFHostPayload(detail map[string]any) map[string]any {
	out := map[string]any{}
	for key, value := range detail {
		lower := strings.ToLower(key)
		if lower == "password" || lower == "rootpassword" || lower == "ippassword" {
			continue
		}
		out[key] = value
	}
	return out
}

func (w *ProvisionWorker) afterProvisionSuccess(ctx context.Context, tx *gorm.DB, instance *commerce.Instance) error {
	if err := markPointRewardsClaimable(ctx, tx, instance); err != nil {
		return err
	}
	commerceService := commerce.NewService(tx, nil, nil)
	if err := commerceService.CreateMemberCommissionSettlementForOrder(ctx, instance.OrderID); err != nil {
		return err
	}
	if err := commerceService.FreezeOrderSettlements(ctx, instance.OrderID); err != nil {
		return err
	}
	if err := createProvisionSuccessNotification(ctx, tx, instance); err != nil && w.logger != nil {
		w.logger.Warn("provision success notification skipped", "instance_id", instance.ID, "error", err)
	}
	if err := commerce.SendInstanceActiveEmail(ctx, tx, *instance, w.plugins); err != nil && w.logger != nil {
		w.logger.Warn("provision success email skipped", "instance_id", instance.ID, "error", err)
	}
	return nil
}

func markPointRewardsClaimable(ctx context.Context, tx *gorm.DB, instance *commerce.Instance) error {
	return tx.WithContext(ctx).
		Model(&commerce.PointReward{}).
		Where("instance_id = ? AND order_id = ? AND status = ?", instance.ID, instance.OrderID, "pending").
		Update("status", "claimable").Error
}

func createProvisionSuccessNotification(ctx context.Context, tx *gorm.DB, instance *commerce.Instance) error {
	enabled, err := notificationInAppEnabled(ctx, tx, instance.UserID, "instance")
	if err != nil {
		return err
	}
	if !enabled {
		return nil
	}
	notification := buildProvisionSuccessNotification(instance)
	return tx.WithContext(ctx).Create(&notification).Error
}

func buildProvisionSuccessNotification(instance *commerce.Instance) commerce.Notification {
	senderName := "云服务器中心"
	link := fmt.Sprintf("/control/server/%d", instance.ID)
	actionLabel := "查看服务"
	return commerce.Notification{
		UserID:      instance.UserID,
		Category:    "instance",
		Level:       "success",
		SenderName:  &senderName,
		SenderType:  "instance",
		Title:       "服务开通成功",
		Content:     fmt.Sprintf("您的服务 #%d 已开通成功。", instance.ID),
		Link:        &link,
		ActionLabel: &actionLabel,
		ActionURL:   &link,
		IsRead:      false,
	}
}

func notificationInAppEnabled(ctx context.Context, tx *gorm.DB, userID int64, category string) (bool, error) {
	var pref commerce.NotificationPreference
	err := tx.WithContext(ctx).
		Where("user_id = ? AND category = ?", userID, category).
		First(&pref).Error
	if err == nil {
		return pref.InAppEnabled, nil
	}
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return true, nil
	}
	return false, err
}

func applyGenericBlankProvision(instance *commerce.Instance, product commerce.Product, now time.Time) {
	hostname := fmt.Sprintf("service-%d", instance.ID)
	if instance.Alias != nil && strings.TrimSpace(*instance.Alias) != "" {
		hostname = strings.TrimSpace(*instance.Alias)
	}
	instance.Hostname = &hostname
	expires := now.AddDate(0, 0, cycleDays(instance.Cycle))
	instance.ExpiresAt = &expires
	instance.Status = "active"
	message := "通用空白产品已完成插件开通"
	instance.ProvisionLog = &message
	instance.UpstreamPayload = jsonRaw(map[string]any{
		"provision_plugin": map[string]any{
			"plugin_id": genericBlankPluginID,
			"links": []map[string]any{
				{"label": "实例详情", "url": fmt.Sprintf("/control/server/%d", instance.ID)},
			},
			"payload": map[string]any{
				"plugin":     genericBlankPluginID,
				"product_id": product.ID,
				"cycle":      instance.Cycle,
			},
		},
	})
}

type provisionPluginResponse struct {
	OK        bool           `json:"ok"`
	Message   string         `json:"message"`
	Retryable bool           `json:"retryable"`
	Result    map[string]any `json:"result"`
}

func (w *ProvisionWorker) callProvisionPlugin(ctx context.Context, tx *gorm.DB, instance commerce.Instance, product commerce.Product, pluginID string) (provisionPluginResponse, error) {
	if w.plugins == nil {
		return provisionPluginResponse{}, fmt.Errorf("Go provision plugin runtime is not enabled")
	}
	request, err := w.provisionPluginRequest(ctx, tx, instance, product, pluginID)
	if err != nil {
		return provisionPluginResponse{}, err
	}
	req := provisionRequestFromBridge(pluginID, request)
	req.Config = w.plugins.MergeConfig(ctx, pluginID, req.Config)
	response, err := w.plugins.Provision(ctx, pluginID, req)
	if err != nil {
		return provisionPluginResponse{}, err
	}
	return provisionPluginResponse{
		OK:        response.OK,
		Message:   response.Message,
		Retryable: response.Retryable,
		Result:    response.Result,
	}, nil
}

func (w *ProvisionWorker) callProvisionPluginFallback(ctx context.Context, tx *gorm.DB, instance commerce.Instance, product commerce.Product, apiType string) (provisionPluginResponse, string, error) {
	if w.plugins == nil {
		return provisionPluginResponse{}, "", fmt.Errorf("unsupported api_type %q and Go provision plugin runtime is not enabled", apiType)
	}
	candidates := []string{}
	if product.APIIdentifier != nil {
		candidates = append(candidates, strings.TrimSpace(*product.APIIdentifier))
	}
	candidates = append(candidates, strings.TrimSpace(strings.TrimPrefix(apiType, "plugin:")))
	seen := map[string]struct{}{}
	for _, candidate := range candidates {
		candidate = strings.TrimSpace(candidate)
		if candidate == "" {
			continue
		}
		if _, ok := seen[candidate]; ok {
			continue
		}
		seen[candidate] = struct{}{}
		manifest, err := w.plugins.Manifest(ctx, candidate)
		if err != nil || !manifest.HasCapability(plugins.CapabilityProvision) {
			continue
		}
		result, err := w.callProvisionPlugin(ctx, tx, instance, product, candidate)
		return result, candidate, err
	}
	return provisionPluginResponse{}, "", fmt.Errorf("unsupported api_type %q: no enabled Go provision plugin matched api_identifier/api_type", apiType)
}

func provisionRequestFromBridge(pluginID string, request map[string]any) plugins.ProvisionRequest {
	req := plugins.ProvisionRequest{
		PluginID:           pluginID,
		Instance:           mapValue(request["instance"]),
		Product:            mapValue(request["product"]),
		Order:              mapValue(request["order"]),
		User:               mapValue(request["user"]),
		Snapshot:           mapValue(request["snapshot"]),
		UpstreamParams:     mapValue(request["upstream_params"]),
		InterfaceConfig:    mapValue(request["interface_config"]),
		PluginFields:       mapValue(request["plugin_fields"]),
		BackwardCompatible: request,
	}
	if values, ok := request["product_config_values"].([]any); ok {
		req.ProductConfigValues = values
	}
	return req
}

func mapValue(value any) map[string]any {
	if out, ok := value.(map[string]any); ok {
		return out
	}
	return map[string]any{}
}

func (w *ProvisionWorker) provisionPluginRequest(ctx context.Context, tx *gorm.DB, instance commerce.Instance, product commerce.Product, pluginID string) (map[string]any, error) {
	var order commerce.Order
	if err := tx.WithContext(ctx).Where("id = ?", instance.OrderID).First(&order).Error; err != nil {
		return nil, err
	}
	var user commerce.UserAccount
	if err := tx.WithContext(ctx).Where("id = ?", instance.UserID).First(&user).Error; err != nil {
		return nil, err
	}
	snapshot := snapshotForInstance(order.ItemsSnapshot, instance)
	sliderValues, _ := snapshot["slider_values"].(map[string]any)
	pluginFields, _ := sliderValues["__plugin_fields"].(map[string]any)
	upstreamParams, _ := snapshot["upstream_params"].(map[string]any)
	interfaceConfig, _ := snapshot["interface_config"].(map[string]any)
	storedInterfaceConfig, err := commerce.LoadProvisionInterfaceRuntimeConfig(ctx, tx, product.ProvisionInterfaceID, pluginID)
	if err != nil {
		return nil, err
	}
	for key, value := range interfaceConfig {
		storedInterfaceConfig[key] = value
	}
	interfaceConfig = storedInterfaceConfig
	productConfigValues, _ := snapshot["provision_config"].([]any)

	return map[string]any{
		"plugin_id":             pluginID,
		"instance":              instanceBridgeMap(instance),
		"product":               productBridgeMap(product),
		"order":                 orderBridgeMap(order),
		"user":                  userBridgeMap(user),
		"snapshot":              snapshot,
		"upstream_params":       upstreamParams,
		"interface_config":      interfaceConfig,
		"plugin_fields":         pluginFields,
		"product_config_values": productConfigValues,
	}, nil
}

func (w *ProvisionWorker) applyProvisionError(tx *gorm.DB, instance *commerce.Instance, message string, retryable bool) error {
	if strings.TrimSpace(message) == "" {
		message = "unknown provision error"
	}
	if !retryable || instance.ProvisionAttempts >= w.maxAttempts {
		instance.Status = "provision_failed"
		if retryable {
			message = fmt.Sprintf("自动开通失败（已重试%d次）：%s", w.maxAttempts, message)
		}
	}
	instance.ProvisionLog = &message
	if err := tx.Save(instance).Error; err != nil {
		return err
	}
	if instance.Status == "provision_failed" {
		service := commerce.NewService(tx, nil, w.logger, w.plugins)
		if err := service.CompensateProvisionFailureTx(context.Background(), instance.ID, message); err != nil {
			return err
		}
		if err := commerce.SendInstanceFailedEmail(context.Background(), tx, *instance, message, w.plugins); err != nil && w.logger != nil {
			w.logger.Warn("provision failed email skipped", "instance_id", instance.ID, "error", err)
		}
	}
	return nil
}

func applyPluginProvisionResult(instance *commerce.Instance, pluginID string, result map[string]any, now time.Time) error {
	if result == nil {
		result = map[string]any{}
	}
	if hostname, ok := stringValue(result["hostname"]); ok {
		instance.Hostname = &hostname
	}
	if mainIP, ok := stringValue(result["main_ip"]); ok {
		instance.MainIP = &mainIP
	}
	if rawPassword, ok := stringValue(firstNonNilValue(result["root_password"], result["password"])); ok {
		encrypted, err := commerce.EncryptStoredSecret(rawPassword)
		if err != nil {
			return err
		}
		instance.RootPassword = &encrypted
	}
	if upstreamID, ok := int64Value(result["upstream_id"]); ok {
		instance.UpstreamID = &upstreamID
	}
	if taskID, ok := int64Value(result["task_id"]); ok {
		instance.UpstreamTaskID = &taskID
	}
	if invoiceID, ok := int64Value(result["invoice_id"]); ok {
		instance.UpstreamInvoiceID = &invoiceID
	}
	if expiresAt, ok := timeValue(result["expires_at"]); ok {
		instance.ExpiresAt = &expiresAt
	} else if instance.ExpiresAt == nil {
		expires := now.AddDate(0, 0, cycleDays(instance.Cycle))
		instance.ExpiresAt = &expires
	}
	message, ok := stringValue(result["message"])
	if !ok {
		message = fmt.Sprintf("插件 %s 开通成功", pluginID)
	}
	instance.ProvisionLog = &message
	instance.Status = "active"
	payload, _ := result["payload"].(map[string]any)
	instance.UpstreamPayload = jsonRaw(map[string]any{
		"provision_plugin": map[string]any{
			"plugin_id":    pluginID,
			"upstream_id":  result["upstream_id"],
			"task_id":      result["task_id"],
			"invoice_id":   result["invoice_id"],
			"links":        result["links"],
			"payload":      payload,
			"bridge":       "go-plugin-process",
			"completed_at": now.Format(time.RFC3339),
		},
	})
	return nil
}

func cycleDays(cycle string) int {
	switch strings.ToLower(strings.TrimSpace(cycle)) {
	case "hourly", "daily":
		return 1
	case "weekly":
		return 7
	case "monthly":
		return 30
	case "quarterly":
		return 90
	case "semi_annually", "semiannually":
		return 180
	case "annually", "yearly":
		return 365
	case "biennially":
		return 730
	case "triennially":
		return 1095
	case "one_time":
		return 3650
	default:
		return 30
	}
}

func snapshotForInstance(raw commerce.JSONRaw, instance commerce.Instance) map[string]any {
	items := []any{}
	if len(raw) > 0 {
		_ = json.Unmarshal(raw, &items)
	}
	var fallback map[string]any
	for _, item := range items {
		snapshot, ok := item.(map[string]any)
		if !ok {
			continue
		}
		if fallback == nil {
			fallback = snapshot
		}
		if id, ok := int64Value(snapshot["product_id"]); ok && id == instance.ProductID {
			return snapshot
		}
	}
	if fallback != nil {
		return fallback
	}
	return map[string]any{}
}

func instanceBridgeMap(instance commerce.Instance) map[string]any {
	return map[string]any{
		"id":                instance.ID,
		"user_id":           instance.UserID,
		"order_id":          instance.OrderID,
		"product_id":        instance.ProductID,
		"cloud_server_id":   instance.CloudServerID,
		"status":            instance.Status,
		"cycle":             instance.Cycle,
		"unit_price":        instance.UnitPrice,
		"auto_renew":        instance.AutoRenew,
		"next_renew_amount": instance.NextRenewAmount,
		"alias":             instance.Alias,
		"hostname":          instance.Hostname,
		"os_image":          instance.OSImage,
		"cpu":               instance.CPU,
		"memory":            instance.Memory,
	}
}

func productBridgeMap(product commerce.Product) map[string]any {
	return map[string]any{
		"id":                     product.ID,
		"group_id":               product.GroupID,
		"sku":                    product.SKU,
		"name":                   product.Name,
		"type":                   product.Type,
		"status":                 product.Status,
		"api_type":               product.APIType,
		"api_identifier":         product.APIIdentifier,
		"provision_interface_id": product.ProvisionInterfaceID,
		"provision_config":       jsonAny(product.ProvisionConfig),
	}
}

func orderBridgeMap(order commerce.Order) map[string]any {
	return map[string]any{
		"id":              order.ID,
		"user_id":         order.UserID,
		"out_order_no":    order.OutOrderNo,
		"status":          order.Status,
		"total_amount":    order.TotalAmount,
		"discount_amount": order.DiscountAmount,
		"promo_code_id":   order.PromoCodeID,
		"aff_code":        order.AffCode,
	}
}

func userBridgeMap(user commerce.UserAccount) map[string]any {
	return map[string]any{
		"id":             user.ID,
		"balance":        user.Balance,
		"invite_code":    user.InviteCode,
		"bound_agent_id": user.BoundAgentID,
	}
}

func jsonAny(raw commerce.JSONRaw) any {
	if len(raw) == 0 {
		return nil
	}
	var value any
	if err := json.Unmarshal(raw, &value); err != nil {
		return nil
	}
	return value
}

func stringValue(value any) (string, bool) {
	switch v := value.(type) {
	case string:
		text := strings.TrimSpace(v)
		return text, text != ""
	case fmt.Stringer:
		text := strings.TrimSpace(v.String())
		return text, text != ""
	default:
		return "", false
	}
}

func int64Value(value any) (int64, bool) {
	switch v := value.(type) {
	case int:
		return int64(v), true
	case int64:
		return v, true
	case float64:
		return int64(v), true
	case json.Number:
		parsed, err := v.Int64()
		return parsed, err == nil
	case string:
		if strings.TrimSpace(v) == "" {
			return 0, false
		}
		parsed, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64)
		return parsed, err == nil
	default:
		return 0, false
	}
}

func timeValue(value any) (time.Time, bool) {
	text, ok := stringValue(value)
	if !ok {
		return time.Time{}, false
	}
	for _, layout := range []string{time.RFC3339Nano, time.RFC3339, "2006-01-02T15:04:05"} {
		parsed, err := time.Parse(layout, text)
		if err == nil {
			return parsed, true
		}
	}
	return time.Time{}, false
}

func jsonRaw(value any) commerce.JSONRaw {
	raw, err := json.Marshal(value)
	if err != nil {
		return nil
	}
	return commerce.JSONRaw(raw)
}

func anySlice(value any) []any {
	switch typed := value.(type) {
	case []any:
		return typed
	case []map[string]any:
		out := make([]any, 0, len(typed))
		for _, item := range typed {
			out = append(out, item)
		}
		return out
	default:
		return []any{}
	}
}

func configFieldKey(value any) string {
	text := strings.TrimSpace(fmt.Sprint(value))
	if text == "" || text == "<nil>" {
		return ""
	}
	if strings.Contains(text, "|") {
		text = strings.SplitN(text, "|", 2)[0]
	}
	return strings.TrimSpace(text)
}

func normalizeUpstreamValue(value any) (any, bool) {
	if !valuePresent(value) {
		return nil, false
	}
	if text, ok := value.(string); ok {
		text = strings.TrimSpace(text)
		if strings.Contains(text, "|") {
			text = strings.TrimSpace(strings.SplitN(text, "|", 2)[0])
		}
		if text == "" {
			return nil, false
		}
		return text, true
	}
	return value, true
}

func valuePresent(value any) bool {
	if value == nil {
		return false
	}
	if text, ok := value.(string); ok {
		text = strings.TrimSpace(text)
		return text != "" && text != "<nil>"
	}
	return true
}

func locationValuePresent(value any) bool {
	if !valuePresent(value) {
		return false
	}
	switch typed := value.(type) {
	case int:
		return typed != 0
	case int64:
		return typed != 0
	case float64:
		return typed != 0
	case string:
		text := strings.TrimSpace(typed)
		return text != "" && text != "0"
	default:
		return true
	}
}

func setParamFromIntPtr(params map[string]any, key string, value *int) {
	if _, exists := params[key]; exists || value == nil {
		return
	}
	params[key] = *value
}

func firstNonNilValue(values ...any) any {
	for _, value := range values {
		if value != nil {
			return value
		}
	}
	return nil
}

func firstNonEmptyString(values ...any) string {
	for _, value := range values {
		if text, ok := stringValue(value); ok {
			return text
		}
	}
	return ""
}

func trimJobText(value string, n int) string {
	value = strings.TrimSpace(value)
	if len(value) <= n {
		return value
	}
	return value[:n]
}

func derefString(value *string) string {
	if value == nil {
		return ""
	}
	return *value
}

func zjmfBillingCycle(cycle string) string {
	switch strings.ToLower(strings.TrimSpace(cycle)) {
	case "hourly":
		return "hour"
	case "daily":
		return "day"
	case "monthly":
		return "monthly"
	case "quarterly":
		return "quarterly"
	case "semi_annually":
		return "semiannually"
	case "annually", "yearly":
		return "annually"
	case "biennially":
		return "biennially"
	case "triennially":
		return "triennially"
	case "one_time":
		return "onetime"
	default:
		if strings.TrimSpace(cycle) == "" {
			return "monthly"
		}
		return strings.TrimSpace(cycle)
	}
}

func snapshotConfigValues(instance commerce.Instance, snapshot map[string]any) map[string]any {
	values := map[string]any{}
	if sliderValues, ok := snapshot["slider_values"].(map[string]any); ok {
		for key, value := range sliderValues {
			if normalized, ok := normalizeUpstreamValue(value); ok {
				values[key] = normalized
			}
		}
	}
	for _, raw := range anySlice(snapshot["provision_config"]) {
		field, ok := raw.(map[string]any)
		if !ok {
			continue
		}
		key := configFieldKey(field["def"])
		if key == "" {
			continue
		}
		if _, exists := values[key]; exists {
			continue
		}
		if strings.TrimSpace(fmt.Sprint(firstNonNilValue(field["type"], "fixed"))) == "fixed" {
			if normalized, ok := normalizeUpstreamValue(field["value"]); ok {
				values[key] = normalized
			}
		}
	}
	addIntValue(values, "cpu", instance.CPU)
	addIntValue(values, "memory", instance.Memory)
	addIntValue(values, "system_disk_size", instance.SystemDiskSize)
	addIntValue(values, "data_disk_size", instance.DataDiskSize)
	addIntValue(values, "in_bw", instance.InBW)
	addIntValue(values, "out_bw", instance.OutBW)
	addIntValue(values, "ip_num", instance.IPNum)
	addIntValue(values, "nat_acl_limit", instance.NatACLLimit)
	addIntValue(values, "traffic_quota", instance.TrafficQuota)
	addIntValue(values, "traffic_type", instance.TrafficType)
	if _, exists := values["bw"]; !exists && instance.OutBW != nil {
		values["bw"] = *instance.OutBW
	}
	return values
}

func addIntValue(values map[string]any, key string, value *int) {
	if _, exists := values[key]; exists || value == nil {
		return
	}
	values[key] = *value
}

func selectedOS(instance commerce.Instance) (string, string) {
	if instance.OSImage == nil {
		return "", ""
	}
	text := strings.TrimSpace(*instance.OSImage)
	if text == "" {
		return "", ""
	}
	if strings.Contains(text, "|") {
		parts := strings.SplitN(text, "|", 2)
		value := strings.TrimSpace(parts[0])
		label := strings.TrimSpace(parts[1])
		if label == "" {
			label = value
		}
		return value, label
	}
	return text, text
}

func zjmfOptionKey(option map[string]any) string {
	return strings.ToLower(configFieldKey(firstNonNilValue(option["option_name"], option["name"])))
}

func candidateConfigValues(key string, values map[string]any) []any {
	aliases := map[string][]string{
		"cpu":              {"cpu", "vcpu"},
		"vcpu":             {"vcpu", "cpu"},
		"memory":           {"memory", "mem", "ram"},
		"mem":              {"memory", "mem", "ram"},
		"ram":              {"memory", "mem", "ram"},
		"bw":               {"bw", "out_bw", "bandwidth"},
		"out_bw":           {"out_bw", "bw", "bandwidth"},
		"in_bw":            {"in_bw"},
		"system_disk":      {"system_disk_size", "system_disk"},
		"system_disk_size": {"system_disk_size", "system_disk"},
		"data_disk":        {"data_disk_size", "data_disk"},
		"data_disk_size":   {"data_disk_size", "data_disk"},
		"ip_num":           {"ip_num", "ipv4", "ip"},
		"nat_acl_limit":    {"nat_acl_limit", "nat_acl"},
		"os":               {"os"},
	}
	keys := aliases[key]
	if len(keys) == 0 {
		keys = []string{key}
	}
	out := []any{}
	for _, item := range keys {
		if valuePresent(values[item]) {
			out = append(out, values[item])
		}
	}
	return out
}

func isZJMFQuantityOption(value any) bool {
	parsed, ok := int64Value(value)
	if !ok {
		return false
	}
	switch parsed {
	case 4, 7, 9, 11, 14, 15, 16, 17, 18, 19:
		return true
	default:
		return false
	}
}

func quantityValueForOption(option map[string]any, desired any) (int, bool) {
	qty, ok := int64Value(desired)
	if !ok {
		qty = firstNumber(desired)
		ok = qty > 0
	}
	minimum, minOK := int64Value(option["qty_minimum"])
	maximum, maxOK := int64Value(option["qty_maximum"])
	if !ok {
		for _, raw := range visibleZJMFSubs(option) {
			if sub, ok := raw.(map[string]any); ok {
				if qty, ok = int64Value(sub["qty_minimum"]); ok {
					break
				}
				qty = firstNumber(sub["option_name"])
				ok = qty > 0
				if ok {
					break
				}
			}
		}
	}
	if !ok && minOK {
		qty = minimum
		ok = true
	}
	if !ok {
		return 0, false
	}
	if minOK && qty < minimum {
		qty = minimum
	}
	if maxOK && maximum > 0 && qty > maximum {
		qty = maximum
	}
	return int(qty), true
}

func matchZJMFSubID(option map[string]any, desired any) (string, string) {
	subs := visibleZJMFSubs(option)
	if len(subs) == 0 {
		return "", ""
	}
	desiredText := strings.ToLower(strings.TrimSpace(fmt.Sprint(desired)))
	desiredNumber := firstNumber(desired)
	if desiredText != "" && desiredText != "<nil>" {
		for _, raw := range subs {
			sub, ok := raw.(map[string]any)
			if !ok {
				continue
			}
			if zjmfSubMatches(sub, desiredText) {
				return firstNonEmptyString(sub["id"]), displayZJMFLabel(firstNonNilValue(sub["option_name"], sub["name"]))
			}
		}
	}
	if desiredNumber > 0 {
		for _, raw := range subs {
			sub, ok := raw.(map[string]any)
			if !ok {
				continue
			}
			for _, value := range []any{sub["option_name"], sub["name"], sub["qty_minimum"], sub["qty_maximum"]} {
				if firstNumber(value) == desiredNumber {
					return firstNonEmptyString(sub["id"]), displayZJMFLabel(firstNonNilValue(sub["option_name"], sub["name"]))
				}
			}
		}
	}
	if first, ok := subs[0].(map[string]any); ok {
		return firstNonEmptyString(first["id"]), displayZJMFLabel(firstNonNilValue(first["option_name"], first["name"]))
	}
	return "", ""
}

func visibleZJMFSubs(option map[string]any) []any {
	subs := anySlice(firstNonNilValue(option["sub"], option["subs"]))
	visible := []any{}
	for _, raw := range subs {
		sub, ok := raw.(map[string]any)
		if !ok {
			continue
		}
		hidden := strings.ToLower(firstNonEmptyString(sub["hidden"], "0"))
		if hidden != "1" && hidden != "true" {
			visible = append(visible, sub)
		}
	}
	if len(visible) > 0 {
		return visible
	}
	return subs
}

func zjmfSubMatches(sub map[string]any, desired string) bool {
	rawName := firstNonNilValue(sub["option_name"], sub["name"])
	key := strings.ToLower(configFieldKey(rawName))
	label := strings.ToLower(displayZJMFLabel(rawName))
	values := []string{
		strings.ToLower(firstNonEmptyString(sub["id"])),
		strings.ToLower(strings.TrimSpace(fmt.Sprint(rawName))),
		key,
		label,
	}
	for _, value := range values {
		if value != "" && value == desired {
			return true
		}
	}
	return false
}

func displayZJMFLabel(value any) string {
	text := strings.TrimSpace(fmt.Sprint(value))
	if strings.Contains(text, "|") {
		text = strings.TrimSpace(strings.SplitN(text, "|", 2)[1])
	}
	if strings.Contains(text, "^") {
		parts := strings.Split(text, "^")
		for i := len(parts) - 1; i >= 0; i-- {
			if part := strings.TrimSpace(parts[i]); part != "" {
				return part
			}
		}
	}
	if text == "<nil>" {
		return ""
	}
	return text
}

func firstNumber(value any) int64 {
	text := strings.TrimSpace(fmt.Sprint(value))
	current := strings.Builder{}
	for _, r := range text {
		if r >= '0' && r <= '9' {
			current.WriteRune(r)
			continue
		}
		if current.Len() > 0 {
			break
		}
	}
	if current.Len() == 0 {
		return 0
	}
	parsed, _ := strconv.ParseInt(current.String(), 10, 64)
	return parsed
}

func parseJSONObject(value any) (map[string]any, bool) {
	switch typed := value.(type) {
	case map[string]any:
		return typed, true
	case string:
		var out map[string]any
		if err := json.Unmarshal([]byte(typed), &out); err == nil {
			return out, true
		}
	}
	return nil, false
}

func zjmfProductFromConfig(rawConfig map[string]any) map[string]any {
	if product, ok := rawConfig["product"].(map[string]any); ok {
		return product
	}
	return rawConfig
}

func randomChar(alphabet string) byte {
	if alphabet == "" {
		return 'a'
	}
	return alphabet[randomInt(int64(len(alphabet)))]
}

func randomInt(max int64) int64 {
	if max <= 0 {
		return 0
	}
	n, err := rand.Int(rand.Reader, big.NewInt(max))
	if err != nil {
		return 0
	}
	return n.Int64()
}

func mergeStringAny(base map[string]any, extra map[string]any) map[string]any {
	out := map[string]any{}
	for key, value := range base {
		out[key] = value
	}
	for key, value := range extra {
		out[key] = value
	}
	return out
}

func upstreamPayloadMap(instance *commerce.Instance) map[string]any {
	payload := map[string]any{}
	if len(instance.UpstreamPayload) > 0 {
		_ = json.Unmarshal(instance.UpstreamPayload, &payload)
	}
	if payload == nil {
		payload = map[string]any{}
	}
	return payload
}

func upstreamPayloadBlock(instance *commerce.Instance, key string) map[string]any {
	payload := upstreamPayloadMap(instance)
	if block, ok := payload[key].(map[string]any); ok {
		return block
	}
	return nil
}

func mergeInstancePayload(instance *commerce.Instance, key string, value any) {
	payload := upstreamPayloadMap(instance)
	payload[key] = value
	instance.UpstreamPayload = jsonRaw(payload)
}

func zjmfPayloadStatus(body map[string]any) int {
	status, ok := int64Value(body["status"])
	if !ok {
		return 0
	}
	return int(status)
}

func extractZJMFInvoiceID(body map[string]any) *int64 {
	data, _ := body["data"].(map[string]any)
	for _, value := range []any{data["invoiceid"], data["invoice_id"], body["invoiceid"], body["invoice_id"]} {
		if id, ok := int64Value(value); ok && id > 0 {
			return &id
		}
	}
	return nil
}

func extractZJMFHostIDs(body map[string]any) []int64 {
	data, _ := body["data"].(map[string]any)
	raw := firstNonNilValue(data["hostid"], data["host_id"], body["hostid"], body["host_id"])
	values := anySlice(raw)
	if len(values) == 0 && raw != nil {
		values = []any{raw}
	}
	out := []int64{}
	for _, value := range values {
		if id, ok := int64Value(value); ok && id > 0 {
			out = append(out, id)
		}
	}
	return out
}

func validateZJMFInvoiceAmount(invoiceDetail map[string]any, localAmountCents int) error {
	data := invoiceDetail
	if nested, ok := invoiceDetail["data"].(map[string]any); ok {
		data = nested
	}
	amount, ok := moneyValue(firstNonNilValue(data["invoice_total"], data["invoice_subtotal"]))
	if !ok {
		return fmt.Errorf("魔方财务账单校验失败：账单金额为空")
	}
	local := float64(localAmountCents) / 100
	if amount-local > 0.01 {
		return fmt.Errorf("魔方财务账单校验失败：上游账单 %.2f 大于本地实收 %.2f", amount, local)
	}
	return nil
}

func moneyValue(value any) (float64, bool) {
	switch typed := value.(type) {
	case float64:
		return typed, typed >= 0
	case int:
		return float64(typed), typed >= 0
	case int64:
		return float64(typed), typed >= 0
	case string:
		parsed, err := strconv.ParseFloat(strings.TrimSpace(typed), 64)
		return parsed, err == nil && parsed >= 0
	default:
		return 0, false
	}
}
