<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\BatchSplitSessions\StoreBatchSplitSessionRequest;
use App\Http\Requests\BatchSplitSessions\UpdateBatchSplitSessionRequest;
use App\Models\Batch;
use App\Models\BatchSplitEvent;
use App\Models\BatchSplitPlan;
use App\Models\BatchSplitSession;
use App\Models\TaskProject;
use App\Support\BatchKpiSnapshotService;
use App\Support\BatchMetrics;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;

class BatchSplitSessionController extends Controller
{
    public function index(): JsonResponse
    {
        $userId = (int) auth('api')->id();
        $sessions = BatchSplitSession::query()
            ->with(['sourceBatch', 'events.destinationBatch', 'lastAdjustedBy'])
            ->where('user_id', $userId)
            ->orderByDesc('date')
            ->latest()
            ->get();

        return $this->success($sessions, 'Batch split sessions list');
    }

    public function store(StoreBatchSplitSessionRequest $request): JsonResponse
    {
        $session = $this->upsertSession(
            (int) auth('api')->id(),
            $request->validated()
        );

        if ($session instanceof JsonResponse) {
            return $session;
        }

        return $this->success($session, 'Batch split session created', 201);
    }

    public function update(UpdateBatchSplitSessionRequest $request, int $id): JsonResponse
    {
        $userId = (int) auth('api')->id();
        $existing = BatchSplitSession::query()
            ->with('events')
            ->where('user_id', $userId)
            ->find($id);

        if (! $existing) {
            return $this->error('Batch split session not found', null, 404);
        }

        $session = $this->upsertSession($userId, $request->validated(), $existing);

        if ($session instanceof JsonResponse) {
            return $session;
        }

        return $this->success($session, 'Batch split session updated');
    }

    public function destroy(int $id): JsonResponse
    {
        $userId = (int) auth('api')->id();
        $session = BatchSplitSession::query()
            ->with('events')
            ->where('user_id', $userId)
            ->find($id);

        if (! $session) {
            return $this->error('Batch split session not found', null, 404);
        }

        $sourceBatch = Batch::query()->where('user_id', $userId)->find((int) $session->source_batch_id);
        $destinationBatchIds = $session->events
            ->pluck('destination_batch_id')
            ->map(fn ($batchId) => (int) $batchId)
            ->unique()
            ->values()
            ->all();

        DB::transaction(function () use ($destinationBatchIds, $session, $sourceBatch, $userId) {
            $this->detachExecutedSplitPlansForSession($userId, (int) $session->id);

            BatchSplitEvent::query()
                ->where('user_id', $userId)
                ->where('batch_split_session_id', (int) $session->id)
                ->delete();

            $session->delete();

            if ($sourceBatch) {
                BatchMetrics::syncBatch($sourceBatch);
                BatchKpiSnapshotService::syncBatch($sourceBatch);
            }

            foreach ($destinationBatchIds as $destinationBatchId) {
                $destinationBatch = Batch::query()->where('user_id', $userId)->find((int) $destinationBatchId);
                if ($destinationBatch) {
                    BatchMetrics::syncBatch($destinationBatch);
                    BatchKpiSnapshotService::syncBatch($destinationBatch);
                }
            }
        });

        return $this->success(null, 'Batch split session deleted');
    }

