<?php

namespace App\Libraries\Tuition;

use App\Libraries\FinancialStatus;
use App\Models\ClassSectionModel;
use App\Models\ConfigurationModel;
use App\Models\EnrollmentModel;
use App\Models\PaymentModel;
use App\Models\RefundModel;
use App\Models\StudentClassModel;
use App\Models\UserModel;

class TuitionForecastService
{
    protected ConfigurationModel $configurationModel;
    protected EnrollmentModel $enrollmentModel;
    protected StudentClassModel $studentClassModel;
    protected ClassSectionModel $classSectionModel;
    protected PaymentModel $paymentModel;
    protected RefundModel $refundModel;
    protected UserModel $userModel;
    protected OldTuitionCalculatorService $oldCalculator;
    protected NewTuitionCalculatorService $newCalculator;

    public function __construct()
    {
        $this->configurationModel = new ConfigurationModel();
        $this->enrollmentModel = new EnrollmentModel();
        $this->studentClassModel = new StudentClassModel();
        $this->classSectionModel = new ClassSectionModel();
        $this->paymentModel = new PaymentModel();
        $this->refundModel = new RefundModel();
        $this->userModel = new UserModel();
        $this->oldCalculator = new OldTuitionCalculatorService();
        $this->newCalculator = new NewTuitionCalculatorService();
    }

    public function calculate(string $schoolYear, string $semester, string $mode = 'compare', array $options = []): array
    {
        $schoolYear = trim($schoolYear) !== '' ? trim($schoolYear) : $this->getDefaultSchoolYear();
        $semester = trim($semester) !== '' ? trim($semester) : $this->getDefaultSemester();
        $mode = $this->normalizeMode($mode);
        $options = $this->normalizeOptions($options);
        $this->unitPriceOverride = $options['unit_price'];
        $this->youthUnitPriceOverride = $options['youth_unit_price'];
        $tuitionConfig = $this->getTuitionConfig();
        $familyRows = [];
        $summary = [
            'family_count' => 0,
            'student_count' => 0,
            'billable_student_count' => 0,
            'old_projected_tuition' => '0.00',
            'new_projected_tuition' => '0.00',
            'old_projected_income' => '0.00',
            'new_projected_income' => '0.00',
            'difference' => '0.00',
            'projected_tuition' => '0.00',
            'projected_income' => '0.00',
            'unit_price' => '0.00',
            'youth_unit_price' => '0.00',
        ];

        $oldProjectedCents = 0;
        $newProjectedCents = 0;
        $studentCount = 0;
        $billableStudentCount = 0;

        foreach ($this->loadFamilies($schoolYear, $semester) as $family) {
            $parentId = (int) ($family['parent_id'] ?? 0);
            if ($parentId <= 0) {
                continue;
            }

            $studentContext = $this->loadFamilyStudents($parentId, $schoolYear, $semester, $options);
            $students = $studentContext['students'];
            $allStudents = $studentContext['all_students'];
            $warnings = $studentContext['warnings'];

            if (empty($allStudents)) {
                continue;
            }

            $oldResult = $this->oldCalculator->calculateFamilyTuition($students, $tuitionConfig);
            $newResult = $this->newCalculator->calculateFamilyTuition($students, $tuitionConfig);

            $oldTotalCents = $this->toCents($oldResult['total'] ?? 0);
            $newTotalCents = $this->toCents($newResult['total'] ?? 0);

            $mergedDetails = $this->mergeStudentDetails($allStudents, $oldResult['details'] ?? [], $newResult['details'] ?? []);
            $studentCount += count($allStudents);
            $billableStudentCount += count($students);
            $oldProjectedCents += $oldTotalCents;
            $newProjectedCents += $newTotalCents;

            $familyRows[] = [
                'parent_id' => $parentId,
                'parent_name' => $family['parent_name'] ?? ('Parent #' . $parentId),
                'student_count' => count($allStudents),
                'billable_student_count' => count($students),
                'old_total' => $this->fromCents($oldTotalCents),
                'new_total' => $this->fromCents($newTotalCents),
                'difference' => $this->fromCents($newTotalCents - $oldTotalCents),
                'warnings' => array_values(array_unique($warnings)),
                'student_details' => $mergedDetails,
            ];
        }

        usort($familyRows, static fn (array $left, array $right): int => strcmp((string) ($left['parent_name'] ?? ''), (string) ($right['parent_name'] ?? '')));

        $summary['family_count'] = count($familyRows);
        $summary['student_count'] = $studentCount;
        $summary['billable_student_count'] = $billableStudentCount;
        $summary['old_projected_tuition'] = $this->fromCents($oldProjectedCents);
        $summary['new_projected_tuition'] = $this->fromCents($newProjectedCents);
        $summary['old_projected_income'] = $summary['old_projected_tuition'];
        $summary['new_projected_income'] = $summary['new_projected_tuition'];
        $summary['difference'] = $this->fromCents($newProjectedCents - $oldProjectedCents);
        $summary['projected_tuition'] = $mode === 'old' ? $summary['old_projected_tuition'] : $summary['new_projected_tuition'];
        $summary['projected_income'] = $mode === 'old' ? $summary['old_projected_income'] : $summary['new_projected_income'];
        $summary['unit_price'] = number_format((float) ($tuitionConfig['new_tuition_full_amount'] ?? 0), 2, '.', '');
        $summary['youth_unit_price'] = number_format((float) ($tuitionConfig['new_tuition_youth_amount'] ?? 0), 2, '.', '');

        return [
            'school_year' => $schoolYear,
            'semester' => $semester,
            'calculator_mode' => $mode,
            'options' => $options,
            'summary' => $summary,
            'families' => $familyRows,
        ];
    }

