<?php

namespace App\Support;

use App\Models\Batch;
use App\Models\BatchKpiSnapshot;
use App\Models\BatchSplitEvent;
use App\Models\BatchSplitSession;
use App\Models\DailyLog;
use App\Models\FinanceCostEntry;
use App\Models\FinanceHarvestEntry;
use App\Models\FinanceSalesEntry;
use Illuminate\Support\Carbon;

class BatchKpiSnapshotService
{
    private const STAGE_FCR_TARGETS = [
        '0x0' => 1.2,
        '2x2' => 1.35,
        '4x4' => 1.55,
        '6x6' => 1.75,
    ];

    public static function syncBatch(Batch $batch, ?string $date = null): ?BatchKpiSnapshot
    {
        $snapshotDate = $date ?: now()->toDateString();
        $payload = self::buildSnapshotPayload($batch, $snapshotDate);

        return BatchKpiSnapshot::query()->updateOrCreate(
            [
                'batch_id' => (int) $batch->id,
                'snapshot_date' => $snapshotDate,
            ],
            $payload
        );
    }

    public static function syncMany(iterable $batches, ?string $date = null): void
    {
        foreach ($batches as $batch) {
            if ($batch instanceof Batch) {
                self::syncBatch($batch, $date);
            }
        }
    }

    public static function ensureHistoricalCoverageForUser(int $userId, int $maxDays = 365): void
    {
        $batches = Batch::query()
            ->with('tank')
            ->where('user_id', $userId)
            ->whereIn('status', ['active', 'closing', 'closed'])
            ->orderBy('id')
            ->get();

        foreach ($batches as $batch) {
            self::backfillBatchHistory($batch, null, null, $maxDays);
        }
    }

    public static function backfillBatchHistory(Batch $batch, ?string $fromDate = null, ?string $toDate = null, int $maxDays = 365): void
    {
        $end = $toDate ? Carbon::parse($toDate) : Carbon::today();
        $startedAt = null;
        if ($batch->started_at) {
            $startedAt = is_object($batch->started_at) && method_exists($batch->started_at, 'toDateString')
                ? $batch->started_at->toDateString()
                : (string) $batch->started_at;
        }

        $start = $fromDate
            ? Carbon::parse($fromDate)
            : ($startedAt ? Carbon::parse($startedAt) : $end->copy());

        if ($start->gt($end)) {
            return;
        }

        if ($start->diffInDays($end) > $maxDays) {
            $start = $end->copy()->subDays($maxDays);
        }

        $existing = BatchKpiSnapshot::query()
            ->where('batch_id', (int) $batch->id)
            ->whereBetween('snapshot_date', [$start->toDateString(), $end->toDateString()])
            ->pluck('snapshot_date')
            ->map(fn ($date) => (string) $date)
            ->all();
        $existingIndex = array_fill_keys($existing, true);

        $cursor = $start->copy();
        while ($cursor->lte($end)) {
            $dateIso = $cursor->toDateString();
            if (! isset($existingIndex[$dateIso])) {
                self::syncBatch($batch, $dateIso);
            }
            $cursor->addDay();
        }
    }

    public static function latestSnapshotsForUser(int $userId)
    {
        return BatchKpiSnapshot::query()
            ->where('user_id', $userId)
            ->orderByDesc('snapshot_date')
            ->orderByDesc('id')
            ->get()
            ->groupBy(fn (BatchKpiSnapshot $snapshot) => (int) $snapshot->batch_id)
            ->map(fn ($rows) => $rows->first())
            ->values();
    }

    public static function growthTrendsForUser(int $userId)
    {
        return BatchKpiSnapshot::query()
            ->where('user_id', $userId)
            ->orderBy('batch_id')
            ->orderBy('snapshot_date')
            ->get()
            ->groupBy(fn (BatchKpiSnapshot $snapshot) => (int) $snapshot->batch_id)
            ->flatMap(function ($rows) {
                return $rows
                    ->sortByDesc('snapshot_date')
                    ->take(8)
                    ->sortBy('snapshot_date')
                    ->map(function (BatchKpiSnapshot $snapshot) {
                        return [
                            'batch_id' => (int) $snapshot->batch_id,
                            'date' => (string) $snapshot->snapshot_date,
                            'avg_weight_g' => self::round2((float) $snapshot->avg_weight_g),
                            'growth_rate_g_per_day' => self::round2((float) $snapshot->growth_rate_g_per_day),
                            'mortality_rate_pct' => self::round2((float) $snapshot->mortality_rate_pct),
                            'feed_consumption_g' => self::round2((float) $snapshot->actual_feed_g),
                        ];
                    })
                    ->values();
            })
            ->values();
    }

