<?php

namespace App\Services\Fees;

use App\Models\Payment;
use Illuminate\Support\Facades\Log;

class FeeRefundCalculatorService
{
    public function __construct(
        private FeeConfigService $config,
        private FeeStudentFeeService $studentFees
    ) {
    }

    public function calculateRefund(array $students, int $parentId): array
    {
        $schoolYear = $this->config->getSchoolYear();
        $refundDeadline = $this->config->getRefundDeadline();
        $weeksOfStudy = $this->config->getWeeksOfStudy();
        $schoolEndDate = $this->config->getSchoolEndDate();

        $totalPaid = Payment::getTotalPaidByParentId($parentId, $schoolYear);
        if ($totalPaid <= 0) {
            Log::info('Refund skipped: parent has no payments', [
                'parent_id' => $parentId,
                'school_year' => $schoolYear,
            ]);
            return $this->resultPayload(0.0, $totalPaid, $refundDeadline, $schoolEndDate, $weeksOfStudy, 0, []);
        }

        [$registered, $withdrawn] = $this->partitionStudents($students);

        if (empty($withdrawn)) {
            Log::info('Refund skipped: no withdrawn students found', [
                'parent_id' => $parentId,
                'registered_count' => count($registered),
            ]);
            return $this->resultPayload(0.0, $totalPaid, $refundDeadline, $schoolEndDate, $weeksOfStudy, 0, []);
        }

        // Per the school billing plan (section 8), each student must carry the
        // tuition fee that was assigned to them during fee calculation BEFORE
        // the refund loop runs. We mark withdrawn vs registered before merging
        // so the fee tier (first/second regular) is computed against the full
        // family and we can still iterate only the withdrawn list afterwards.
        foreach ($registered as &$student) {
            $student['_is_withdrawn'] = false;
        }
        unset($student);
        foreach ($withdrawn as &$student) {
            $student['_is_withdrawn'] = true;
        }
        unset($student);

        $allStudents = $this->studentFees->assignFees(array_merge($registered, $withdrawn));

        $withdrawnWithFees = array_values(array_filter(
            $allStudents,
            fn (array $student): bool => !empty($student['_is_withdrawn'])
        ));

        $refundAmount = 0.0;
        $withdrawCount = 0;
        $studentBreakdown = [];

        foreach ($withdrawnWithFees as $student) {
            $studentId = $student['student_id'] ?? null;
            $studentFee = (float) ($student['tuition_fee'] ?? 0);
            $withdrawalDate = $student['withdrawal_date'] ?? null;

            $entry = [
                'student_id' => $studentId,
                'grade' => $student['grade'] ?? null,
                'grade_level' => $student['grade_level'] ?? null,
                'fee_category' => $student['fee_category'] ?? null,
                'tuition_fee' => round($studentFee, 2),
                'withdrawal_date' => $withdrawalDate,
                'weeks_remaining' => 0,
                'refund_amount' => 0.0,
                'eligible' => false,
                'reason' => null,
            ];

            if (!$withdrawalDate) {
                $entry['reason'] = 'missing_withdrawal_date';
                Log::warning('Refund skipped for student: missing withdrawal date', [
                    'parent_id' => $parentId,
                    'student_id' => $studentId,
                ]);
                $studentBreakdown[] = $entry;
                continue;
            }

            if (!$refundDeadline || strtotime($withdrawalDate) > strtotime($refundDeadline)) {
                $entry['reason'] = 'after_refund_deadline';
                $studentBreakdown[] = $entry;
                continue;
            }

            if (!$schoolEndDate) {
                $entry['reason'] = 'missing_school_end_date';
                Log::warning('Refund skipped for student: missing school end date', [
                    'parent_id' => $parentId,
                    'student_id' => $studentId,
                ]);
                $studentBreakdown[] = $entry;
                continue;
            }

            if ($studentFee <= 0 || $weeksOfStudy <= 0) {
                $entry['reason'] = 'zero_fee_or_weeks';
                Log::warning('Refund skipped for student: zero fee or weeks_of_study', [
                    'parent_id' => $parentId,
                    'student_id' => $studentId,
                    'student_fee' => $studentFee,
                    'weeks_of_study' => $weeksOfStudy,
                ]);
                $studentBreakdown[] = $entry;
                continue;
            }

            $weeksRemaining = $this->weeksRemaining($withdrawalDate, $schoolEndDate, $weeksOfStudy);

            // Plan section 7: Refund Amount = Student Tuition Fee / Weeks of Study * Weeks Remaining
            $proportionalRefund = ($studentFee / $weeksOfStudy) * $weeksRemaining;
            $proportionalRefund = round(max(0.0, $proportionalRefund), 2);

            $entry['weeks_remaining'] = $weeksRemaining;
            $entry['refund_amount'] = $proportionalRefund;
            $entry['eligible'] = $proportionalRefund > 0;
            if ($entry['eligible']) {
                $withdrawCount++;
                $refundAmount += $proportionalRefund;
            } else {
                $entry['reason'] = 'no_weeks_remaining';
            }

            $studentBreakdown[] = $entry;
        }

        $uncappedRefund = $refundAmount;
        if ($refundAmount > $totalPaid) {
            Log::info('Refund capped at total paid', [
                'parent_id' => $parentId,
                'uncapped' => round($uncappedRefund, 2),
                'capped' => round($totalPaid, 2),
            ]);
            $refundAmount = $totalPaid;

            // Scale per-student refunds proportionally so the breakdown
            // adds up to the capped family total.
            if ($uncappedRefund > 0) {
                $ratio = $totalPaid / $uncappedRefund;
                foreach ($studentBreakdown as &$row) {
                    if (!empty($row['eligible'])) {
                        $row['refund_amount'] = round($row['refund_amount'] * $ratio, 2);
                    }
                }
                unset($row);
            }
        }

        Log::info('Refund calculation complete', [
            'parent_id' => $parentId,
            'school_year' => $schoolYear,
            'withdrawn_total' => count($withdrawnWithFees),
            'withdrawn_eligible' => $withdrawCount,
            'refund_amount' => round($refundAmount, 2),
            'total_paid' => round($totalPaid, 2),
        ]);

        return $this->resultPayload(
            $refundAmount,
            $totalPaid,
            $refundDeadline,
            $schoolEndDate,
            $weeksOfStudy,
            $withdrawCount,
            $studentBreakdown
        );
    }