    public function getAvailableSchoolYears(): array
    {
        $values = [];

        if ($this->enrollmentModel->db->tableExists('enrollments')) {
            $rows = $this->enrollmentModel->db->table('enrollments')
                ->select('school_year')
                ->where('school_year IS NOT NULL', null, false)
                ->where('school_year !=', '')
                ->groupBy('school_year')
                ->orderBy('school_year', 'DESC')
                ->get()
                ->getResultArray();

            foreach ($rows as $row) {
                $value = trim((string) ($row['school_year'] ?? ''));
                if ($value !== '') {
                    $values[$value] = $value;
                }
            }
        }

        if ($this->paymentModel->db->tableExists('invoices')) {
            $rows = $this->paymentModel->db->table('invoices')
                ->select('school_year')
                ->where('school_year IS NOT NULL', null, false)
                ->where('school_year !=', '')
                ->groupBy('school_year')
                ->orderBy('school_year', 'DESC')
                ->get()
                ->getResultArray();

            foreach ($rows as $row) {
                $value = trim((string) ($row['school_year'] ?? ''));
                if ($value !== '') {
                    $values[$value] = $value;
                }
            }
        }

        if ($values === []) {
            $default = $this->getDefaultSchoolYear();
            if ($default !== '') {
                $values[$default] = $default;
            }
        }

        krsort($values);

        return array_values($values);
    }

    public function getAvailableSemesters(): array
    {
        $values = [];

        if ($this->enrollmentModel->db->tableExists('enrollments')) {
            $rows = $this->enrollmentModel->db->table('enrollments')
                ->select('semester')
                ->where('semester IS NOT NULL', null, false)
                ->where('semester !=', '')
                ->groupBy('semester')
                ->orderBy('semester', 'ASC')
                ->get()
                ->getResultArray();

            foreach ($rows as $row) {
                $value = trim((string) ($row['semester'] ?? ''));
                if ($value !== '') {
                    $values[$value] = $value;
                }
            }
        }

        if ($values === []) {
            $default = $this->getDefaultSemester();
            if ($default !== '') {
                $values[$default] = $default;
            }
        }

        return array_values($values);
    }