    private static function buildSnapshotPayload(Batch $batch, string $snapshotDate): array
    {
        $batch->refresh();

        $userId = (int) $batch->user_id;
        $fishAlive = BatchMetrics::liveFishCountAtDate($batch, $snapshotDate);
        $avgWeightG = max(0.0, (float) ($batch->avg_weight_current_g ?? 0));
        $biomassKg = self::round2(($fishAlive * $avgWeightG) / 1000);
        $totalCost = self::costTotals($userId, (int) $batch->id, (int) $batch->tank_id, $snapshotDate);
        $costPerKg = $biomassKg > 0 ? self::round2($totalCost / $biomassKg) : 0.0;
        $salePrice = self::effectiveSalePrice($userId, (int) $batch->id, (int) $batch->tank_id, $snapshotDate);
        $realizedRevenue = self::realizedRevenue($userId, (int) $batch->id, (int) $batch->tank_id, $snapshotDate);
        $harvestedKg = self::harvestedKg($userId, (int) $batch->id, $snapshotDate);
        $soldKg = self::soldKg($userId, (int) $batch->id, (int) $batch->tank_id, $snapshotDate);
        $cashRecoveryPct = $totalCost > 0 ? self::round2(($realizedRevenue / $totalCost) * 100) : null;
        $netCashflow = self::round2($realizedRevenue - $totalCost);
        $projectedRevenue = self::round2($biomassKg * $salePrice);
        $projectedMargin = self::round2($projectedRevenue - $totalCost);
        $marginPerKg = $biomassKg > 0 ? self::round2($projectedMargin / $biomassKg) : 0.0;
        $roiPct = $totalCost > 0 ? self::round2(($netCashflow / $totalCost) * 100) : null;
        $projectedRoiPct = $totalCost > 0 ? self::round2(($projectedMargin / $totalCost) * 100) : null;
        $breakEvenWeightG = $fishAlive > 0 ? self::round2((($totalCost / max(0.000001, $salePrice)) / $fishAlive) * 1000) : 0.0;
        $growthRate = self::estimateGrowthRate($batch, $snapshotDate);

        $targetWeightG = 500.0;
        $daysToTarget = null;
        if ($growthRate > 0 && $avgWeightG < $targetWeightG) {
            $daysToTarget = (int) ceil(($targetWeightG - $avgWeightG) / $growthRate);
        } elseif ($avgWeightG >= $targetWeightG) {
            $daysToTarget = 0;
        }

        $hiddenLossFish = self::hiddenLossFish($userId, (int) $batch->id, $snapshotDate);
        $hiddenLossBiomassKg = self::round2(($hiddenLossFish * $avgWeightG) / 1000);
        $mortalityRate = self::mortalityRate($batch, $fishAlive);
        $recommendedFeed = FeedingRecommendationEngine::recommendForBatch($batch, $snapshotDate) ?? [];
        $stageCode = isset($recommendedFeed['feed_type']) ? (string) $recommendedFeed['feed_type'] : null;
        $stageDaysElapsed = max(0, ((int) ($recommendedFeed['stage_weeks_elapsed'] ?? 0)) * 7);
        $stageStartWeightG = max(0.0, (float) ($recommendedFeed['stage_start_weight_estimated_g'] ?? 0));
        $recommendedFeedG = self::round2((float) ($recommendedFeed['daily_dose_g'] ?? 0));
        $actualFeedG = self::round2((float) (DailyLog::query()
            ->where('user_id', $userId)
            ->where('batch_id', (int) $batch->id)
            ->whereDate('date', $snapshotDate)
            ->sum('feed_given_g') ?: 0.0));
        $feedVarianceG = self::round2($actualFeedG - $recommendedFeedG);
        $cumulativeFeedG = self::round2((float) (DailyLog::query()
            ->where('user_id', $userId)
            ->where('batch_id', (int) $batch->id)
            ->whereDate('date', '<=', $snapshotDate)
            ->sum('feed_given_g') ?: 0.0));
        $cumulativeFeedKg = self::round2($cumulativeFeedG / 1000);
        $initialBiomassKg = self::round2((
            max(0, (int) ($batch->fish_count_initial ?? 0)) *
            max(0.0, (float) ($batch->avg_weight_initial_g ?? 0))
        ) / 1000);
        $biomassGainKg = self::round2(max(0.0, $biomassKg - $initialBiomassKg));
        $fcrReal = $biomassGainKg > 0 ? self::round2($cumulativeFeedKg / $biomassGainKg) : null;

        $tank = $batch->relationLoaded('tank') ? $batch->tank : $batch->tank()->first();
        $volumeM3 = $tank ? max(0.0, ((float) ($tank->volume_liters ?? 0)) / 1000) : null;
        $densityFishM3 = $volumeM3 && $volumeM3 > 0 ? self::round2($fishAlive / $volumeM3) : null;
        $densityBiomassKgM3 = $volumeM3 && $volumeM3 > 0 ? self::round2($biomassKg / $volumeM3) : null;
        $cycleDay = 0;
        $capitalLockedDays = 0.0;
        $batchStatus = (string) ($batch->status ?? 'active');
        $cycleClosedAt = null;
        if ($batch->started_at) {
            $startedAtIso = is_object($batch->started_at) && method_exists($batch->started_at, 'toDateString')
                ? $batch->started_at->toDateString()
                : (string) $batch->started_at;
            $cycleDay = max(0, Carbon::parse($snapshotDate)->diffInDays($startedAtIso));
            $capitalLockedDays = (float) $cycleDay;
        }
        if ($batch->ended_at) {
            $cycleClosedAt = is_object($batch->ended_at) && method_exists($batch->ended_at, 'toDateString')
                ? $batch->ended_at->toDateString()
                : (string) $batch->ended_at;
            if ($startedAtIso ?? null) {
                $capitalLockedDays = (float) max(0, Carbon::parse($cycleClosedAt)->diffInDays($startedAtIso));
            }
        } elseif ($batchStatus === 'closed') {
            $cycleClosedAt = $snapshotDate;
        }
        $harvestCompletionPct = ($harvestedKg > 0 || $biomassKg > 0)
            ? self::round2(($harvestedKg / max(0.000001, $harvestedKg + $biomassKg)) * 100)
            : null;
        $salesRealizationPct = $harvestedKg > 0
            ? self::round2(min(100.0, ($soldKg / $harvestedKg) * 100))
            : null;
        $cycleClosureReady = $batchStatus === 'closed'
            || ($batchStatus === 'closing')
            || ($fishAlive <= 0)
            || ($biomassKg <= 1.0)
            || (($harvestCompletionPct ?? 0) >= 95.0);
        $cycleClosureState = $batchStatus === 'closed'
            ? 'CLOSED'
            : ($cycleClosureReady ? 'READY_TO_CLOSE' : 'OPEN');
        $uniformity = self::latestUniformityMetrics($userId, (int) $batch->id, $snapshotDate);
        $stageStartDate = null;
        if (! empty($recommendedFeed['stage_start_week_index']) || (int) ($recommendedFeed['stage_start_week_index'] ?? 0) === 0) {
            if (! empty($startedAtIso)) {
                $stageStartDate = Carbon::parse($startedAtIso)->addDays(((int) ($recommendedFeed['stage_start_week_index'] ?? 0)) * 7)->toDateString();
            }
        }
        $stageFeedKg = $stageStartDate ? self::sumFeedKg($userId, (int) $batch->id, $stageStartDate, $snapshotDate) : null;
        $stageBiomassGainKg = $stageStartWeightG > 0 && $avgWeightG > $stageStartWeightG
            ? self::round2(($fishAlive * ($avgWeightG - $stageStartWeightG)) / 1000)
            : null;
        $stageFcrReal = ($stageFeedKg !== null && $stageBiomassGainKg && $stageBiomassGainKg > 0)
            ? self::round2($stageFeedKg / $stageBiomassGainKg)
            : null;
        $lastSplitPerf = self::lastSplitPerformance($userId, (int) $batch->id, $snapshotDate, $fishAlive, $avgWeightG);
        $stageFcrTarget = self::stageFcrTarget($stageCode);
        $stageFcrGap = ($stageFcrReal !== null && $stageFcrTarget !== null)
            ? self::round2($stageFcrReal - $stageFcrTarget)
            : null;
        $postSplitFcrGap = ($lastSplitPerf['post_split_fcr_real'] !== null && $stageFcrTarget !== null)
            ? self::round2($lastSplitPerf['post_split_fcr_real'] - $stageFcrTarget)
            : null;
        $targetProgressPct = $targetWeightG > 0 ? self::round2(min(100.0, ($avgWeightG / $targetWeightG) * 100)) : null;

        $status = 'HEALTHY';
        if ($costPerKg >= $salePrice || $marginPerKg < 0) {
            $status = 'CRITICAL';
        } elseif ($costPerKg >= $salePrice * 0.85) {
            $status = 'RISK';
        }

        return [
            'user_id' => $userId,
            'batch_id' => (int) $batch->id,
            'tank_id' => (int) $batch->tank_id,
            'batch_status' => $batchStatus,
            'snapshot_date' => $snapshotDate,
            'cycle_day' => $cycleDay,
            'stage_code' => $stageCode,
            'last_split_date' => $uniformity['last_split_date'],
            'stage_days_elapsed' => $stageDaysElapsed,
            'uniformity_cv_pct' => $uniformity['uniformity_cv_pct'],
            'weight_spread_g' => $uniformity['weight_spread_g'],
            'days_since_last_split' => $lastSplitPerf['days_since_last_split'],
            'post_split_biomass_gain_kg' => $lastSplitPerf['post_split_biomass_gain_kg'],
            'post_split_feed_kg' => $lastSplitPerf['post_split_feed_kg'],
            'post_split_fcr_real' => $lastSplitPerf['post_split_fcr_real'],
            'post_split_fcr_target' => $stageFcrTarget,
            'post_split_fcr_gap' => $postSplitFcrGap,
            'stage_biomass_gain_kg' => $stageBiomassGainKg,
            'stage_feed_kg' => $stageFeedKg,
            'stage_fcr_real' => $stageFcrReal,
            'stage_fcr_target' => $stageFcrTarget,
            'stage_fcr_gap' => $stageFcrGap,
            'fish_alive' => $fishAlive,
            'avg_weight_g' => self::round2($avgWeightG),
            'biomass_kg' => $biomassKg,
            'total_cost_crc' => $totalCost,
            'realized_revenue_crc' => $realizedRevenue,
            'harvested_kg_cumulative' => $harvestedKg,
            'sold_kg_cumulative' => $soldKg,
            'cash_recovery_pct' => $cashRecoveryPct,
            'capital_locked_days' => self::round2($capitalLockedDays),
            'cycle_closed_at' => $cycleClosedAt,
            'harvest_completion_pct' => $harvestCompletionPct,
            'sales_realization_pct' => $salesRealizationPct,
            'cycle_closure_ready' => $cycleClosureReady,
            'cycle_closure_state' => $cycleClosureState,
            'target_progress_pct' => $targetProgressPct,
            'net_cashflow_crc' => $netCashflow,
            'roi_pct' => $roiPct,
            'projected_roi_pct' => $projectedRoiPct,
            'cost_per_kg_crc' => $costPerKg,
            'projected_revenue_crc' => $projectedRevenue,
            'projected_margin_crc' => $projectedMargin,
            'margin_per_kg_crc' => $marginPerKg,
            'break_even_weight_g' => $breakEvenWeightG,
            'days_to_target_estimated' => $daysToTarget,
            'status' => $status,
            'growth_rate_g_per_day' => $growthRate,
            'mortality_rate_pct' => $mortalityRate,
            'hidden_loss_fish' => $hiddenLossFish,
            'hidden_loss_biomass_kg' => $hiddenLossBiomassKg,
            'recommended_feed_g' => $recommendedFeedG,
            'actual_feed_g' => $actualFeedG,
            'feed_variance_g' => $feedVarianceG,
            'cumulative_feed_g' => $cumulativeFeedG,
            'cumulative_feed_kg' => $cumulativeFeedKg,
            'feed_conversion_ratio_real' => $fcrReal,
            'density_fish_m3' => $densityFishM3,
            'density_biomass_kg_m3' => $densityBiomassKgM3,
        ];
    }

