"""Core saponification calculator."""

from __future__ import annotations

from dataclasses import dataclass
from typing import List, Optional

from soap_calc.fragrance import calculate_fragrance
from soap_calc.models import (
    AdditiveResult,
    LiquidBreakdown,
    LyeResult,
    LyeType,
    Oil,
    OilEntry,
    PercentBase,
    Recipe,
    RecipeResult,
    Stage,
    WaterCalculationMode,
)
from soap_calc.properties import blend_fatty_acids, predict_properties
from soap_calc.skin_feel import analyze_skin_feel
from soap_calc.validation import validate


# ---------------------------------------------------------------------------
# Resolved oil entry (internal)
# ---------------------------------------------------------------------------


@dataclass
class _ResolvedOil:
    """An oil entry with its absolute weight computed from percentage."""

    oil: Oil
    percentage: float
    weight: float  # grams


def _resolve_entries(entries: List[OilEntry], total_weight: float) -> List[_ResolvedOil]:
    """Convert percentage-based oil entries to absolute weights.

    Args:
        entries: List of oil entries with percentage composition.
        total_weight: Total weight in grams to distribute across all entries.

    Returns:
        List of resolved oil entries with computed absolute weights in grams.
    """
    # Normalize percentages if they don't sum to 100?
    # For now assume they are roughly 100 or relative to the total_weight phase.
    # Validation handles the sum check. Here we just apply the % logic.
    # If the user provides 50% + 50% = 100%, then sum of weights = total_weight.
    return [
        _ResolvedOil(
            oil=e.oil,
            percentage=e.percentage,
            weight=round(total_weight * e.percentage / 100.0, 2),
        )
        for e in entries
    ]


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------


