#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Модуль для расчетов рецептов кормления.

Перенесен из корневого recipe_calculator.py в пакет app.services
без изменения алгоритмов.
"""

from typing import List, Dict, Any, Optional, Tuple
import os
import json
import logging


logger = logging.getLogger(__name__)


def _calc_debug_enabled() -> bool:
    """
    Включает подробный трейс расчетов только по флагу окружения.
    Удобно для диагностики: set CALC_DEBUG=1
    """
    return str(os.getenv("CALC_DEBUG", "")).strip().lower() in {
        "1",
        "true",
        "yes",
        "on",
    }


def _calc_log(msg: str, **data) -> None:
    if not _calc_debug_enabled():
        return
    try:
        if data:
            logger.info(
                "[CALC] %s | %s",
                msg,
                json.dumps(data, ensure_ascii=False, default=str),
            )
        else:
            logger.info("[CALC] %s", msg)
    except Exception:
        # никогда не ломаем расчет из-за логов
        pass


def round_to_step5(value: float) -> float:
    """
    Округляет значение до кратного 5 кг
    """
    return round(value / 5) * 5


def calculate_ingredients(
    ingredients: List[Dict[str, Any]],
    heads_count: int,
    trip_percent: float,
    component_dry_matter_map: Optional[Dict[str, float]] = None,
) -> List[Dict[str, float]]:
    """
    Рассчитывает веса и сухое вещество для ингредиентов
    """
    calculated_ingredients = []

    _calc_log(
        "calculate_ingredients:start",
        heads_count=heads_count,
        trip_percent=trip_percent,
        ingredient_count=len(ingredients),
    )

    for idx, ing in enumerate(ingredients, 1):
        weight_per_head_raw = ing.get("weightPerHead", 0)
        weight_per_head = float(weight_per_head_raw)
        dry_matter_percent = float(ing.get("dryMatter", 0))
        component_id = ing.get("component_id")
        _calc_log(
            "calculate_ingredients:weightPerHead:input",
            idx=idx,
            component_id=component_id,
            weightPerHead_raw=weight_per_head_raw,
            weightPerHead_raw_type=type(weight_per_head_raw).__name__,
            weightPerHead_float=weight_per_head,
        )

        # Получаем dry_matter из компонента, если не указан и есть component_id
        dm_source = "payload"
        if dry_matter_percent == 0 and component_id and component_dry_matter_map:
            if component_id in component_dry_matter_map:
                dry_matter_percent = component_dry_matter_map[component_id]
                dm_source = "component_map"

        # Расчеты
        weight = weight_per_head * heads_count
        trip_weight = weight * (trip_percent / 100)
        dry_matter_per_head = weight_per_head * (dry_matter_percent / 100)

        if 0 < weight_per_head < 0.01:
            rounded_wph = round(weight_per_head, 3)
        else:
            rounded_wph = round(weight_per_head, 2)

        if 0 < dry_matter_per_head < 0.01:
            rounded_dm_per_head = round(dry_matter_per_head, 4)
        else:
            rounded_dm_per_head = round(dry_matter_per_head, 4)

        _calc_log(
            "ingredient:calc",
            idx=idx,
            component_id=component_id,
            dm_source=dm_source,
            input={
                "weightPerHead": weight_per_head,
                "dryMatterPercent": dry_matter_percent,
                "headsCount": heads_count,
                "tripPercent": trip_percent,
            },
            formula={
                "totalWeight": "weightPerHead * headsCount",
                "tripWeight": "totalWeight * (tripPercent/100)",
                "dryMatterPerHead": "weightPerHead * (dryMatterPercent/100)",
            },
            result={
                "totalWeight": round(weight, 2),
                "tripWeight": round(trip_weight, 2),
                "dryMatterPerHead": rounded_dm_per_head,
                "weightPerHead": rounded_wph,
            },
        )
        _calc_log(
            "calculate_ingredients:weightPerHead:output",
            idx=idx,
            component_id=component_id,
            weightPerHead_before_round=weight_per_head,
            rounded_wph=rounded_wph,
            rounded_wph_type=type(rounded_wph).__name__,
        )

        calculated_ingredients.append(
            {
                "totalWeight": round(weight, 2),
                "tripWeight": round(trip_weight, 2),
                "dryMatterPerHead": rounded_dm_per_head,
                "weightPerHead": rounded_wph,
            }
        )

    _calc_log("calculate_ingredients:end", ingredient_count=len(calculated_ingredients))
    return calculated_ingredients


def calculate_totals(
    calculated_ingredients: List[Dict[str, float]],
    ingredients: List[Dict[str, Any]],
) -> Dict[str, float]:
    """
    Рассчитывает общие итоги по ингредиентам
    """
    total_weight = 0.0
    total_trip_weight = 0.0
    total_dry_matter_per_head = 0.0
    total_weight_per_head = 0.0

    _calc_log(
        "calculate_totals:start", ingredient_count=len(calculated_ingredients)
    )

    for i, calc in enumerate(calculated_ingredients):
        total_weight += calc["totalWeight"]
        total_trip_weight += calc["tripWeight"]
        total_dry_matter_per_head += calc["dryMatterPerHead"]

        if "weightPerHead" in calc:
            total_weight_per_head += calc["weightPerHead"]
        elif i < len(ingredients):
            total_weight_per_head += float(ingredients[i].get("weightPerHead", 0))

    totals = {
        "totalWeight": round(total_weight, 2),
        "totalTripWeight": round(total_trip_weight, 2),
        "totalDryMatterPerHead": round(total_dry_matter_per_head, 2),
        "totalWeightPerHead": round(total_weight_per_head, 2),
    }

    _calc_log("calculate_totals:end", totals=totals)
    return totals


def calculate_unloading_groups(
    unloading_groups: List[Dict[str, Any]],
    total_trip_weight: float,
    heads_count: int,
) -> Tuple[List[Dict[str, float]], Dict[str, Any]]:
    """
    Рассчитывает веса для групп выгрузки.
    """
    calculated_groups: List[Dict[str, float]] = []
    total_percent = 0.0
    total_heads = 0.0
    total_weight_kg = 0.0

    for group in unloading_groups:
        group_type = group.get("distributionType", "percent")
        value = float(group.get("value", 0))

        calculated_weight = 0.0
        if group_type == "percent" and value > 0:
            calculated_weight = (total_trip_weight * value) / 100.0
        elif group_type == "heads" and value > 0 and heads_count > 0:
            calculated_weight = (total_trip_weight / heads_count) * value

        calculated_weight_rounded = round_to_step5(calculated_weight)

        calculated_groups.append({"calculatedWeight": calculated_weight_rounded})

        if group_type == "percent":
            total_percent += value
        else:
            total_heads += value

        total_weight_kg += calculated_weight

    unloading_totals = {
        "totalPercent": round(total_percent, 1),
        "totalHeads": int(total_heads),
        "totalWeightKg": round_to_step5(total_weight_kg),
    }

    return calculated_groups, unloading_totals


def calculate_recipe(
    ingredients: List[Dict[str, Any]],
    heads_count: int,
    trip_percent: float,
    unloading_groups: Optional[List[Dict[str, Any]]] = None,
    component_dry_matter_map: Optional[Dict[str, float]] = None,
    calculate_from_dry_matter: bool = False,
) -> Dict[str, Any]:
    """
    Главная функция для расчета всего рецепта.
    """
    _calc_log(
        "calculate_recipe:start",
        heads_count=heads_count,
        trip_percent=trip_percent,
        calculate_from_dry_matter=calculate_from_dry_matter,
        ingredient_count=len(ingredients or []),
        unloading_group_count=len(unloading_groups or []),
    )

    if unloading_groups is None:
        unloading_groups = []

    if calculate_from_dry_matter:
        calculated_ingredients = calculate_ingredients_from_dry_matter(
            ingredients, heads_count, trip_percent, component_dry_matter_map
        )
    else:
        calculated_ingredients = calculate_ingredients(
            ingredients, heads_count, trip_percent, component_dry_matter_map
        )

    totals = calculate_totals(calculated_ingredients, ingredients)

    calculated_groups, unloading_totals = calculate_unloading_groups(
        unloading_groups, totals["totalTripWeight"], heads_count
    )

    result: Dict[str, Any] = {
        "ingredients": calculated_ingredients,
        "totals": totals,
        "unloadingGroups": calculated_groups,
        "unloadingTotals": unloading_totals,
    }

    _calc_log("calculate_recipe:end", totals=totals, unloadingTotals=unloading_totals)
    return result


def calculate_ingredients_from_dry_matter(
    ingredients: List[Dict[str, Any]],
    heads_count: int,
    trip_percent: float,
    component_dry_matter_map: Optional[Dict[str, float]] = None,
) -> List[Dict[str, float]]:
    """
    Рассчитывает веса от сухого вещества (обратный расчет).
    """
    calculated_ingredients: List[Dict[str, float]] = []

    _calc_log(
        "calculate_ingredients_from_dry_matter:start",
        heads_count=heads_count,
        trip_percent=trip_percent,
        ingredient_count=len(ingredients),
    )

    for idx, ing in enumerate(ingredients, 1):
        dry_matter_per_head_raw = ing.get("dryMatterPerHead", 0)
        dry_matter_per_head = float(dry_matter_per_head_raw)
        dry_matter_percent = float(ing.get("dryMatter", 0))
        component_id = ing.get("component_id")
        _calc_log(
            "calculate_ingredients_from_dry_matter:input",
            idx=idx,
            component_id=component_id,
            dryMatterPerHead_raw=dry_matter_per_head_raw,
            dryMatterPercent=dry_matter_percent,
        )

        dm_source = "payload"
        if dry_matter_percent == 0 and component_id and component_dry_matter_map:
            if component_id in component_dry_matter_map:
                dry_matter_percent = component_dry_matter_map[component_id]
                dm_source = "component_map"

        EPSILON = 1e-6

        if dry_matter_percent > EPSILON:
            weight_per_head = (dry_matter_per_head * 100.0) / dry_matter_percent

            if weight_per_head < 0.01 and dry_matter_per_head >= 0.001:
                weight_per_head = 0.01
        else:
            if dry_matter_per_head > EPSILON:
                _calc_log(
                    "ingredient:warning_zero_dm_percent",
                    idx=idx,
                    component_id=component_id,
                    dry_matter_per_head=dry_matter_per_head,
                    message=(
                        "dry_matter_percent равен 0, но dry_matter_per_head > 0 - "
                        "веса обнулены"
                    ),
                )
            weight_per_head = 0.0

        weight = weight_per_head * heads_count
        trip_weight = weight * (trip_percent / 100.0)

        _calc_log(
            "ingredient:inverse_calc",
            idx=idx,
            component_id=component_id,
            dm_source=dm_source,
            input={
                "dryMatterPerHead": dry_matter_per_head,
                "dryMatterPercent": dry_matter_percent,
                "headsCount": heads_count,
                "tripPercent": trip_percent,
            },
        )

        calculated_ingredients.append(
            {
                "totalWeight": round(weight, 2),
                "tripWeight": round(trip_weight, 2),
                "weightPerHead": round(weight_per_head, 2),
                "dryMatterPerHead": round(dry_matter_per_head, 4),
            }
        )

    _calc_log(
        "calculate_ingredients_from_dry_matter:end",
        ingredient_count=len(calculated_ingredients),
    )
    return calculated_ingredients