    private static function costTotals(int $userId, int $batchId, int $tankId, string $snapshotDate): float
    {
        $entries = FinanceCostEntry::query()
            ->where('user_id', $userId)
            ->whereDate('date', '<=', $snapshotDate)
            ->get();

        $direct = $entries->reduce(function (float $acc, FinanceCostEntry $entry) use ($batchId) {
            $scope = $entry->scope;
            if ((int) ($scope['batch_id'] ?? 0) === $batchId) {
                return $acc + (float) ($entry->amount_crc ?? 0);
            }

            return $acc;
        }, 0.0);

        $tankOnly = $entries->reduce(function (float $acc, FinanceCostEntry $entry) use ($tankId) {
            $scope = $entry->scope;
            if ((int) ($scope['batch_id'] ?? 0) !== 0) {
                return $acc;
            }
            if ((int) ($scope['tank_id'] ?? 0) === $tankId) {
                return $acc + (float) ($entry->amount_crc ?? 0);
            }

            return $acc;
        }, 0.0);

        return self::round2(max(0.0, $direct + $tankOnly));
    }

    private static function effectiveSalePrice(int $userId, int $batchId, int $tankId, string $snapshotDate): float
    {
        $entries = FinanceSalesEntry::query()
            ->where('user_id', $userId)
            ->whereDate('date', '<=', $snapshotDate)
            ->get();

        $direct = $entries->reduce(function (array $acc, FinanceSalesEntry $entry) use ($batchId) {
            $scope = $entry->scope;
            if ((int) ($scope['batch_id'] ?? 0) === $batchId) {
                $acc['kg'] += max(0.0, (float) ($entry->kg_sold ?? 0));
                $acc['crc'] += max(0.0, (float) ($entry->total_crc ?? 0));
            }

            return $acc;
        }, ['kg' => 0.0, 'crc' => 0.0]);

        $tankOnly = $entries->reduce(function (array $acc, FinanceSalesEntry $entry) use ($tankId) {
            $scope = $entry->scope;
            if ((int) ($scope['batch_id'] ?? 0) !== 0) {
                return $acc;
            }
            if ((int) ($scope['tank_id'] ?? 0) === $tankId) {
                $acc['kg'] += max(0.0, (float) ($entry->kg_sold ?? 0));
                $acc['crc'] += max(0.0, (float) ($entry->total_crc ?? 0));
            }

            return $acc;
        }, ['kg' => 0.0, 'crc' => 0.0]);

        $soldKg = (float) $direct['kg'] + (float) $tankOnly['kg'];
        $soldCrc = (float) $direct['crc'] + (float) $tankOnly['crc'];
        $realPrice = $soldKg > 0 ? $soldCrc / $soldKg : 0.0;

        return $realPrice > 0 ? self::round2($realPrice) : 3500.0;
    }

