package commerce

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"sort"
	"strconv"
	"strings"
	"time"

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

func (s *Service) ProcessExpiredMemberCoupons(ctx context.Context, limit int) (map[string]any, error) {
	if limit <= 0 {
		limit = 200
	}
	summary := map[string]any{"checked": 0, "refunded_count": 0, "refunded_points": 0}
	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var coupons []PromoCode
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("issuer_type = ? AND is_enabled = ? AND expires_at IS NOT NULL AND expires_at <= ?", "member", true, time.Now().UTC()).Order("expires_at ASC, id ASC").Limit(limit).Find(&coupons).Error; err != nil {
			return err
		}
		summary["checked"] = len(coupons)
		txService := s.withDB(tx)
		for i := range coupons {
			points, err := txService.refundUnusedMemberCoupon(ctx, &coupons[i], "expired")
			if err != nil {
				return err
			}
			if points > 0 {
				summary["refunded_count"] = summary["refunded_count"].(int) + 1
				summary["refunded_points"] = summary["refunded_points"].(int) + points
			}
		}
		return nil
	})
	return summary, err
}

func (s *Service) NotifyExpiringCoupons(ctx context.Context, hoursBefore int, limit int) (map[string]any, error) {
	if hoursBefore <= 0 {
		hoursBefore = 24
	}
	if limit <= 0 {
		limit = 200
	}
	type expiringCoupon struct {
		ClaimID   int64     `gorm:"column:claim_id"`
		UserID    int64     `gorm:"column:user_id"`
		Code      string    `gorm:"column:code"`
		ExpiresAt time.Time `gorm:"column:expires_at"`
	}
	var rows []expiringCoupon
	now := time.Now().UTC()
	err := s.db.WithContext(ctx).Table("coupon_mall_claims AS c").
		Select("c.id AS claim_id, c.user_id, p.code, p.expires_at").
		Joins("JOIN promo_codes p ON p.id = c.promo_code_id").
		Where("c.status = ? AND p.expires_at IS NOT NULL AND p.expires_at > ? AND p.expires_at <= ?", "claimed", now, now.Add(time.Duration(hoursBefore)*time.Hour)).
		Order("p.expires_at ASC, c.id ASC").Limit(limit).Scan(&rows).Error
	if err != nil {
		return nil, err
	}
	summary := map[string]any{"checked": len(rows), "notified": 0, "skipped": 0}
	err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		for _, row := range rows {
			eventKey := fmt.Sprintf("coupon_expire_reminder:%d:%d", row.UserID, row.ClaimID)
			var count int64
			if err := tx.Table("notification_events").Where("event_key = ?", eventKey).Count(&count).Error; err != nil {
				return err
			}
			if count > 0 {
				summary["skipped"] = summary["skipped"].(int) + 1
				continue
			}
			event := map[string]any{"event_key": eventKey, "event_type": "coupon_expire_reminder", "user_id": row.UserID, "resource_type": "coupon_mall_claim", "resource_id": row.ClaimID, "created_at": now, "updated_at": now}
			if err := tx.Table("notification_events").Create(event).Error; err != nil {
				if isDuplicateKeyError(err) {
					summary["skipped"] = summary["skipped"].(int) + 1
					continue
				}
				return err
			}
			link := "/promotion"
			sender := "promotion"
			notification := Notification{UserID: row.UserID, Category: "promotion", Level: "warning", SenderType: "promotion", SenderName: &sender, Title: "优惠券即将过期", Content: fmt.Sprintf("优惠券 %s 将于 %s 过期，请及时使用。", row.Code, row.ExpiresAt.Local().Format("2006-01-02 15:04")), Link: &link, ActionURL: &link}
			if err := tx.Create(&notification).Error; err != nil {
				return err
			}
			summary["notified"] = summary["notified"].(int) + 1
		}
		return nil
	})
	return summary, err
}

func (s *Service) ProcessRechargeMaintenance(ctx context.Context) (map[string]any, error) {
	expireMinutes := s.settingInt(ctx, "recharge.expire_minutes", 30)
	if expireMinutes <= 0 {
		expireMinutes = 30
	}
	cutoff := time.Now().UTC().Add(-time.Duration(expireMinutes) * time.Minute)
	result := s.db.WithContext(ctx).Model(&RechargeOrder{}).Where("status = ? AND created_at <= ?", "pending", cutoff).Updates(map[string]any{"status": "expired", "updated_at": time.Now().UTC()})
	if result.Error != nil {
		return nil, result.Error
	}
	var pending int64
	if err := s.db.WithContext(ctx).Model(&RechargeOrder{}).Where("status = ?", "pending").Count(&pending).Error; err != nil {
		return nil, err
	}
	return map[string]any{"expired": result.RowsAffected, "pending": pending, "cutoff": cutoff, "reconciled": 0, "reconciliation_supported": false}, nil
}

