package recipe

import (
	"context"
	"errors"
	"fmt"
	"math"

	"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"

	"gonum.org/v1/gonum/mat"
	"gonum.org/v1/gonum/optimize/convex/lp"
)

// RecipeGenerateManager 定义了配方生成器的能力。
// 它可以有多种实现，例如基于成本优化、基于生长性能优化等。
type RecipeGenerateManager interface {
	// GenerateRecipe 根据猪的营养需求和可用原料，生成一个配方。
	GenerateRecipe(ctx context.Context, pigType models.PigType, materials []models.RawMaterial) (*models.Recipe, error)
}

// recipeGenerateManagerImpl 是 RecipeGenerateManager 的默认实现。
// 它实现了基于成本最优的配方生成逻辑。
type recipeGenerateManagerImpl struct {
	ctx context.Context
}

// NewRecipeGenerateManager 创建一个默认的配方生成器实例。
func NewRecipeGenerateManager(ctx context.Context) RecipeGenerateManager {
	return &recipeGenerateManagerImpl{
		ctx: ctx,
	}
}

const (
	// internalFillerRawMaterialName 是内部虚拟填充料的名称。
	// 该填充料用于线性规划计算，确保总比例为100%，但不会出现在最终配方中。
	internalFillerRawMaterialName = "内部填充料_InternalFiller"
	// internalFillerNutrientID 是内部虚拟填充营养素的ID。
	// 使用 math.MaxUint32 作为一个极大的、不可能与实际ID冲突的值，用于关联填充料。
	internalFillerNutrientID = math.MaxUint32
)