def calculate(
    recipe: Recipe,
    *,
    oil_weight: Optional[float] = None,
    run_validation: bool = True,
) -> RecipeResult:
    """Calculate the soap recipe results, including lye, water, and additives.

    This function performs the core saponification mathematics. It determines:
    1.  **Lye Amount**: Based on the SAP values of the oils and the selected lye type (NaOH, KOH, or Hybrid).
    2.  **Liquids**: Calculates water/liquid amount based on the specified mode (ratio, concentration, or % of oils).
    3.  **Additives**: Resolves additive amounts and any lye consumption (e.g., for citric acid).
    4.  **Properties**: Predicts soap characteristics (hardness, cleansing, etc.) based on fatty acid profiles.

    Args:
        recipe (Recipe): The completely defined soap recipe.
        oil_weight (float, optional): An optional override for the total oil weight in grams.
            If provided, the recipe is scaled to this weight. Defaults to None.
        run_validation (bool): If True, runs the `validate()` function on the recipe and
            includes any warnings in the result. Defaults to True.

    Returns:
        RecipeResult: A dataclass containing all calculation details, including:
            -   `lye`: Breakdown of NaOH and KOH amounts.
            -   `total_liquid`: Total grams of liquid to use.
            -   `total_batch_weight`: The final weight of the soap batter.
            -   `properties`: Predicted qualities of the cured soap.
            -   `warnings`: A list of potential issues (safety or quality).

    Examples:
        >>> from soap_calc import SoapCalculator, Recipe
        >>> recipe = Recipe.load("my_recipe.json")
        >>> result = calculate(recipe)
        >>> print(f"Lye needed: {result.lye.naoh_amount}g")
    """
    
    # 1. Total Batch Oil Weight
    # This is the sum of Base Oils + Superfat Oils (if any)
    total_batch_oil_weight = recipe.resolve_oil_weight(oil_weight)

    # 2. Determine Split (Base vs Superfat Phase)
    has_superfat_oils = len(recipe.superfat_oils) > 0
    
    # Check if base_oil_weight drives the sizing (no override, no mold)
    _core_mode = (
        oil_weight is None
        and recipe.mold is None
        and recipe.base_oil_weight is not None
        and has_superfat_oils
    )
    
    if has_superfat_oils:
        if _core_mode:
            # Core-mode: base = core directly, superfat added on top
            weight_base_phase = float(recipe.base_oil_weight)  # type: ignore[arg-type]
            weight_superfat_phase = round(
                weight_base_phase * recipe.superfat_pct / 100.0, 2
            )
            total_batch_oil_weight = weight_base_phase + weight_superfat_phase
        else:
            # Total-mode: split total by superfat_pct
            sf_ratio = recipe.superfat_pct / 100.0
            if sf_ratio >= 1.0:
                sf_ratio = 0.99
            weight_superfat_phase = round(total_batch_oil_weight * sf_ratio, 2)
            weight_base_phase = total_batch_oil_weight - weight_superfat_phase
        
        # Base is fully saponified (0% discount)
        base_lye_discount = 0.0
        effective_superfat = recipe.superfat_pct
    else:
        # Standard Logic
        weight_base_phase = total_batch_oil_weight
        weight_superfat_phase = 0.0
        
        # Applying discount to base
        base_lye_discount = recipe.superfat_pct
        effective_superfat = recipe.superfat_pct

    # 3. Resolve Base Oils
    resolved_base = _resolve_entries(recipe.oils, weight_base_phase)

    # 4. Calculate Lye (on Base Phase)
    lye_discount_factor = 1.0 - (base_lye_discount / 100.0)

    purity_naoh = recipe.naoh_purity / 100.0
    purity_koh = recipe.koh_purity / 100.0
    
    # Avoid division by zero
    if purity_naoh <= 0: purity_naoh = 1.0
    if purity_koh <= 0: purity_koh = 0.90

    if recipe.lye_type == LyeType.NAOH:
        naoh_pure = sum(r.weight * r.oil.sap_naoh for r in resolved_base)
        naoh_pure = naoh_pure * lye_discount_factor
        naoh = round(naoh_pure / purity_naoh, 2)
        koh = 0.0
    elif recipe.lye_type == LyeType.KOH:
        koh_pure = sum(r.weight * r.oil.sap_koh for r in resolved_base)
        koh_pure = koh_pure * lye_discount_factor
        koh = round(koh_pure / purity_koh, 2)
        naoh = 0.0
    else:  # DUAL
        naoh_frac = recipe.naoh_ratio / 100.0
        koh_frac = 1.0 - naoh_frac
        
        naoh_pure = sum(r.weight * r.oil.sap_naoh for r in resolved_base)
        koh_pure = sum(r.weight * r.oil.sap_koh for r in resolved_base)
        
        naoh_pure = naoh_pure * naoh_frac * lye_discount_factor
        koh_pure = koh_pure * koh_frac * lye_discount_factor
        
        naoh = round(naoh_pure / purity_naoh, 2)
        koh = round(koh_pure / purity_koh, 2)

    # Additive Lye Consumption
    # We calculate this LATER in the process (step 6), but we need it for the final LyeResult.
    # To fix this circularity without refactoring everything, we can do a pre-pass or move Step 6 up.
    # Actually, Step 6 (Additives) depends on `total_batch_oil_weight` and `total_liquid` (for percentages).
    # `total_lye` (step 4) doesn't depend on additives usually.
    # BUT if additives consume lye, we need to add that to the Total Lye reported.
    
    # We will just initialize it here and add it later? 
    # No, `LyeResult` is immutable-ish (dataclass). We construct it at line 149.
    # We should calculate additives *before* constructing LyeResult? 
    # But additives might depend on `total_lye` for "Percent of Total Batch".
    # Circular dependency: Lye <- Additives <- Lye.
    #
    # Approx solution: Calculate additives based on "Base Lye" (Saponification Lye).
    # Then add `additive_lye` to the final LyeResult.
    
    # Let's peek ahead at additives to calculate extra lye.
    extra_naoh = 0.0
    extra_koh = 0.0
    
    # ... Wait, `calculator.py` structure:
    # 4. Calculate Lye
    # 5. Liquid
    # 6. Additives
    # ...
    # 10. Result
    
    # `LyeResult` is created at step 4.
    # We can recreate or modify it at the end before returning.
    
    # Let's verify `LyeResult` usage. It's used in Step 5 (Liquid) -> `total_lye`.
    # Does extra lye require extra water? Usually yes (to dissolve it).
    # If using Citric Acid, you dissolve it in water, then add lye. The extra lye reacts.
    # The water calc should probably include the extra lye if it's "Water:Lye Ratio".
    
    # REFACTOR STRATEGY:
    # We'll do a preliminary Additive pass just to find `lye_consumed`.
    # But `additive.amount` might depend on `total_lye` (Total Batch %).
    # 
    # Simplification: Assume "Lye Consumed" additives don't use "Percent of Total Batch" mode.
    # They usually use "Percent of Oils".
    # So we can calculate them early.
    
    # Moving Step 6 logic (partial) up is risky.
    # Alternative: Update `LyeResult` and `Total Liquid` at the end?
    # This might be safer.
    
    total_lye = naoh + koh
    lye = LyeResult(naoh_amount=naoh, koh_amount=koh)

    # 5. Calculate Liquid (on Base Phase or Lye)
    mode = recipe.water_mode
    val = recipe.water_value
    
    if mode == WaterCalculationMode.WATER_LYE_RATIO:
        base_liquid = total_lye * val
    elif mode == WaterCalculationMode.LYE_CONCENTRATION:
        if val > 0 and val < 100:
            base_liquid = (total_lye / (val / 100.0)) - total_lye
        else:
            base_liquid = total_lye * 2.0
    else:  # WATER_PERCENT_OF_OILS
        # Use Base Phase weight logic
        base_liquid = weight_base_phase * (val / 100.0)

    # Apply liquid discount
    if recipe.liquid_discount_pct > 0:
        base_liquid *= (1.0 - recipe.liquid_discount_pct / 100.0)
    
    total_liquid = round(base_liquid, 2)

    # Liquid Breakdown
    liquid_breakdown: List[LiquidBreakdown] = []
    total_liq_pct = sum(l.percentage for l in recipe.liquids)
    if total_liq_pct > 0:
         for liq in recipe.liquids:
            amount = round(total_liquid * liq.percentage / total_liq_pct, 2)
            liquid_breakdown.append(LiquidBreakdown(name=liq.name, amount=amount, handling_notes=liq.handling_notes))
    else:
        liquid_breakdown.append(LiquidBreakdown(name="Water", amount=total_liquid))


    # 6. Process Additives
    # Note: Percentages usually relative to "Oils". In reformulation, this likely means TOTAL Batch Oils?
    # If I say "add 3% fragrance", I usually mean 3% of the total soaping oils.
    # So we use `total_batch_oil_weight`.
    
    additive_results: List[AdditiveResult] = []
    additive_weight = 0.0
    
    for add in recipe.additives:
        amt = 0.0
        if add.amount is not None:
            amt = add.amount
        elif add.percentage is not None:
            base_mass = 0.0
            if add.percent_base == PercentBase.OIL_WEIGHT:
                base_mass = total_batch_oil_weight
            elif add.percent_base == PercentBase.LIQUID_WEIGHT:
                base_mass = total_liquid
            else: # TOTAL_BATCH (Approx)
                base_mass = total_batch_oil_weight + total_lye + total_liquid
            
            amt = base_mass * add.percentage / 100.0
        
        # Lye Consumption (Reactive Additives)
        lye_consumed = 0.0
        # Check for manual adjustment on the specific entry, OR lookup from DB info
        # The Additive model has `lye_adjustment`. If not set, we might want to check the DB.
        # But `add` object here is from recipe, might not have DB info attached unless we look it up.
        # However, `validation.py` looks up additives. `calculator.py` usually just processes what's there.
        # To support this properly, we should look up the additive info if available.
        from soap_calc.additives import get_additive
        info = get_additive(add.name)
        
        # Priority: Manual override > DB value > 0
        current_adjustment = 0.0
        if add.lye_adjustment is not None:
             current_adjustment = add.lye_adjustment
        elif info and info.lye_adjustment:
             current_adjustment = info.lye_adjustment
             
        if current_adjustment > 0:
             lye_consumed = round(amt * current_adjustment, 2)

        amt = round(amt, 2)
        additive_weight += amt
        additive_results.append(
            AdditiveResult(
                name=add.name, 
                amount=amt, 
                stage=add.stage, 
                lye_consumed=lye_consumed,
                notes=add.notes
            )
        )

    # 7. Process Superfat Oils
    superfat_results: List[AdditiveResult] = []
    
    # Calculate extra lye from additives (assumed NaOH for now as most adjustments are standardized to NaOH)
    # If we need KOH adjustment, we'd need to convert using ratio ~1.403 or support specific KOH fields.
    # For now, we add to NaOH amount (or KOH if 100% KOH?).
    # Safety: ensure we have enough lye.
    
    total_lye_consumed = sum(a.lye_consumed for a in additive_results)
    
    if total_lye_consumed > 0:
        if recipe.lye_type == LyeType.KOH:
            # Convert NaOH grams to KOH grams
            # Ratio: 56.11 / 40.00 = 1.40275
            extra_koh = round(total_lye_consumed * 1.40275, 2)
            lye = LyeResult(naoh_amount=lye.naoh_amount, koh_amount=round(lye.koh_amount + extra_koh, 2))
        else:
            # Add to NaOH
            lye = LyeResult(naoh_amount=round(lye.naoh_amount + total_lye_consumed, 2), koh_amount=lye.koh_amount)
        
        # Recalculate Total Lye for liquid calculation?
        # Liquid was calculated in Step 5.
        # If we add lye now, liquid might be "low" relative to new total lye.
        # But usually we dissolve the additive in *extra* water or the existing water is sufficient.
        # Safety-wise: adding lye without adding water results in higher concentration.
        # If the user follows "Water:Lye Ratio" (e.g. 2:1), they expect 2 * (BaseLye + AdditiveLye).
        # We should probably update total_liquid if in Ratio mode?
        # This gets complex. For now, we just ensure the LYE amount is correct for saponification/neutralization.
        # The user can adjust water if needed, or we rely on the fact that additives like Citric Acid
        # are dissolved in "some water" (deducted from total liquid or extra).
        # Let's LEAVE total_liquid as is for now to avoid breaking existing liquid logic, 
        # but the Total Batch Weight will increase by the extra lye weight.
        
    if has_superfat_oils and weight_superfat_phase > 0:
        resolved_sf = _resolve_entries(recipe.superfat_oils, weight_superfat_phase)
        for r in resolved_sf:
            superfat_results.append(
                AdditiveResult(
                    name=r.oil.name,
                    amount=r.weight,
                    stage=Stage.POST_COOK,
                    notes="Superfat Phase"
                )
            )

    # 8. Fragrances
    fragrance_results: List[AdditiveResult] = []
    fragrance_warnings: List[str] = []
    fragrance_weight = 0.0
    
    for frag in recipe.fragrances:
        # Calculate relative to TOTAL oils
        f_res = calculate_fragrance(frag, total_batch_oil_weight)
        fragrance_weight += f_res.amount
        fragrance_results.append(
             AdditiveResult(
                 name=f_res.name,
                 amount=f_res.amount,
                 stage=frag.stage,
                 notes=frag.notes
             )
        )
        if f_res.warning:
            fragrance_warnings.append(f_res.warning)

    # 9. Properties
    # Properties should be based on the BASE PHASE (Saponified).
    # Superfat oils don't contribute to the crystalline soap structure properties (hardness/lather).
    # They act as conditioners.
    # So we pass `resolved_base` (converted back to simple OilEntries for the predictor)
    # The predictor expects a list of OilEntries with percentages.
    # Since `resolved_base` has absolute weights, we can reconstruct the percentages equivalent
    # relative to the BASE PHASE.
    
    # Actually, `recipe.oils` ALREADY contains the percentages relative to the Base Phase!
    # (Since we resolve `weight_base_phase` using them).
    # So we can just use `recipe.oils` for property prediction.
    
    properties = predict_properties(recipe.oils)
    fa_profile = blend_fatty_acids(recipe.oils)

    # 10. Total Weight & Result
    total_batch_weight = round(
        weight_base_phase + 
        weight_superfat_phase + 
        lye.total_weight + 
        total_liquid + 
        additive_weight + 
        fragrance_weight, 
        2
    )
    
    warnings: List[str] = fragrance_warnings[:]
    if run_validation:
        warnings.extend(validate(recipe))
        
    filtered_warnings = []
    if recipe.ignore_warnings:
        for w in warnings:
            if not any(token.lower() in w.lower() for token in recipe.ignore_warnings):
                filtered_warnings.append(w)
    else:
        filtered_warnings = warnings

    skin_feel = (
        analyze_skin_feel(
            recipe.superfat_oils,
            base_oils=recipe.oils,
            superfat_pct=recipe.superfat_pct,
        )
        if recipe.superfat_oils
        else None
    )

    return RecipeResult(
        lye=lye,
        total_liquid=total_liquid,
        liquid_breakdown=liquid_breakdown,
        total_oil_weight=weight_base_phase, # Saponified weight
        total_batch_weight=total_batch_weight,
        fatty_acid_profile=fa_profile,
        properties=properties,
        additives=additive_results,
        fragrances=fragrance_results,
        superfat_oils=superfat_results,
        effective_superfat_pct=effective_superfat,
        superfat_analysis=skin_feel,
        warnings=filtered_warnings
    )