    private static function realizedRevenue(int $userId, int $batchId, int $tankId, string $snapshotDate): float
    {
        $entries = FinanceSalesEntry::query()
            ->where('user_id', $userId)
            ->whereDate('date', '<=', $snapshotDate)
            ->get();

        $direct = $entries->reduce(function (float $acc, FinanceSalesEntry $entry) use ($batchId) {
            $scope = $entry->scope;
            if ((int) ($scope['batch_id'] ?? 0) === $batchId) {
                return $acc + max(0.0, (float) ($entry->total_crc ?? 0));
            }

            return $acc;
        }, 0.0);

        $tankOnly = $entries->reduce(function (float $acc, FinanceSalesEntry $entry) use ($tankId) {
            $scope = $entry->scope;
            if ((int) ($scope['batch_id'] ?? 0) !== 0) {
                return $acc;
            }
            if ((int) ($scope['tank_id'] ?? 0) === $tankId) {
                return $acc + max(0.0, (float) ($entry->total_crc ?? 0));
            }

            return $acc;
        }, 0.0);

        return self::round2(max(0.0, $direct + $tankOnly));
    }

    private static function harvestedKg(int $userId, int $batchId, string $snapshotDate): float
    {
        return self::round2((float) (FinanceHarvestEntry::query()
            ->where('user_id', $userId)
            ->where('batch_id', $batchId)
            ->whereDate('date', '<=', $snapshotDate)
            ->sum('kg_harvested') ?: 0.0));
    }