func (s *Service) CheckCloudNodeHealth(ctx context.Context, limit int) (map[string]any, error) {
	if limit <= 0 {
		limit = 200
	}
	var servers []CloudServer
	if err := s.db.WithContext(ctx).Where("disabled = ?", false).Order("id ASC").Limit(limit).Find(&servers).Error; err != nil {
		return nil, err
	}
	summary := map[string]any{"checked": 0, "healthy": 0, "failed": 0, "failures": []map[string]any{}}
	for _, server := range servers {
		result, err := s.AdminTestCloudServer(ctx, server.ID, nil)
		ok := err == nil && result["ok"] == true
		message := "healthy"
		if err != nil {
			message = err.Error()
		} else if text := strings.TrimSpace(fmt.Sprint(result["message"])); text != "" && text != "<nil>" {
			message = text
		}
		status := "offline"
		if ok {
			status = "online"
			summary["healthy"] = summary["healthy"].(int) + 1
		} else {
			summary["failed"] = summary["failed"].(int) + 1
			failures := summary["failures"].([]map[string]any)
			if len(failures) < 30 {
				summary["failures"] = append(failures, map[string]any{"server_id": server.ID, "name": server.Name, "error": message})
			}
		}
		now := time.Now().UTC()
		if updateErr := s.db.WithContext(ctx).Model(&CloudServer{}).Where("id = ?", server.ID).Updates(map[string]any{"status": status, "last_check_at": now, "last_check_message": trimText(message, 500)}).Error; updateErr != nil {
			return nil, updateErr
		}
		summary["checked"] = summary["checked"].(int) + 1
	}
	return summary, nil
}

func (s *Service) SyncZJMFProductStocks(ctx context.Context, limit int) (map[string]any, error) {
	if limit <= 0 {
		limit = 500
	}
	var products []Product
	if err := s.db.WithContext(ctx).Where("api_type = ? AND cloud_server_id IS NOT NULL AND api_identifier IS NOT NULL", "zjmf_finance").Order("cloud_server_id ASC, id ASC").Limit(limit).Find(&products).Error; err != nil {
		return nil, err
	}
	summary := map[string]any{"checked": 0, "updated": 0, "unchanged": 0, "failed": 0, "skipped": 0, "tracked": 0, "unlimited": 0, "out_of_stock": 0, "hidden": 0, "retired": 0, "failures": []map[string]any{}, "updates": []map[string]any{}}
	for _, product := range products {
		pid := strings.TrimSpace(valueOrDefault(product.APIIdentifier, ""))
		if pid == "" || product.CloudServerID == nil {
			summary["skipped"] = summary["skipped"].(int) + 1
			continue
		}
		var server CloudServer
		if err := s.db.WithContext(ctx).Where("id = ? AND disabled = ?", *product.CloudServerID, false).First(&server).Error; err != nil || !isZJMFServer(server) {
			summary["skipped"] = summary["skipped"].(int) + 1
			continue
		}
		result, err := CallZJMFAPI(ctx, server, "/cart/get_product_config", "GET", map[string]any{"pid": pid}, 30*time.Second)
		if err != nil || result.StatusCode < 200 || result.StatusCode >= 300 {
			message := fmt.Sprint(err)
			if err == nil {
				message = upstreamMessage(result.Body, result.Text)
			}
			summary["failed"] = summary["failed"].(int) + 1
			failures := summary["failures"].([]map[string]any)
			if len(failures) < 30 {
				summary["failures"] = append(failures, map[string]any{"product_id": product.ID, "upstream_pid": pid, "error": message})
			}
			continue
		}
		upstreamProduct := zjmfStockProduct(result.Payload)
		if upstreamProduct == nil {
			summary["failed"] = summary["failed"].(int) + 1
			appendMaintenanceFailure(summary, map[string]any{"product_id": product.ID, "upstream_pid": pid, "error": "魔方财务库存响应未包含产品配置"})
			continue
		}
		tracked := truthyAny(upstreamProduct["stock_control"])
		stock := 0
		policy := "unlimited"
		if tracked {
			policy = "tracked"
			summary["tracked"] = summary["tracked"].(int) + 1
			stock = maxInt(0, intValueAny(upstreamProduct["qty"], 0))
			if stock == 0 {
				summary["out_of_stock"] = summary["out_of_stock"].(int) + 1
			}
		} else {
			summary["unlimited"] = summary["unlimited"].(int) + 1
		}
		if truthyAny(upstreamProduct["hidden"]) {
			summary["hidden"] = summary["hidden"].(int) + 1
		}
		if truthyAny(upstreamProduct["retired"]) {
			summary["retired"] = summary["retired"].(int) + 1
		}
		summary["checked"] = summary["checked"].(int) + 1
		if product.StockPolicy == policy && product.Stock == stock {
			summary["unchanged"] = summary["unchanged"].(int) + 1
			continue
		}
		oldPolicy, oldStock := product.StockPolicy, product.Stock
		if err := s.db.WithContext(ctx).Model(&Product{}).Where("id = ?", product.ID).Updates(map[string]any{"stock_policy": policy, "stock": stock}).Error; err != nil {
			return nil, err
		}
		summary["updated"] = summary["updated"].(int) + 1
		updates := summary["updates"].([]map[string]any)
		if len(updates) < 30 {
			summary["updates"] = append(updates, map[string]any{"product_id": product.ID, "name": product.Name, "upstream_pid": pid, "stock_policy": map[string]any{"old": oldPolicy, "new": policy}, "stock": map[string]any{"old": oldStock, "new": stock}, "hidden": truthyAny(upstreamProduct["hidden"]), "retired": truthyAny(upstreamProduct["retired"])})
		}
	}
	return summary, nil
}