    private function upsertSession(int $userId, array $payload, ?BatchSplitSession $existing = null)
    {
        $sourceBatchId = $existing ? (int) $existing->source_batch_id : (int) $payload['source_batch_id'];
        $sourceBatch = Batch::query()->where('user_id', $userId)->find($sourceBatchId);
        if (! $sourceBatch) {
            return $this->error('Source batch not found', null, 404);
        }

        if ((string) $sourceBatch->status !== 'active') {
            return $this->error('Only active source batches can be used in desdobles', null, 422);
        }

        $date = (string) $payload['date'];
        $baselineEvents = BatchSplitEvent::query()
            ->where('user_id', $userId)
            ->when($existing, fn ($query) => $query->where('batch_split_session_id', '!=', (int) $existing->id))
            ->get();
        $theoretical = BatchMetrics::liveFishCountAtDate($sourceBatch, $date, null, $baselineEvents);
        $fishMissing = max(0, (int) ($payload['fish_missing'] ?? 0));
        $fishCounted = array_key_exists('fish_count_counted', $payload)
            ? max(0, (int) $payload['fish_count_counted'])
            : max(0, $theoretical - $fishMissing);

        if ($fishCounted > $theoretical) {
            return $this->error('Counted fish cannot exceed theoretical live fish', [
                'fish_count_theoretical' => $theoretical,
                'fish_count_counted' => $fishCounted,
            ], 422);
        }

        if (! array_key_exists('fish_missing', $payload)) {
            $fishMissing = max(0, $theoretical - $fishCounted);
        }

        if (($fishCounted + $fishMissing) > $theoretical) {
            return $this->error('Counted plus missing fish cannot exceed theoretical live fish', [
                'fish_count_theoretical' => $theoretical,
                'fish_count_counted' => $fishCounted,
                'fish_missing' => $fishMissing,
            ], 422);
        }

        $lines = collect($payload['lines'] ?? [])->values();
        $destinations = Batch::query()
            ->where('user_id', $userId)
            ->whereIn('id', $lines->pluck('destination_batch_id')->map(fn ($id) => (int) $id)->all())
            ->get()
            ->keyBy('id');

        $fishMovedTotal = 0;
        $biomassMovedTotalKg = 0;

        foreach ($lines as $line) {
            $destinationBatchId = (int) $line['destination_batch_id'];
            $destinationBatch = $destinations->get($destinationBatchId);
            if (! $destinationBatch) {
                return $this->error('Destination batch not found', ['destination_batch_id' => $destinationBatchId], 404);
            }

            if ((int) $destinationBatch->id === (int) $sourceBatch->id) {
                return $this->error('Source and destination batch must be different', ['destination_batch_id' => $destinationBatchId], 422);
            }

            if ((string) $destinationBatch->status !== 'active') {
                return $this->error('Only active destination batches can be used in desdobles', ['destination_batch_id' => $destinationBatchId], 422);
            }

            $fishMoved = max(0, (int) ($line['fish_moved'] ?? 0));
            $avgWeightMovedG = max(0.0, (float) ($line['avg_weight_moved_g'] ?? 0));

            $fishMovedTotal += $fishMoved;
            $biomassMovedTotalKg += BatchMetrics::biomassKg($fishMoved, $avgWeightMovedG);
        }

        if ($fishMovedTotal > $fishCounted) {
            return $this->error('Moved fish cannot exceed counted fish in the desdoble session', [
                'fish_count_counted' => $fishCounted,
                'fish_moved_total' => $fishMovedTotal,
            ], 422);
        }

        $fishRemainingSource = max(0, $fishCounted - $fishMovedTotal);
        $avgWeightReferenceG = max(0.0, (float) ($payload['avg_weight_reference_g'] ?? $sourceBatch->avg_weight_current_g ?? 0));
        $destinationBatchIds = $lines->pluck('destination_batch_id')->map(fn ($id) => (int) $id)->unique()->values()->all();
        $affectedDestinationBatchIds = $existing
            ? collect($existing->events ?? [])->pluck('destination_batch_id')->map(fn ($id) => (int) $id)->merge($destinationBatchIds)->unique()->values()->all()
            : $destinationBatchIds;

        return DB::transaction(function () use (
            $avgWeightReferenceG,
            $affectedDestinationBatchIds,
            $biomassMovedTotalKg,
            $date,
            $destinations,
            $existing,
            $fishCounted,
            $fishMissing,
            $fishMovedTotal,
            $fishRemainingSource,
            $lines,
            $payload,
            $sourceBatch,
            $theoretical,
            $userId
        ) {
            $session = $existing ?? new BatchSplitSession();
            $session->fill([
                'user_id' => $userId,
                'source_batch_id' => (int) $sourceBatch->id,
                'date' => $date,
                'fish_count_theoretical' => $theoretical,
                'fish_count_counted' => $fishCounted,
                'fish_missing' => $fishMissing,
                'fish_moved_total' => $fishMovedTotal,
                'fish_remaining_source' => $fishRemainingSource,
                'biomass_moved_total_kg' => $biomassMovedTotalKg,
                'avg_weight_reference_g' => $avgWeightReferenceG,
                'notes' => $payload['notes'] ?? null,
                'revision_count' => $existing ? ((int) ($existing->revision_count ?? 0) + 1) : 0,
                'adjustment_reason' => $existing ? ($payload['adjustment_reason'] ?? null) : null,
                'last_adjusted_at' => $existing ? now() : null,
                'last_adjusted_by' => $existing ? $userId : null,
            ]);
            $session->save();

            if ($existing) {
                BatchSplitEvent::query()
                    ->where('user_id', $userId)
                    ->where('batch_split_session_id', (int) $existing->id)
                    ->delete();
            }

            foreach ($lines as $index => $line) {
                $fishMoved = max(0, (int) ($line['fish_moved'] ?? 0));
                $avgWeightMovedG = max(0.0, (float) ($line['avg_weight_moved_g'] ?? 0));

                BatchSplitEvent::create([
                    'user_id' => $userId,
                    'batch_split_session_id' => (int) $session->id,
                    'source_batch_id' => (int) $sourceBatch->id,
                    'destination_batch_id' => (int) $line['destination_batch_id'],
                    'grading_label' => $line['grading_label'] ?? null,
                    'date' => $date,
                    'fish_count_theoretical' => $index === 0 ? $theoretical : 0,
                    'fish_count_counted' => $index === 0 ? $fishCounted : 0,
                    'fish_moved' => $fishMoved,
                    'fish_missing' => $index === 0 ? $fishMissing : 0,
                    'avg_weight_moved_g' => $avgWeightMovedG,
                    'biomass_moved_kg' => BatchMetrics::biomassKg($fishMoved, $avgWeightMovedG),
                    'notes' => $line['notes'] ?? null,
                ]);
            }

            BatchMetrics::syncBatch($sourceBatch);
            BatchKpiSnapshotService::syncBatch($sourceBatch, $date);
            foreach ($affectedDestinationBatchIds as $destinationBatchId) {
                $destinationBatch = $destinations->get($destinationBatchId)
                    ?? Batch::query()->where('user_id', $userId)->find((int) $destinationBatchId);
                if ($destinationBatch) {
                    BatchMetrics::syncBatch($destinationBatch);
                    BatchKpiSnapshotService::syncBatch($destinationBatch, $date);
                }
            }

            $loaded = $session->load(['sourceBatch', 'events.destinationBatch', 'lastAdjustedBy']);
            $this->syncMatchingSplitPlanExecution($userId, $session);

            return $loaded->fresh()->load(['sourceBatch', 'events.destinationBatch', 'lastAdjustedBy']);
        });
    }