    private static function soldKg(int $userId, int $batchId, int $tankId, string $snapshotDate): float
    {
        $entries = FinanceSalesEntry::query()
            ->where('user_id', $userId)
            ->whereDate('date', '<=', $snapshotDate)
            ->get();

        $direct = $entries->reduce(function (float $acc, FinanceSalesEntry $entry) use ($batchId) {
            $scope = $entry->scope;
            if ((int) ($scope['batch_id'] ?? 0) === $batchId) {
                return $acc + max(0.0, (float) ($entry->kg_sold ?? 0));
            }

            return $acc;
        }, 0.0);

        $tankOnly = $entries->reduce(function (float $acc, FinanceSalesEntry $entry) use ($tankId) {
            $scope = $entry->scope;
            if ((int) ($scope['batch_id'] ?? 0) !== 0) {
                return $acc;
            }
            if ((int) ($scope['tank_id'] ?? 0) === $tankId) {
                return $acc + max(0.0, (float) ($entry->kg_sold ?? 0));
            }

            return $acc;
        }, 0.0);

        return self::round2(max(0.0, $direct + $tankOnly));
    }

    private static function hiddenLossFish(int $userId, int $batchId, string $snapshotDate): int
    {
        return (int) BatchSplitEvent::query()
            ->where('user_id', $userId)
            ->where('source_batch_id', $batchId)
            ->whereDate('date', '<=', $snapshotDate)
            ->sum('fish_missing');
    }

