<?php

namespace App\Services;

use App\Models\Cattle;
use App\Models\CattleHealthEvent;
use App\Models\FatteningLot;
use App\Models\FatteningLotAnimal;
use App\Models\MaterialExpense;
use App\Models\WeightRecord;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use InvalidArgumentException;

class FatteningLotService
{
    public function __construct(
        private readonly RanchAnimalProfitabilityService $profitabilityService,
        private readonly CattleLocationMovementService $locationMovementService,
        private readonly FatteningLotPastureCostService $pastureCostService,
        private readonly CattleDeactivationService $deactivationService,
        private readonly FatteningLotFeedIntakeService $feedIntakeService,
    ) {}

    public function addAnimal(FatteningLot $lot, Cattle $cattle, float $entryWeightKg, ?Carbon $entryDate = null): FatteningLotAnimal
    {
        if ($lot->status !== FatteningLot::STATUS_OPEN) {
            throw new InvalidArgumentException('El lote de engorda está cerrado.');
        }

        if (! $cattle->isActive()) {
            throw new InvalidArgumentException('Solo se pueden agregar animales activos.');
        }

        if ($cattle->activeFatteningAssignment()) {
            throw new InvalidArgumentException('El animal ya está en un lote de engorda activo.');
        }

        $entryDate ??= Carbon::today();

        $assignment = FatteningLotAnimal::create([
            'fattening_lot_id' => $lot->id,
            'cattle_id' => $cattle->id,
            'entry_weight_kg' => $entryWeightKg,
            'entry_date' => $entryDate,
        ]);

        $this->locationMovementService->recordFatteningEntry($cattle, $lot, $entryDate);

        return $assignment;
    }

    public function closeLot(FatteningLot $lot, array $exitWeightsByCattleId): FatteningLot
    {
        return DB::transaction(function () use ($lot, $exitWeightsByCattleId) {
            $closedAt = Carbon::today();

            foreach ($lot->activeAnimals()->with('cattle')->get() as $assignment) {
                $exitWeight = $exitWeightsByCattleId[$assignment->cattle_id] ?? null;
                if ($exitWeight === null) {
                    throw new InvalidArgumentException('Falta peso de salida para arete '.$assignment->cattle->ear_tag);
                }

                $assignment->update([
                    'exit_weight_kg' => $exitWeight,
                    'exit_date' => $closedAt,
                ]);

                $this->locationMovementService->recordFatteningExit(
                    $assignment->cattle,
                    $lot,
                    $closedAt,
                    null,
                    'Cierre de lote de engorda',
                );
            }

            $lot->update([
                'status' => FatteningLot::STATUS_CLOSED,
                'closed_at' => $closedAt,
            ]);

            return $lot->fresh(['activeAnimals.cattle', 'expenses.materialType']);
        });
    }

    /**
     * @return array{total_feed_cost: float, total_weight_gain_kg: float, feed_conversion_ratio: float|null, days_in_confinement: int}
     */
    public function evaluateLot(FatteningLot $lot): array
    {
        $feedCost = (float) $this->feedIntakeService->investmentSummary($lot)['total_cost'];
        $healthBreakdown = $this->healthCostBreakdownForLot($lot);
        $healthCost = $healthBreakdown['actual_total'];
        $pastureCost = $this->pastureCostService->accruedCost($lot);

        $assignments = $lot->animals()->with('cattle')->get();
        $totalGain = 0.0;

        foreach ($assignments as $assignment) {
            if ($assignment->exit_weight_kg !== null) {
                $totalGain += max(0, (float) $assignment->exit_weight_kg - (float) $assignment->entry_weight_kg);

                continue;
            }

            $latestWeight = WeightRecord::query()
                ->where('cattle_id', $assignment->cattle_id)
                ->whereDate('weighed_at', '>=', $assignment->entry_date)
                ->orderByDesc('weighed_at')
                ->value('weight_kg');

            if ($latestWeight !== null) {
                $totalGain += max(0, (float) $latestWeight - (float) $assignment->entry_weight_kg);
            }
        }

        $conversion = $totalGain > 0 ? round($feedCost / $totalGain, 4) : null;
        $totalInvestment = round($feedCost + $healthCost + $pastureCost, 2);
        $totalGainRounded = round($totalGain, 2);
        $pricePerKg = (float) ($lot->target_price_per_kg
            ?? config('ranch.fattening.default_price_per_kg_projection', 45));
        $gainValue = round($totalGainRounded * $pricePerKg, 2);

        return array_merge([
            'total_feed_cost' => round($feedCost, 2),
            'health_cost' => round($healthCost, 2),
            'pasture_cost' => $pastureCost,
            'total_investment' => $totalInvestment,
            'total_weight_gain_kg' => $totalGainRounded,
            'weight_gain_summary' => [
                'total_kg' => $totalGainRounded,
                'price_per_kg' => $pricePerKg,
                'value_at_price' => $gainValue,
                'investment' => $totalInvestment,
                'net_profit' => round($gainValue - $totalInvestment, 2),
            ],
            'feed_conversion_ratio' => $conversion,
            'days_in_confinement' => $lot->daysInConfinement(),
            'planned_duration_days' => $lot->plannedDurationDays(),
            'planned_exit_date' => $lot->plannedExitDate()->toDateString(),
            'days_remaining' => $lot->daysRemaining(),
            'active_animals_count' => $lot->activeAnimals()->count(),
            'pasture_accrual_start_date' => $lot->pastureAccrualStartDate()->toDateString(),
            'estimated_feed_cost_per_head' => $lot->activeAnimals()->count() > 0
                ? round($feedCost / $lot->activeAnimals()->count(), 2)
                : null,
            'estimated_health_cost_per_head' => $lot->activeAnimals()->count() > 0
                ? round($healthCost / $lot->activeAnimals()->count(), 2)
                : null,
            'projected_health_cost' => round($healthBreakdown['projected_total'], 2),
            'projected_health_cost_per_head' => $lot->activeAnimals()->count() > 0
                ? round($healthBreakdown['projected_total'] / $lot->activeAnimals()->count(), 2)
                : null,
        ], $this->profitEvaluation($lot));
    }