    private function syncMatchingSplitPlanExecution(int $userId, BatchSplitSession $session): void
    {
        $sessionDate = Carbon::parse((string) $session->date);

        $alreadyLinked = BatchSplitPlan::query()
            ->where('user_id', $userId)
            ->where('executed_batch_split_session_id', (int) $session->id)
            ->first();

        $plan = $alreadyLinked ?: $this->resolveAutoMatchSplitPlan($userId, (int) $session->source_batch_id, $sessionDate);

        if (! $plan) {
            return;
        }

        $plan->update([
            'status' => 'completed',
            'executed_batch_split_session_id' => (int) $session->id,
            'executed_at' => $sessionDate,
        ]);

        if ($plan->task_project_id) {
            $project = TaskProject::query()->where('user_id', $userId)->find((int) $plan->task_project_id);
            if ($project) {
                $project->update([
                    'status' => 'completed',
                    'completed_at' => $sessionDate,
                ]);

                foreach ($project->tasks()->get() as $task) {
                    $task->update([
                        'status' => 'completed',
                        'completed_at' => $sessionDate,
                    ]);
                }
            }
        }
    }

    private function resolveAutoMatchSplitPlan(int $userId, int $batchId, Carbon $sessionDate): ?BatchSplitPlan
    {
        $candidates = BatchSplitPlan::query()
            ->where('user_id', $userId)
            ->where('batch_id', $batchId)
            ->where('status', 'active')
            ->whereNull('executed_batch_split_session_id')
            ->whereBetween('planned_date', [
                $sessionDate->copy()->subDays(7)->toDateString(),
                $sessionDate->copy()->addDays(3)->toDateString(),
            ])
            ->orderBy('planned_date')
            ->orderBy('id')
            ->get();

        if ($candidates->isEmpty()) {
            return null;
        }

        $exact = $candidates->filter(
            fn (BatchSplitPlan $plan) => $plan->planned_date && $plan->planned_date->toDateString() === $sessionDate->toDateString()
        )->values();

        if ($exact->count() === 1) {
            return $exact->first();
        }

        if ($exact->count() > 1) {
            return null;
        }

        $ranked = $candidates
            ->map(function (BatchSplitPlan $plan) use ($sessionDate) {
                $distance = $plan->planned_date ? abs($sessionDate->diffInDays($plan->planned_date, false)) : 9999;

                return [
                    'plan' => $plan,
                    'distance' => $distance,
                ];
            })
            ->sortBy(fn (array $row) => sprintf('%05d-%010d', (int) $row['distance'], (int) $row['plan']->id))
            ->values();

        $best = $ranked->first();
        $second = $ranked->get(1);

        if (! $best) {
            return null;
        }

        if ((int) $best['distance'] > 2) {
            return null;
        }

        if ($second && (int) $second['distance'] === (int) $best['distance']) {
            return null;
        }

        return $best['plan'];
    }

    private function detachExecutedSplitPlansForSession(int $userId, int $sessionId): void
    {
        $plans = BatchSplitPlan::query()
            ->where('user_id', $userId)
            ->where('executed_batch_split_session_id', $sessionId)
            ->get();

        foreach ($plans as $plan) {
            $plan->update([
                'status' => 'active',
                'executed_batch_split_session_id' => null,
                'executed_at' => null,
            ]);

            if ($plan->task_project_id) {
                $project = TaskProject::query()->where('user_id', $userId)->find((int) $plan->task_project_id);
                if ($project) {
                    $project->update([
                        'status' => 'active',
                        'completed_at' => null,
                    ]);

                    foreach ($project->tasks()->get() as $task) {
                        $task->update([
                            'status' => 'pending',
                            'completed_at' => null,
                        ]);
                    }
                }
            }
        }
    }
}