    private static function mortalityRate(Batch $batch, int $fishAlive): float
    {
        $fishInitial = max(0, (int) ($batch->fish_count_initial ?? 0));
        if ($fishInitial <= 0) {
            return 0.0;
        }

        return self::round2((($fishInitial - $fishAlive) / $fishInitial) * 100);
    }

    private static function latestUniformityMetrics(int $userId, int $batchId, string $snapshotDate): array
    {
        $session = BatchSplitSession::query()
            ->with('events')
            ->where('user_id', $userId)
            ->where('source_batch_id', $batchId)
            ->whereDate('date', '<=', $snapshotDate)
            ->orderByDesc('date')
            ->orderByDesc('id')
            ->first();

        $eventCount = 0;
        if ($session && $session->events) {
            $eventCount = $session->events->count();
        }

        if (! $session || $eventCount <= 0) {
            return [
                'last_split_date' => null,
                'uniformity_cv_pct' => null,
                'weight_spread_g' => null,
            ];
        }

        $weights = [];
        foreach ($session->events as $event) {
            $count = max(0, (int) ($event->fish_moved ?? 0));
            $weight = max(0.0, (float) ($event->avg_weight_moved_g ?? 0));
            if ($count <= 0 || $weight <= 0) {
                continue;
            }
            $weights[] = ['count' => $count, 'weight' => $weight];
        }

        if (! count($weights)) {
            return [
                'last_split_date' => $session->date ? $session->date->toDateString() : null,
                'uniformity_cv_pct' => null,
                'weight_spread_g' => null,
            ];
        }

        $totalCount = array_reduce($weights, fn ($acc, $row) => $acc + $row['count'], 0);
        if ($totalCount <= 0) {
            return [
                'last_split_date' => $session->date ? $session->date->toDateString() : null,
                'uniformity_cv_pct' => null,
                'weight_spread_g' => null,
            ];
        }

        $weightedMean = array_reduce($weights, fn ($acc, $row) => $acc + ($row['count'] * $row['weight']), 0.0) / $totalCount;
        $variance = array_reduce($weights, fn ($acc, $row) => $acc + ($row['count'] * (($row['weight'] - $weightedMean) ** 2)), 0.0) / $totalCount;
        $stdDev = sqrt(max(0.0, $variance));
        $cvPct = $weightedMean > 0 ? self::round2(($stdDev / $weightedMean) * 100) : null;
        $spread = self::round2(max(array_column($weights, 'weight')) - min(array_column($weights, 'weight')));

        return [
            'last_split_date' => $session->date ? $session->date->toDateString() : null,
            'uniformity_cv_pct' => $cvPct,
            'weight_spread_g' => $spread,
        ];
    }