    protected function normalizeMode(string $mode): string
    {
        $value = strtolower(trim($mode));

        return in_array($value, ['old', 'new'], true) ? $value : 'compare';
    }

    protected function normalizeOptions(array $options): array
    {
        $includeWithdrawnMode = strtolower(trim((string) ($options['include_withdrawn_mode'] ?? 'refund_deadline')));
        if (!in_array($includeWithdrawnMode, ['refund_deadline', 'include', 'exclude'], true)) {
            $includeWithdrawnMode = 'refund_deadline';
        }

        return [
            'include_withdrawn_mode' => $includeWithdrawnMode,
            'include_payment_pending' => $this->toBool($options['include_payment_pending'] ?? true),
            'include_event_only' => $this->toBool($options['include_event_only'] ?? false),
            'include_paid_invoices' => $this->toBool($options['include_paid_invoices'] ?? true),
            'unit_price' => $this->normalizeMoney($options['unit_price'] ?? null),
            'youth_unit_price' => $this->normalizeMoney($options['youth_unit_price'] ?? null),
        ];
    }

    protected function loadFamilies(string $schoolYear, string $semester): array
    {
        $builder = $this->enrollmentModel->db->table('enrollments e')
            ->select("e.parent_id, CONCAT(COALESCE(u.lastname, ''), ', ', COALESCE(u.firstname, '')) AS parent_name", false)
            ->join('users u', 'u.id = e.parent_id', 'left')
            ->where('e.school_year', $schoolYear)
            ->groupBy('e.parent_id')
            ->orderBy('parent_name', 'ASC');

        $this->applyEnrollmentSemesterFilter($builder, 'e', $semester);

        return $builder->get()->getResultArray();
    }

    protected function loadFamilyStudents(int $parentId, string $schoolYear, string $semester, array $options): array
    {
        $rows = $this->enrollmentModel->db->table('enrollments e')
            ->select('e.*, s.firstname, s.lastname, s.is_active')
            ->join('students s', 's.id = e.student_id', 'left')
            ->where('e.parent_id', $parentId)
            ->where('e.school_year', $schoolYear)
            ->orderBy('e.updated_at', 'DESC')
            ->orderBy('e.id', 'DESC')
            ->get()
            ->getResultArray();

        $latestByStudent = [];
        foreach ($rows as $row) {
            $studentId = (int) ($row['student_id'] ?? 0);
            if ($studentId <= 0 || isset($latestByStudent[$studentId])) {
                continue;
            }

            if (!$this->matchesEnrollmentSemester($row, $semester)) {
                continue;
            }

            $latestByStudent[$studentId] = $row;
        }

        ksort($latestByStudent);

        $withinRefundWindow = $this->isWithinRefundWindow((string) ($this->configurationModel->getConfig('refund_deadline') ?? ''));
        $students = [];
        $allStudents = [];
        $warnings = [];
        $eventOnlyCount = 0;
        $missingClassCount = 0;
        $inactiveCount = 0;
        $pendingSkippedCount = 0;

        foreach ($latestByStudent as $row) {
            $studentId = (int) ($row['student_id'] ?? 0);
            $studentName = trim(((string) ($row['firstname'] ?? '')) . ' ' . ((string) ($row['lastname'] ?? '')));
            $studentLabel = $studentName !== '' ? $studentName : ('Student #' . $studentId);

            if ((int) ($row['is_active'] ?? 1) === 0) {
                $inactiveCount++;
                $allStudents[] = [
                    'student_id' => $studentId,
                    'student_name' => $studentLabel,
                    'grade_level' => 'N/A',
                    'billable' => false,
                    'excluded_reason' => 'inactive',
                ];
                continue;
            }

            $inclusion = $this->resolveEnrollmentInclusion($row, $options, $withinRefundWindow);
            if (!$inclusion['include']) {
                if (($inclusion['reason'] ?? '') === 'payment_pending_excluded') {
                    $pendingSkippedCount++;
                }
                continue;
            }

            $classFlags = $this->resolveStudentClassFlags($studentId, $schoolYear);
            $gradeName = $this->resolveGradeName($studentId, $schoolYear, $row['class_section_id'] ?? null);
            $studentBase = [
                'student_id' => $studentId,
                'student_name' => $studentLabel,
                'grade_level' => $gradeName,
                'billable' => false,
                'excluded_reason' => null,
            ];

            if (!$classFlags['has_any_assignment']) {
                $missingClassCount++;
                $studentBase['excluded_reason'] = 'missing_class_assignment';
                $allStudents[] = $studentBase;
                continue;
            }

            if (!$classFlags['has_non_event_assignment']) {
                $eventOnlyCount++;
                $studentBase['excluded_reason'] = 'event_only';
                $allStudents[] = $studentBase;
                if ($options['include_event_only']) {
                    $warnings[] = $studentLabel . ' is event-only and excluded from tuition billing.';
                }
                continue;
            }

            if ($gradeName === 'N/A') {
                $warnings[] = $studentLabel . ' has no resolved class section grade.';
            }

            $studentBase['billable'] = true;
            $allStudents[] = $studentBase;
            $students[] = [
                'student_id' => $studentId,
                'student_name' => $studentLabel,
                'grade_level' => $gradeName,
            ];
        }

        if ($eventOnlyCount > 0) {
            $warnings[] = $eventOnlyCount . ' event-only student(s) excluded from tuition.';
        }

        if ($missingClassCount > 0) {
            $warnings[] = $missingClassCount . ' student(s) missing class assignments.';
        }

        if ($inactiveCount > 0) {
            $warnings[] = $inactiveCount . ' inactive student(s) skipped.';
        }

        if ($pendingSkippedCount > 0) {
            $warnings[] = $pendingSkippedCount . ' payment-pending student(s) skipped by filter.';
        }

        if ($this->toCents($this->configurationModel->getConfig('new_tuition_full_amount') ?? 0) <= 0) {
            $warnings[] = 'New tuition full amount is missing or zero.';
        }

        return [
            'students' => $students,
            'all_students' => $allStudents,
            'warnings' => array_values(array_unique($warnings)),
        ];
    }