    public function healthCostForLot(FatteningLot $lot): float
    {
        return $this->healthCostBreakdownForLot($lot)['actual_total'];
    }

    /**
     * Gasto veterinario/salud por animal en el lote (real y proyectado al cierre).
     *
     * @return array{
     *   by_cattle: array<int, array{actual: float, projected: float}>,
     *   actual_total: float,
     *   projected_total: float
     * }
     */
    public function healthCostBreakdownForLot(FatteningLot $lot): array
    {
        $lot->loadMissing(['animals.cattle', 'activeAnimals.cattle']);

        $assignments = $lot->status === FatteningLot::STATUS_OPEN
            ? $lot->activeAnimals
            : $lot->animals;

        $byCattle = [];
        foreach ($assignments as $assignment) {
            $actual = $this->healthCostForAssignment($assignment);
            $byCattle[$assignment->cattle_id] = [
                'actual' => round($actual, 2),
                'projected' => $this->projectedHealthCostForAssignment($assignment, $lot, $actual),
            ];
        }

        $attributedActual = array_sum(array_column($byCattle, 'actual'));
        $lotTotalActual = (float) MaterialExpense::query()
            ->where('fattening_lot_id', $lot->id)
            ->whereHas('materialType', fn ($q) => $q->where('category', 'animal_health'))
            ->sum('total_cost');

        $unallocated = max(0, round($lotTotalActual - $attributedActual, 2));
        if ($unallocated > 0 && count($byCattle) > 0) {
            $share = round($unallocated / count($byCattle), 2);
            foreach ($byCattle as $cattleId => $row) {
                $byCattle[$cattleId]['actual'] = round($row['actual'] + $share, 2);
                $byCattle[$cattleId]['projected'] = max(
                    $byCattle[$cattleId]['projected'],
                    $byCattle[$cattleId]['actual'],
                );
            }
        }

        $actualTotal = round(array_sum(array_column($byCattle, 'actual')), 2);
        $projectedTotal = round(array_sum(array_column($byCattle, 'projected')), 2);

        return [
            'by_cattle' => $byCattle,
            'actual_total' => $actualTotal,
            'projected_total' => max($projectedTotal, $actualTotal),
        ];
    }

    private function healthCostForAssignment(FatteningLotAnimal $assignment): float
    {
        $end = $assignment->exit_date ?? Carbon::today();

        return (float) CattleHealthEvent::query()
            ->where('cattle_id', $assignment->cattle_id)
            ->whereDate('event_date', '>=', $assignment->entry_date)
            ->whereDate('event_date', '<=', $end)
            ->with('materialExpense:id,total_cost')
            ->get()
            ->sum(fn (CattleHealthEvent $event) => (float) (
                $event->materialExpense?->total_cost ?? $event->cost ?? 0
            ));
    }

    private function projectedHealthCostForAssignment(
        FatteningLotAnimal $assignment,
        FatteningLot $lot,
        float $actualCost,
    ): float {
        if ($lot->status !== FatteningLot::STATUS_OPEN) {
            return round($actualCost, 2);
        }

        $daysIn = max(1, $assignment->daysInLot());
        $daysRemaining = max(0, $lot->daysRemaining());

        if ($daysRemaining === 0) {
            return round($actualCost, 2);
        }

        if ($actualCost > 0) {
            $dailyRate = $actualCost / $daysIn;

            return round($actualCost + ($dailyRate * $daysRemaining), 2);
        }

        $monthlyDefault = (float) config('ranch.fattening.default_health_cost_per_animal_month', 0);
        if ($monthlyDefault > 0) {
            return round($monthlyDefault * ($daysRemaining / 30), 2);
        }

        $heads = max(1, $lot->activeAnimals()->count());
        $lotHealth = (float) MaterialExpense::query()
            ->where('fattening_lot_id', $lot->id)
            ->whereHas('materialType', fn ($q) => $q->where('category', 'animal_health'))
            ->sum('total_cost');

        if ($lotHealth > 0) {
            $lotDays = max(1, $lot->daysInConfinement());

            return round(($lotHealth / $heads / $lotDays) * ($daysIn + $daysRemaining), 2);
        }

        return round($actualCost, 2);
    }