    /**
     * @return array{0: array<int, array<string, mixed>>, 1: array<int, array<string, mixed>>}
     */
    private function partitionStudents(array $students): array
    {
        $registered = [];
        $withdrawn = [];

        foreach ($students as $student) {
            if ($this->studentFees->isWithdrawn($student)) {
                $withdrawn[] = $student;
                continue;
            }

            if ($this->studentFees->isBillable($student)) {
                $registered[] = $student;
            }
        }

        return [$registered, $withdrawn];
    }

    private function weeksRemaining(string $withdrawalDate, string $schoolEndDate, float $weeksOfStudy): int
    {
        $withdrawDateObj = new \DateTime(date('Y-m-d', strtotime($withdrawalDate)));
        $schoolEndDateObj = new \DateTime($schoolEndDate);

        if ($withdrawDateObj > $schoolEndDateObj) {
            return 0;
        }

        $daysRemaining = $withdrawDateObj->diff($schoolEndDateObj)->days;
        return (int) min($weeksOfStudy, max(0, (int) ceil($daysRemaining / 7)));
    }

    private function resultPayload(
        float $refund,
        float $totalPaid,
        ?string $refundDeadline,
        ?string $schoolEndDate,
        float $weeksOfStudy,
        int $withdrawCount,
        array $students
    ): array {
        return [
            'refund_amount' => round($refund, 2),
            'total_paid' => round($totalPaid, 2),
            'refund_deadline' => $refundDeadline,
            'school_end_date' => $schoolEndDate,
            'weeks_of_study' => $weeksOfStudy,
            'withdrawn_students' => $withdrawCount,
            'students' => $students,
        ];
    }
}