    protected function resolveEnrollmentInclusion(array $row, array $options, bool $withinRefundWindow): array
    {
        $admissionStatus = strtolower(trim((string) ($row['admission_status'] ?? '')));
        $enrollmentStatus = strtolower(trim((string) ($row['enrollment_status'] ?? '')));

        if ($admissionStatus === 'denied' || $enrollmentStatus === 'admission under review') {
            return ['include' => false, 'reason' => 'not_admitted'];
        }

        if ($enrollmentStatus === 'payment pending') {
            return ['include' => $options['include_payment_pending'], 'reason' => 'payment_pending_excluded'];
        }

        if ($enrollmentStatus === 'enrolled') {
            return ['include' => true, 'reason' => null];
        }

        if (in_array($enrollmentStatus, ['withdrawn', 'refund pending', 'withdraw under review'], true)) {
            if ($options['include_withdrawn_mode'] === 'include') {
                return ['include' => true, 'reason' => null];
            }

            if ($options['include_withdrawn_mode'] === 'exclude') {
                return ['include' => false, 'reason' => 'withdrawn_excluded'];
            }

            return ['include' => !$withinRefundWindow, 'reason' => 'refund_deadline_excluded'];
        }

        return ['include' => $admissionStatus === 'accepted', 'reason' => 'accepted_only'];
    }

    protected function resolveStudentClassFlags(int $studentId, string $schoolYear): array
    {
        $rows = $this->studentClassModel->where('student_id', $studentId)
            ->where('school_year', $schoolYear)
            ->findAll();

        $hasAnyAssignment = !empty($rows);
        $hasNonEventAssignment = false;
        $hasEventOnlyAssignment = false;

        foreach ($rows as $row) {
            $isEventOnly = (int) ($row['is_event_only'] ?? 0) === 1;
            $hasEventOnlyAssignment = $hasEventOnlyAssignment || $isEventOnly;
            $hasNonEventAssignment = $hasNonEventAssignment || !$isEventOnly;
        }

        return [
            'has_any_assignment' => $hasAnyAssignment,
            'has_non_event_assignment' => $hasNonEventAssignment,
            'has_event_only_assignment' => $hasEventOnlyAssignment,
        ];
    }