    /**
     * Proyección al cerrar con pesos de salida propuestos (antes de persistir).
     *
     * @param  array<int, float>  $exitWeightsByCattleId
     * @return array<string, mixed>
     */
    public function previewClose(FatteningLot $lot, array $exitWeightsByCattleId): array
    {
        $lot->load(['activeAnimals.cattle', 'expenses']);

        $projectedIncome = 0.0;
        $totalGain = 0.0;
        $pricePerKg = $lot->target_price_per_kg
            ?? config('ranch.fattening.default_price_per_kg_projection', 45);

        foreach ($lot->activeAnimals as $assignment) {
            $exit = $exitWeightsByCattleId[$assignment->cattle_id] ?? null;
            if ($exit === null) {
                continue;
            }
            $projectedIncome += (float) $exit * (float) $pricePerKg;
            $totalGain += max(0, (float) $exit - (float) $assignment->entry_weight_kg);
        }

        $feedCost = (float) $this->feedIntakeService->investmentSummary($lot)['total_cost'];
        $healthBreakdown = $this->healthCostBreakdownForLot($lot);
        $healthCostActual = $healthBreakdown['actual_total'];
        $healthCostProjected = $healthBreakdown['projected_total'];
        $healthCost = $lot->status === FatteningLot::STATUS_OPEN
            ? $healthCostProjected
            : $healthCostActual;
        $pastureCost = $this->pastureCostService->accruedCost($lot);
        $totalCost = round($feedCost + $healthCost + $pastureCost, 2);
        $netProfit = round($projectedIncome - $totalCost, 2);
        $target = $lot->target_net_profit !== null ? (float) $lot->target_net_profit : null;

        return [
            'projected_income' => round($projectedIncome, 2),
            'projected_feed_cost' => round($feedCost, 2),
            'projected_pasture_cost' => $pastureCost,
            'projected_health_cost' => round($healthCost, 2),
            'current_health_cost' => round($healthCostActual, 2),
            'projected_total_cost' => $totalCost,
            'projected_net_profit' => $netProfit,
            'projected_weight_gain_kg' => round($totalGain, 2),
            'price_per_kg_used' => (float) $pricePerKg,
            'target_net_profit' => $target,
            'meets_target' => $target === null ? null : $netProfit >= $target,
            'profit_per_kg_gained' => $totalGain > 0 ? round($netProfit / $totalGain, 2) : null,
            'planned_exit_date' => $lot->plannedExitDate()->toDateString(),
        ];
    }

    /**
     * Cierra el lote vendiendo todos los animales activos al precio/kg indicado.
     *
     * @param  array<int, float>  $exitWeightsByCattleId
     */
    public function closeAndSellLot(
        FatteningLot $lot,
        array $exitWeightsByCattleId,
        float $pricePerKg,
        ?Carbon $saleDate = null,
        ?int $userId = null
    ): FatteningLot {
        if ($lot->status !== FatteningLot::STATUS_OPEN) {
            throw new InvalidArgumentException('El lote de engorda ya está cerrado.');
        }

        if ($pricePerKg <= 0) {
            throw new InvalidArgumentException('El precio por kg debe ser mayor a cero.');
        }

        return DB::transaction(function () use ($lot, $exitWeightsByCattleId, $pricePerKg, $saleDate, $userId) {
            $saleDate ??= Carbon::today();
            $userId ??= 1;

            foreach ($lot->activeAnimals()->with('cattle')->get() as $assignment) {
                $exitWeight = $exitWeightsByCattleId[$assignment->cattle_id] ?? null;
                if ($exitWeight === null) {
                    throw new InvalidArgumentException('Falta peso de salida para arete '.$assignment->cattle->ear_tag);
                }

                $this->deactivationService->deactivate($assignment->cattle, [
                    'reason' => Cattle::STATUS_SOLD,
                    'exit_weight_kg' => $exitWeight,
                    'price_per_kg' => $pricePerKg,
                    'sale_date' => $saleDate->toDateString(),
                    'notes' => 'Venta — cierre lote: '.$lot->name,
                ], $userId);
            }

            $lot->update([
                'status' => FatteningLot::STATUS_CLOSED,
                'closed_at' => $saleDate,
                'target_price_per_kg' => $pricePerKg,
            ]);

            return $lot->fresh(['animals.cattle', 'expenses.materialType']);
        });
    }

    /**
     * @return array<string, mixed>
     */
    private function profitEvaluation(FatteningLot $lot): array
    {
        if ($lot->target_net_profit === null) {
            return [
                'target_net_profit' => null,
                'actual_net_profit' => null,
                'meets_target' => null,
            ];
        }

        $profitRows = collect($this->profitabilityService->generate(null, null, 'all')['fattening_lots'])
            ->firstWhere('lot_id', $lot->id);

        $actualNet = $profitRows['net_profit'] ?? null;

        return [
            'target_net_profit' => (float) $lot->target_net_profit,
            'actual_net_profit' => $actualNet,
            'meets_target' => $actualNet !== null ? $actualNet >= (float) $lot->target_net_profit : null,
        ];
    }
}