// GenerateRecipe 根据猪的营养需求和可用原料，使用线性规划计算出成本最低的饲料配方。
func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType models.PigType, materials []models.RawMaterial) (*models.Recipe, error) {
	// 1. 基础校验
	if len(materials) == 0 {
		return nil, errors.New("无法生成配方：未提供任何原料")
	}
	if len(pigType.PigNutrientRequirements) == 0 {
		return nil, errors.New("无法生成配方：猪类型未设置营养需求")
	}

	// 收集猪类型所有有需求的营养素ID (包括min_requirement或max_requirement不为0的)。
	// 用于后续过滤掉完全不相关的原料。
	requiredNutrientIDs := make(map[uint32]bool)
	for _, req := range pigType.PigNutrientRequirements {
		requiredNutrientIDs[req.NutrientID] = true
	}

	// 过滤掉那些不包含猪类型任何所需营养素的原料。
	var filteredMaterials []models.RawMaterial
	for _, mat := range materials {
		hasRelevantNutrient := false
		for _, matNut := range mat.RawMaterialNutrients {
			// 检查原料是否包含猪类型所需的任何营养素
			if requiredNutrientIDs[matNut.NutrientID] {
				hasRelevantNutrient = true
				break
			}
		}
		// 如果原料包含至少一个猪类型需求的营养素，则保留
		if hasRelevantNutrient {
			filteredMaterials = append(filteredMaterials, mat)
		}
	}
	materials = filteredMaterials // 使用过滤后的原料列表

	if len(materials) == 0 {
		return nil, errors.New("无法生成配方：所有提供的原料都不包含猪类型所需的任何营养素，请检查原料配置或猪类型营养需求")
	}

	// 创建一个虚拟的、价格为0、不含任何实际营养素的填充料。
	// 其唯一目的是在LP求解中作为“凑数”的选项，确保总比例为100%，且不影响实际配方成本。
	fillerRawMaterial := models.RawMaterial{
		Model: models.Model{
			ID: math.MaxUint32 - 1, // 使用一个极大的、不可能与实际原料ID冲突的值
		},
		Name:           internalFillerRawMaterialName,
		Description:    "内部虚拟填充料，用于线性规划凑足100%比例，不含实际营养，价格为0。",
		ReferencePrice: 0.0, // 价格为0，确保LP优先选择它来凑数
		RawMaterialNutrients: []models.RawMaterialNutrient{
			{
				NutrientID: internalFillerNutrientID, // 关联一个虚拟营养素，确保其在LP中被识别，但其含量为0
				Value:      0.0,
			},
		},
	}
	materials = append(materials, fillerRawMaterial) // 将填充料添加到原料列表中

	// ---------------------------------------------------------
	// 2. 准备数据结构
	// ---------------------------------------------------------

	// materialNutrients 映射: 为了快速查找原料的营养含量 [RawMaterialID][NutrientID] => Value
	materialNutrients := make(map[uint32]map[uint32]float64)
	// materialIndex 映射: 原料ID到矩阵列索引的映射 (前 N 列对应 N 种原料)
	materialIndex := make(map[uint32]int)
	// materialIDs 列表: 记录原料ID以便结果回溯
	materialIDs := make([]uint32, len(materials))

	for i, m := range materials {
		materialIndex[m.ID] = i
		materialIDs[i] = m.ID
		materialNutrients[m.ID] = make(map[uint32]float64)
		for _, n := range m.RawMaterialNutrients {
			// 注意：这里假设 float32 转 float64 精度足够
			materialNutrients[m.ID][n.NutrientID] = float64(n.Value)
		}
	}

	// nutrientConstraints 存储营养素的下限和上限约束信息。
	type nutrientConstraintInfo struct {
		isMax      bool // true=上限约束(<=), false=下限约束(>=)
		nutrientID uint32
		limit      float64
	}
	var nutrientConstraints []nutrientConstraintInfo

	// 添加营养约束
	for _, req := range pigType.PigNutrientRequirements {
		// 排除内部虚拟填充营养素的约束，因为它不应有实际需求
		if req.NutrientID == internalFillerNutrientID {
			continue
		}

		// 添加下限约束 (Value >= Min)
		// 逻辑: Sum(Mat * x) >= Min -> Sum(Mat * x) - slack = Min
		nutrientConstraints = append(nutrientConstraints, nutrientConstraintInfo{
			isMax:      false,
			nutrientID: req.NutrientID,
			limit:      float64(req.MinRequirement),
		})

		// 添加上限约束 (Value <= Max)
		// 逻辑: Sum(Mat * x) <= Max -> Sum(Mat * x) + slack = Max
		if req.MaxRequirement > 0 {
			// 简单的校验，如果 Min > Max 则是逻辑矛盾，直接报错
			if req.MinRequirement > req.MaxRequirement {
				return nil, fmt.Errorf("营养素 %d 的需求配置无效: 最小需求 (%f) 大于最大需求 (%f)", req.NutrientID, req.MinRequirement, req.MaxRequirement)
			}
			nutrientConstraints = append(nutrientConstraints, nutrientConstraintInfo{
				isMax:      true,
				nutrientID: req.NutrientID,
				limit:      float64(req.MaxRequirement),
			})
		}
	}

	// maxAdditionConstraints 存储每个原料的最大添加比例约束 (x_i <= limit)。
	type maxAdditionConstraintInfo struct {
		materialColIndex int // 原料在 A 矩阵中的列索引
		limit            float64
	}
	var maxAdditionConstraints []maxAdditionConstraintInfo

	// 遍历所有原料，包括填充料，添加 MaxAdditionRatio 约束
	for _, mat := range materials {
		// 填充料不应受 MaxAdditionRatio 限制
		if mat.ID == fillerRawMaterial.ID {
			continue
		}

		// 只有当 MaxAdditionRatio > 0 时才添加约束。
		// 如果 MaxAdditionRatio 为 0 或负数，则表示该原料没有最大添加比例限制。
		if mat.MaxAdditionRatio > 0 {
			materialColIndex, ok := materialIndex[mat.ID]
			if !ok {
				return nil, fmt.Errorf("内部错误：未找到原料 %d (%s) 的列索引", mat.ID, mat.Name)
			}
			maxAdditionConstraints = append(maxAdditionConstraints, maxAdditionConstraintInfo{
				materialColIndex: materialColIndex,
				limit:            float64(mat.MaxAdditionRatio) / 100.0,
			})
		}
	}

	// ---------------------------------------------------------
	// 3. 构建线性规划矩阵 (Ax = b) 和 目标函数 (c)
	// ---------------------------------------------------------

	numMaterials := len(materials) // 此时已包含填充料
	numNutrientConstraints := len(nutrientConstraints)
	numMaxAdditionConstraints := len(maxAdditionConstraints)

	// 松弛变量数量 = 营养约束数量 + 最大添加比例约束数量
	numSlack := numNutrientConstraints + numMaxAdditionConstraints
	numCols := numMaterials + numSlack

	// 行数 = 1 (总量约束) + 营养约束数量 + 最大添加比例约束数量
	numRows := 1 + numNutrientConstraints + numMaxAdditionConstraints

	// A: 约束系数矩阵
	A := mat.NewDense(numRows, numCols, nil)
	// b: 约束值向量
	b := make([]float64, numRows)
	// c: 成本向量 (目标函数系数)
	c := make([]float64, numCols)

	// 填充 c (成本)
	for i, m := range materials {
		c[i] = float64(m.ReferencePrice)
	}
	// 松弛变量的成本为 0，Go 默认初始化为 0，无需操作

	// 填充 Row 0: 总量约束 (Sum(x) = 1)
	// 系数: 所有原料对应列为 1，松弛变量列为 0
	for j := 0; j < numMaterials; j++ {
		A.Set(0, j, 1.0)
	}
	b[0] = 1.0

	// currentConstraintRowIndex 记录当前正在填充的约束行索引，从1开始（0行被总量约束占用）。
	currentConstraintRowIndex := 1

	// 填充营养约束行
	for i, cons := range nutrientConstraints {
		rowIndex := currentConstraintRowIndex + i
		// 营养约束的松弛变量列紧跟在原料列之后
		slackColIndex := numMaterials + i

		b[rowIndex] = cons.limit

		// 设置原料系数
		for j, m := range materials {
			// 获取该原料这种营养素的含量，如果没有则为0
			val := materialNutrients[m.ID][cons.nutrientID]
			A.Set(rowIndex, j, val)
		}

		// 设置松弛变量系数
		// 如果是下限 (>=): Sum - s = Limit  => s系数为 -1
		// 如果是上限 (<=): Sum + s = Limit  => s系数为 +1
		if cons.isMax {
			A.Set(rowIndex, slackColIndex, 1.0)
		} else {
			A.Set(rowIndex, slackColIndex, -1.0)
		}
	}
	currentConstraintRowIndex += numNutrientConstraints // 推进当前约束行索引

	// 填充 MaxAdditionRatio 约束行
	for i, cons := range maxAdditionConstraints {
		rowIndex := currentConstraintRowIndex + i
		// MaxAdditionRatio 约束的松弛变量列在营养约束的松弛变量之后
		slackColIndex := numMaterials + numNutrientConstraints + i

		// 约束形式: x_j + s_k = Limit_j (其中 x_j 是原料 j 的比例，s_k 是松弛变量)
		A.Set(rowIndex, cons.materialColIndex, 1.0) // 原料本身的系数
		A.Set(rowIndex, slackColIndex, 1.0)         // 松弛变量的系数
		b[rowIndex] = cons.limit
	}

	// ---------------------------------------------------------
	// 4. 执行单纯形法求解
	// ---------------------------------------------------------

	// lp.Simplex 求解: minimize c^T * x subject to A * x = b, x >= 0
	_, x, err := lp.Simplex(c, A, b, 1e-8, nil)

	if err != nil {
		if errors.Is(err, lp.ErrInfeasible) {
			return nil, errors.New("无法生成配方：根据提供的原料，无法满足所有营养需求或最大添加比例限制 (无可行解)，请检查原料配置、营养需求或最大添加比例")
		}
		if errors.Is(err, lp.ErrUnbounded) {
			return nil, errors.New("计算错误：解无界 (可能数据配置有误，例如某个营养素没有上限约束且成本为负)")
		}
		return nil, fmt.Errorf("配方计算失败: %w", err)
	}

	// ---------------------------------------------------------
	// 5. 结果解析与构建
	// ---------------------------------------------------------

	// 统计实际原料数量（排除填充料）
	actualMaterialCount := 0
	for _, m := range materials {
		if m.ID != fillerRawMaterial.ID {
			actualMaterialCount++
		}
	}

	recipe := &models.Recipe{
		Name:              fmt.Sprintf("%s-%s - 自动计算配方", pigType.Breed.Name, pigType.AgeStage.Name), // 提供一个默认的名称
		Description:       fmt.Sprintf("基于 %d 种原料计算的最优成本配方。", actualMaterialCount),                  // 提供一个默认的描述
		RecipeIngredients: []models.RecipeIngredient{},
	}

	// 遍历原料部分的解 (前 numMaterials 个变量)
	totalPercentage := 0.0
	for i := 0; i < numMaterials; i++ {
		// 排除内部虚拟填充料，不将其加入最终配方
		if materialIDs[i] == fillerRawMaterial.ID {
			continue
		}

		proportion := x[i]

		// 忽略极小值 (浮点数误差)。
		// 调整过滤阈值到万分之一 (0.01%)，即小于0.0001的比例将被忽略。
		if proportion < 1e-4 {
			continue
		}

		// 记录总和用于最后的校验
		totalPercentage += proportion

		recipe.RecipeIngredients = append(recipe.RecipeIngredients, models.RecipeIngredient{
			RawMaterialID: materialIDs[i],
			// 比例: float64 -> float32
			Percentage: float32(proportion * 100.0),
		})
	}

	// 二次校验: 确保实际原料总量不超过 100% (允许小于100%因为填充料被移除)。
	// 允许略微超过100%的浮点误差，但不能显著超过。
	if totalPercentage > 1.0+1e-3 {
		return nil, fmt.Errorf("计算结果异常：实际原料总量超过 100%% (计算值: %.2f)，请检查算法或数据配置", totalPercentage)
	}

	return recipe, nil
}