func (s *Service) CollectInstanceRealData(ctx context.Context, limit int) (map[string]any, error) {
	if limit <= 0 {
		limit = 500
	}
	var instances []Instance
	if err := s.db.WithContext(ctx).Where("status IN ? AND upstream_id IS NOT NULL AND cloud_server_id IS NOT NULL", []string{"active", "suspended"}).Order("cloud_server_id ASC, id ASC").Limit(limit).Find(&instances).Error; err != nil {
		return nil, err
	}
	summary := map[string]any{"checked": 0, "collected": 0, "failed": 0, "cleaned": int64(0), "failures": []map[string]any{}}
	groups, serverIDs := groupRealDataInstances(instances)
	var servers []CloudServer
	if len(serverIDs) > 0 {
		if err := s.db.WithContext(ctx).Where("id IN ? AND disabled = ?", serverIDs, false).Find(&servers).Error; err != nil {
			return nil, err
		}
	}
	serverByID := map[int64]CloudServer{}
	for _, server := range servers {
		serverByID[server.ID] = server
	}
	for _, serverID := range serverIDs {
		group := groups[serverID]
		server, ok := serverByID[serverID]
		if !ok {
			for _, instance := range group {
				markRealDataFailure(summary, instance.ID, "云节点不存在或已禁用")
			}
			continue
		}
		if isZJMFServer(server) {
			s.collectZJMFRealDataGroup(ctx, server, group, summary)
			continue
		}
		s.collectDCIMRealDataGroup(ctx, server, group, summary)
	}
	cleanup := s.db.WithContext(ctx).Exec("DELETE FROM instance_real_data_snapshots WHERE collected_at < ?", time.Now().UTC().Add(-3*24*time.Hour))
	if cleanup.Error != nil {
		return nil, cleanup.Error
	}
	summary["cleaned"] = cleanup.RowsAffected
	return summary, nil
}

func appendMaintenanceFailure(summary map[string]any, failure map[string]any) {
	failures := summary["failures"].([]map[string]any)
	if len(failures) < 30 {
		summary["failures"] = append(failures, failure)
	}
}

func zjmfStockProduct(payload map[string]any) map[string]any {
	if payload == nil {
		return nil
	}
	for _, key := range []string{"data", "products", "product"} {
		if nested, ok := payload[key].(map[string]any); ok {
			if product := zjmfStockProduct(nested); product != nil {
				return product
			}
		}
		for _, raw := range asSlice(payload[key]) {
			if nested, ok := raw.(map[string]any); ok {
				if product := zjmfStockProduct(nested); product != nil {
					return product
				}
			}
		}
	}
	if _, hasStock := payload["stock_control"]; hasStock {
		return payload
	}
	if _, hasQty := payload["qty"]; hasQty {
		return payload
	}
	return nil
}