    protected function resolveGradeName(int $studentId, string $schoolYear, $classSectionId = null): string
    {
        $sectionId = $classSectionId;

        if (empty($sectionId)) {
            $row = $this->studentClassModel
                ->select('class_section_id')
                ->where('student_id', $studentId)
                ->where('school_year', $schoolYear)
                ->where('is_event_only', 0)
                ->orderBy('updated_at', 'DESC')
                ->first();
            $sectionId = $row['class_section_id'] ?? null;
        }

        if (empty($sectionId)) {
            return 'N/A';
        }

        $name = $this->classSectionModel->getClassSectionNameBySectionId($sectionId);

        return is_string($name) && trim($name) !== '' ? strtoupper(trim($name)) : 'N/A';
    }

    protected function calculateAlreadyCollected(int $parentId, string $schoolYear, string $semester): int
    {
        $paymentBuilder = $this->paymentModel->db->table('payments')
            ->select('COALESCE(SUM(paid_amount),0) AS total_amount')
            ->where('parent_id', $parentId)
            ->where('school_year', $schoolYear);

        if ($semester !== '') {
            $paymentBuilder->where('semester', $semester);
        }

        if ($this->paymentModel->db->fieldExists('status', 'payments')) {
            $paymentBuilder->groupStart()
                ->whereNotIn('status', FinancialStatus::EXCLUDED_PAYMENT_STATUSES)
                ->orWhere('status IS NULL', null, false)
                ->groupEnd();
        }

        if ($this->paymentModel->db->fieldExists('is_void', 'payments')) {
            $paymentBuilder->groupStart()
                ->where('is_void', 0)
                ->orWhere('is_void IS NULL', null, false)
                ->groupEnd();
        }

        $paymentRow = $paymentBuilder->get()->getRowArray();
        $paidCents = $this->toCents($paymentRow['total_amount'] ?? 0);

        $refundBuilder = $this->refundModel->db->table('refunds')
            ->select('COALESCE(SUM(refund_paid_amount),0) AS total_amount')
            ->where('parent_id', $parentId)
            ->where('school_year', $schoolYear)
            ->whereIn('status', FinancialStatus::REFUND_REDUCES_INVOICE_STATUSES);

        if ($semester !== '' && $this->refundModel->db->fieldExists('semester', 'refunds')) {
            $refundBuilder->where('semester', $semester);
        }

        $refundRow = $refundBuilder->get()->getRowArray();
        $refundCents = $this->toCents($refundRow['total_amount'] ?? 0);

        return max(0, $paidCents - $refundCents);
    }

    protected function mergeStudentDetails(array $allStudents, array $oldDetails, array $newDetails): array
    {
        $oldMap = [];
        foreach ($oldDetails as $detail) {
            $oldMap[(int) ($detail['student_id'] ?? 0)] = $detail;
        }

        $newMap = [];
        foreach ($newDetails as $detail) {
            $newMap[(int) ($detail['student_id'] ?? 0)] = $detail;
        }

        $rows = [];
        foreach ($allStudents as $student) {
            $studentId = (int) ($student['student_id'] ?? 0);
            $old = $oldMap[$studentId] ?? null;
            $new = $newMap[$studentId] ?? null;

            $rows[] = [
                'student_id' => $studentId,
                'student_name' => $student['student_name'] ?? '',
                'grade_level' => $student['grade_level'] ?? 'N/A',
                'billable' => (bool) ($student['billable'] ?? false),
                'excluded_reason' => $student['excluded_reason'] ?? null,
                'old_rule' => $old['rule'] ?? null,
                'old_amount' => $old['amount'] ?? '0.00',
                'new_rule' => $new['rule'] ?? null,
                'new_amount' => $new['amount'] ?? '0.00',
            ];
        }

        return $rows;
    }