    private static function estimateGrowthRate(Batch $batch, string $snapshotDate): float
    {
        $initial = (float) ($batch->avg_weight_initial_g ?? 0);
        $current = (float) ($batch->avg_weight_current_g ?? 0);
        if ($current <= $initial) {
            return 0.0;
        }

        $startedAt = null;
        if ($batch->started_at) {
            $startedAt = is_object($batch->started_at) && method_exists($batch->started_at, 'toDateString')
                ? $batch->started_at->toDateString()
                : (string) $batch->started_at;
        }
        if (! $startedAt) {
            return 0.0;
        }

        $days = max(1, Carbon::parse($snapshotDate)->diffInDays($startedAt));
        return self::round2(($current - $initial) / $days);
    }

    private static function sumFeedKg(int $userId, int $batchId, string $fromDate, string $toDate): float
    {
        $feedG = (float) (DailyLog::query()
            ->where('user_id', $userId)
            ->where('batch_id', $batchId)
            ->whereBetween('date', [$fromDate, $toDate])
            ->sum('feed_given_g') ?: 0.0);

        return self::round2($feedG / 1000);
    }

    private static function lastSplitPerformance(int $userId, int $batchId, string $snapshotDate, int $fishAlive, float $avgWeightG): array
    {
        $latestOutgoing = BatchSplitSession::query()
            ->where('user_id', $userId)
            ->where('source_batch_id', $batchId)
            ->whereDate('date', '<=', $snapshotDate)
            ->orderByDesc('date')
            ->orderByDesc('id')
            ->first();

        $latestIncoming = BatchSplitEvent::query()
            ->where('user_id', $userId)
            ->where('destination_batch_id', $batchId)
            ->whereDate('date', '<=', $snapshotDate)
            ->orderByDesc('date')
            ->orderByDesc('id')
            ->first();

        $lastSplitDate = null;
        $referenceWeightG = null;

        if ($latestOutgoing) {
            $lastSplitDate = $latestOutgoing->date ? $latestOutgoing->date->toDateString() : null;
            $referenceWeightG = max(0.0, (float) ($latestOutgoing->avg_weight_reference_g ?? 0));
        }

        if ($latestIncoming) {
            $incomingDate = $latestIncoming->date ? $latestIncoming->date->toDateString() : null;
            if ($incomingDate && (! $lastSplitDate || $incomingDate > $lastSplitDate)) {
                $lastSplitDate = $incomingDate;
                $referenceWeightG = max(0.0, (float) ($latestIncoming->avg_weight_moved_g ?? 0));
            }
        }

        if (! $lastSplitDate || ! $referenceWeightG || $referenceWeightG <= 0) {
            return [
                'days_since_last_split' => null,
                'post_split_biomass_gain_kg' => null,
                'post_split_feed_kg' => null,
                'post_split_fcr_real' => null,
            ];
        }

        $daysSinceSplit = max(0, Carbon::parse($snapshotDate)->diffInDays($lastSplitDate));
        $biomassGainKg = $avgWeightG > $referenceWeightG
            ? self::round2(($fishAlive * ($avgWeightG - $referenceWeightG)) / 1000)
            : null;
        $feedKg = self::sumFeedKg($userId, $batchId, $lastSplitDate, $snapshotDate);
        $fcrReal = ($biomassGainKg && $biomassGainKg > 0) ? self::round2($feedKg / $biomassGainKg) : null;

        return [
            'days_since_last_split' => $daysSinceSplit,
            'post_split_biomass_gain_kg' => $biomassGainKg,
            'post_split_feed_kg' => $feedKg,
            'post_split_fcr_real' => $fcrReal,
        ];
    }

    private static function round2(float $value): float
    {
        return round($value, 2);
    }

    private static function stageFcrTarget(?string $stageCode): ?float
    {
        if (! $stageCode) {
            return null;
        }

        return array_key_exists($stageCode, self::STAGE_FCR_TARGETS)
            ? self::round2((float) self::STAGE_FCR_TARGETS[$stageCode])
            : null;
    }
}