func groupRealDataInstances(instances []Instance) (map[int64][]Instance, []int64) {
	groups := map[int64][]Instance{}
	for _, instance := range instances {
		if instance.CloudServerID != nil {
			groups[*instance.CloudServerID] = append(groups[*instance.CloudServerID], instance)
		}
	}
	serverIDs := make([]int64, 0, len(groups))
	for serverID := range groups {
		serverIDs = append(serverIDs, serverID)
	}
	sort.Slice(serverIDs, func(i, j int) bool { return serverIDs[i] < serverIDs[j] })
	return groups, serverIDs
}

func (s *Service) collectDCIMRealDataGroup(ctx context.Context, server CloudServer, instances []Instance, summary map[string]any) {
	ids := make([]int64, 0, len(instances))
	for _, instance := range instances {
		if instance.UpstreamID != nil {
			ids = append(ids, *instance.UpstreamID)
		}
	}
	result, err := callDCIMPath(ctx, server, "/clouds/real_data", http.MethodPost, map[string]any{"id": ids}, 12*time.Second)
	if err != nil || result.StatusCode < 200 || result.StatusCode >= 300 {
		message := fmt.Sprint(err)
		if err == nil {
			message = fmt.Sprintf("HTTP %d: %s", result.StatusCode, trimText(result.Text, 200))
		}
		for _, instance := range instances {
			markRealDataFailure(summary, instance.ID, message)
		}
		return
	}
	byID := map[string]map[string]any{}
	for _, item := range extractRealDataItems(result.Body) {
		if id := strings.TrimSpace(fmt.Sprint(item["id"])); id != "" && id != "<nil>" {
			byID[id] = item
		}
	}
	for _, instance := range instances {
		summary["checked"] = summary["checked"].(int) + 1
		item := byID[strconv.FormatInt(*instance.UpstreamID, 10)]
		if item == nil {
			markRealDataFailureWithoutCheck(summary, instance.ID, "上游批量监控响应未包含该实例")
			continue
		}
		if err := s.writeRealDataSnapshot(ctx, instance, item); err != nil {
			markRealDataFailureWithoutCheck(summary, instance.ID, err.Error())
			continue
		}
		summary["collected"] = summary["collected"].(int) + 1
	}
}

func (s *Service) collectZJMFRealDataGroup(ctx context.Context, server CloudServer, instances []Instance, summary map[string]any) {
	password, err := decryptStoredSecret(server.PasswordEncrypted)
	if err != nil {
		for _, instance := range instances {
			markRealDataFailure(summary, instance.ID, err.Error())
		}
		return
	}
	client := &http.Client{Timeout: 20 * time.Second}
	jwt, err := zjmfLogin(ctx, client, server, password)
	if err != nil {
		for _, instance := range instances {
			markRealDataFailure(summary, instance.ID, err.Error())
		}
		return
	}
	for _, instance := range instances {
		summary["checked"] = summary["checked"].(int) + 1
		item, err := collectZJMFRealData(ctx, client, server, jwt, instance)
		if err != nil {
			markRealDataFailureWithoutCheck(summary, instance.ID, err.Error())
			continue
		}
		if item == nil {
			markRealDataFailureWithoutCheck(summary, instance.ID, "魔方财务未返回有效监控指标")
			continue
		}
		if err := s.writeRealDataSnapshot(ctx, instance, item); err != nil {
			markRealDataFailureWithoutCheck(summary, instance.ID, err.Error())
			continue
		}
		summary["collected"] = summary["collected"].(int) + 1
	}
}

func collectZJMFRealData(ctx context.Context, client *http.Client, server CloudServer, jwt string, instance Instance) (map[string]any, error) {
	if instance.UpstreamID == nil {
		return nil, fmt.Errorf("实例缺少上游 ID")
	}
	hostID := strconv.FormatInt(*instance.UpstreamID, 10)
	result := map[string]any{"id": *instance.UpstreamID, "provider": "zjmf_finance"}
	rawCharts := map[string]any{}
	for _, chartType := range []string{"cpu", "memory", "flow", "disk"} {
		chart, err := zjmfRequest(ctx, client, server, jwt, fmt.Sprintf("/v1/hosts/%s/module/charts", hostID), http.MethodGet, map[string]any{"type": chartType})
		if err != nil || chart.StatusCode < 200 || chart.StatusCode >= 300 || !zjmfSuccess(chart.Body) {
			continue
		}
		rawCharts[chartType] = chart.Body
		values := chartLastValues(chart.Body)
		data := extractPayload(chart.Body)
		scale := unitScale(fmt.Sprint(data["unit"]))
		switch chartType {
		case "cpu":
			if len(values) > 0 && values[0] != nil {
				result["cpu_usage"] = *values[0]
			}
		case "memory":
			if len(values) > 1 && values[0] != nil && values[1] != nil && *values[0] > 0 {
				result["memory_total"] = fmt.Sprint(*values[0])
				result["memory_usable"] = fmt.Sprint(maxFloat(0, *values[0]-*values[1]))
				result["memory_usage"] = *values[1] / *values[0] * 100
			}
		case "flow":
			if len(values) > 0 && values[0] != nil {
				result["current_in_bw"] = fmt.Sprintf("%.2fMbps", *values[0]*scale/(1024*1024))
			}
			if len(values) > 1 && values[1] != nil {
				result["current_out_bw"] = fmt.Sprintf("%.2fMbps", *values[1]*scale/(1024*1024))
			}
		case "disk":
			if len(values) > 0 && values[0] != nil {
				result["current_read_byte"] = *values[0] * scale / (1024 * 1024)
			}
			if len(values) > 1 && values[1] != nil {
				result["current_write_byte"] = *values[1] * scale / (1024 * 1024)
			}
		}
	}
	if len(result) <= 2 {
		return nil, nil
	}
	result["charts"] = rawCharts
	return result, nil
}