    protected function getTuitionConfig(): array
    {
        return [
            'grade_fee' => $this->configurationModel->getConfig('grade_fee'),
            'first_student_fee' => $this->configurationModel->getConfig('first_student_fee'),
            'second_student_fee' => $this->configurationModel->getConfig('second_student_fee'),
            'youth_fee' => $this->configurationModel->getConfig('youth_fee') ?? '200.00',
            'new_tuition_full_amount' => $this->normalizedUnitPriceOverride(),
            'new_tuition_youth_amount' => $this->normalizedYouthUnitPriceOverride(),
            'new_tuition_second_student_discount' => $this->configurationModel->getConfig('new_tuition_second_student_discount'),
            'new_tuition_third_student_discount' => $this->configurationModel->getConfig('new_tuition_third_student_discount'),
            'new_tuition_fourth_plus_discount' => $this->configurationModel->getConfig('new_tuition_fourth_plus_discount'),
        ];
    }

    protected ?string $unitPriceOverride = null;
    protected ?string $youthUnitPriceOverride = null;

    protected function normalizedUnitPriceOverride(): string
    {
        return $this->unitPriceOverride
            ?? (string) ($this->configurationModel->getConfig('new_tuition_full_amount') ?? '0.00');
    }

    protected function normalizedYouthUnitPriceOverride(): string
    {
        return $this->youthUnitPriceOverride
            ?? (string) (
                $this->configurationModel->getConfig('new_tuition_youth_amount')
                ?? $this->configurationModel->getConfig('youth_fee')
                ?? '200.00'
            );
    }

    protected function normalizeMoney($value): ?string
    {
        if ($value === null || trim((string) $value) === '') {
            return null;
        }

        $normalized = preg_replace('/[^0-9.]+/', '', (string) $value);
        if ($normalized === '' || !is_numeric($normalized)) {
            return null;
        }

        $amount = (float) $normalized;

        return $amount > 0 ? number_format($amount, 2, '.', '') : null;
    }

    protected function isWithinRefundWindow(string $refundDeadline): bool
    {
        if ($refundDeadline === '') {
            return true;
        }

        try {
            $timeZone = new \DateTimeZone((string) (config('School')->attendance['timezone'] ?? user_timezone()));
            $today = new \DateTimeImmutable('today', $timeZone);
            $deadline = new \DateTimeImmutable($refundDeadline, $timeZone);

            return $today <= $deadline;
        } catch (\Throwable $e) {
            return true;
        }
    }

    protected function getDefaultSchoolYear(): string
    {
        return (string) ($this->configurationModel->getConfig('school_year') ?? '');
    }

    protected function getDefaultSemester(): string
    {
        return '';
    }

    protected function applyEnrollmentSemesterFilter($builder, string $alias, string $semester): void
    {
        if ($semester === '') {
            return;
        }

        $builder->groupStart()
            ->where($alias . '.semester', $semester)
            ->orWhere($alias . '.semester', '')
            ->orWhere($alias . '.semester IS NULL', null, false)
            ->groupEnd();
    }

    protected function matchesEnrollmentSemester(array $row, string $semester): bool
    {
        if ($semester === '') {
            return true;
        }

        $value = trim((string) ($row['semester'] ?? ''));

        return $value === '' || strcasecmp($value, $semester) === 0;
    }

    protected function toBool($value): bool
    {
        if (is_bool($value)) {
            return $value;
        }

        $normalized = strtolower(trim((string) $value));

        return in_array($normalized, ['1', 'true', 'yes', 'on'], true);
    }

    protected function toCents($amount): int
    {
        return (int) round(((float) $amount) * 100);
    }

    protected function fromCents(int $cents): string
    {
        return number_format($cents / 100, 2, '.', '');
    }
}