func extractRealDataItems(value any) []map[string]any {
	switch typed := value.(type) {
	case map[string]any:
		for _, key := range []string{"data", "list"} {
			if items := extractRealDataItems(typed[key]); len(items) > 0 {
				return items
			}
		}
		if typed["id"] != nil {
			return []map[string]any{typed}
		}
	case []any:
		items := make([]map[string]any, 0, len(typed))
		for _, raw := range typed {
			if item, ok := raw.(map[string]any); ok {
				items = append(items, item)
			}
		}
		return items
	}
	return nil
}

func chartLastValues(body map[string]any) []*float64 {
	series := asSlice(extractPayload(body)["list"])
	values := make([]*float64, 0, len(series))
	for _, rawSeries := range series {
		var last *float64
		for _, point := range asSlice(rawSeries) {
			value := point
			if item, ok := point.(map[string]any); ok {
				value = item["value"]
			}
			if parsed := nullableFloatAny(value); parsed != nil {
				number := parsed.(float64)
				last = &number
			}
		}
		values = append(values, last)
	}
	return values
}

func unitScale(unit string) float64 {
	unit = strings.ToLower(strings.TrimSpace(unit))
	if unit == "" || unit == "<nil>" {
		return 1
	}
	switch unit[0] {
	case 'k':
		return 1024
	case 'm':
		return 1024 * 1024
	case 'g':
		return 1024 * 1024 * 1024
	case 't':
		return 1024 * 1024 * 1024 * 1024
	default:
		return 1
	}
}

func maxFloat(left float64, right float64) float64 {
	if left > right {
		return left
	}
	return right
}

func (s *Service) writeRealDataSnapshot(ctx context.Context, instance Instance, payload map[string]any) error {
	raw, _ := json.Marshal(payload)
	now := time.Now().UTC()
	row := map[string]any{"instance_id": instance.ID, "upstream_id": instance.UpstreamID, "kvm": nullStringAny(payload["kvm"]), "cpu_usage": nullableFloatAny(payload["cpu_usage"]), "memory_usage": nullableFloatAny(payload["memory_usage"]), "memory_total": nullStringAny(payload["memory_total"]), "memory_usable": nullStringAny(payload["memory_usable"]), "current_in_bw": nullStringAny(payload["current_in_bw"]), "current_out_bw": nullStringAny(payload["current_out_bw"]), "current_read_byte": nullableFloatAny(payload["current_read_byte"]), "current_write_byte": nullableFloatAny(payload["current_write_byte"]), "collected_at": now, "raw_json": string(raw), "created_at": now, "updated_at": now}
	return s.db.WithContext(ctx).Table("instance_real_data_snapshots").Create(row).Error
}

func markRealDataFailure(summary map[string]any, instanceID int64, message string) {
	summary["checked"] = summary["checked"].(int) + 1
	markRealDataFailureWithoutCheck(summary, instanceID, message)
}

func markRealDataFailureWithoutCheck(summary map[string]any, instanceID int64, message string) {
	summary["failed"] = summary["failed"].(int) + 1
	appendMaintenanceFailure(summary, map[string]any{"instance_id": instanceID, "error": strings.TrimSpace(message)})
}

func nullableFloatAny(value any) any {
	if value == nil {
		return nil
	}
	var parsed float64
	if _, err := fmt.Sscan(strings.TrimSpace(fmt.Sprint(value)), &parsed); err != nil {
		return nil
	}
	return parsed
}
