<?php

namespace App\Services\Invoices;

use App\Models\AdditionalCharge;
use App\Models\ClassSection;
use App\Models\DiscountUsage;
use App\Models\Enrollment;
use App\Models\EventCharges;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\Refund;
use App\Models\Student;
use App\Models\StudentClass;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

class InvoicePdfService
{
    public function __construct(
        private InvoiceConfigService $config,
        private InvoiceGradeService $grades
    ) {
    }

    public function buildPdf(int $invoiceId): string
    {
        $data = $this->prepareInvoiceData($invoiceId);
        if (isset($data['error'])) {
            return $this->renderErrorPdf($data['error']);
        }

        return $this->renderPdfInvoice($data);
    }

    private function prepareInvoiceData(int $invoiceId): array
    {
        $invoice = Invoice::query()->find($invoiceId);
        if (!$invoice) {
            return ['error' => 'No invoice was generated. Please contact the school administration.'];
        }

        $parentId = (int) ($invoice->parent_id ?? 0);
        $schoolYear = (string) ($invoice->school_year ?? $this->config->getSchoolYear());

        $paymentsQuery = Payment::query()
            ->where('parent_id', $parentId)
            ->where('school_year', $schoolYear)
            ->where('invoice_id', $invoiceId);

        if (Schema::hasColumn('payments', 'status')) {
            $exclude = ['void', 'voided', 'refunded', 'failed', 'chargeback', 'declined', 'reversed', 'canceled', 'cancelled'];
            $paymentsQuery->where(function ($q) use ($exclude) {
                $q->whereNotIn('status', $exclude)->orWhereNull('status');
            });
        }
        if (Schema::hasColumn('payments', 'is_void')) {
            $paymentsQuery->where(function ($q) {
                $q->where('is_void', 0)->orWhereNull('is_void');
            });
        }

        $payments = $paymentsQuery->get()->map(fn ($r) => $r->toArray())->all();

        $parent = User::query()->find($parentId);
        if (!$parent) {
            return ['error' => 'Parent associated with the invoice was not found.'];
        }

        $enrollments = Enrollment::query()
            ->where('parent_id', $parentId)
            ->where('school_year', $schoolYear)
            ->get()
            ->map(fn ($r) => $r->toArray())
            ->all();

        if (empty($enrollments)) {
            return ['error' => 'No enrollments found for this invoice.'];
        }

        $registeredKids = [];
        $withdrawnKids = [];

        foreach ($enrollments as $enrollment) {
            $student = Student::query()->find($enrollment['student_id']);
            if (!$student) {
                continue;
            }

            $grade = StudentClass::getStudentGrade((int) $student->id);
            $studentData = [
                'student_id' => (int) $student->id,
                'student_firstname' => (string) ($student->firstname ?? ''),
                'student_lastname' => (string) ($student->lastname ?? ''),
                'grade' => $grade,
                'tuition_fee' => $enrollment['tuition_fee'] ?? 0,
                'enrollment_status' => $enrollment['enrollment_status'],
            ];

            if (in_array($enrollment['enrollment_status'], ['enrolled', 'payment pending'], true)) {
                $registeredKids[] = $studentData;
            } elseif (in_array($enrollment['enrollment_status'], ['withdrawn', 'withdraw under review', 'refund pending'], true)) {
                $withdrawnKids[] = $studentData;
            }
        }

        $registeredKids = array_values(array_filter($registeredKids, function ($student) use ($schoolYear) {
            $sid = (int) ($student['student_id'] ?? 0);
            return $sid > 0 && StudentClass::hasNonEventAssignment($sid, $schoolYear);
        }));

        $withdrawnKids = array_values(array_filter($withdrawnKids, function ($student) use ($schoolYear) {
            $sid = (int) ($student['student_id'] ?? 0);
            return $sid > 0 && StudentClass::hasNonEventAssignment($sid, $schoolYear);
        }));

        usort($registeredKids, fn ($a, $b) => $this->grades->compareGrades((string) $a['grade'], (string) $b['grade']));
        usort($withdrawnKids, fn ($a, $b) => $this->grades->compareGrades((string) $a['grade'], (string) $b['grade']));

        $refundAllowed = true;
        $refundDeadline = $this->config->getRefundDeadline();
        if ($refundDeadline) {
            try {
                $tz = new \DateTimeZone($this->config->getTimezone());
                $today = new \DateTimeImmutable('today', $tz);
                $deadline = new \DateTimeImmutable($refundDeadline, $tz);
                $refundAllowed = $today <= $deadline;
            } catch (\Throwable $e) {
                $refundAllowed = true;
            }
        }

        $studentCharges = [];
        $regularCount = 0;
        $gradeFee = $this->config->getGradeFee();
        $firstFee = $this->config->getFirstStudentFee();
        $secondFee = $this->config->getSecondStudentFee();
        $youthFee = $this->config->getYouthFee();

        $computeCharge = function (int $gradeLevel, string $gradeName) use (&$regularCount, $gradeFee, $firstFee, $secondFee, $youthFee) {
            $isRegular = $this->grades->isKindergarten($gradeName) || ($gradeLevel > 0 && $gradeLevel <= $gradeFee);
            if ($isRegular) {
                $fee = $regularCount === 0 ? $firstFee : $secondFee;
                $regularCount++;
                return $fee;
            }
            return $youthFee;
        };

        foreach ($registeredKids as $student) {
            $gradeName = (string) ($student['grade'] ?? '');
            $gradeLevel = $this->grades->gradeLevelInt($gradeName);
            $unitFee = $computeCharge($gradeLevel, $gradeName);
            $studentCharges[$student['student_id']] = ['unit_fee' => $unitFee, 'refund' => 0];
        }

        if (!$refundAllowed) {
            foreach ($withdrawnKids as $student) {
                $gradeName = (string) ($student['grade'] ?? '');
                $gradeLevel = $this->grades->gradeLevelInt($gradeName);
                $unitFee = $computeCharge($gradeLevel, $gradeName);
                $studentCharges[$student['student_id']] = ['unit_fee' => $unitFee, 'refund' => 0];
            }
        }

        $eventsList = EventCharges::getChargesWithEventInfo($parentId, $schoolYear);

        $studentIds = array_values(array_unique(array_map(static fn ($r) => (int) ($r['student_id'] ?? 0), array_merge($registeredKids, $withdrawnKids))));
        $schoolIdMap = [];
        if (!empty($studentIds)) {
            $rows = Student::query()->whereIn('id', $studentIds)->get(['id', 'school_id'])->all();
            foreach ($rows as $row) {
                $schoolIdMap[(int) $row->id] = $row->school_id ?? 'N/A';
            }
        }

        $students = array_merge($registeredKids, $withdrawnKids);
        foreach ($students as $i => $s) {
            $sid = (int) ($s['student_id'] ?? 0);
            $students[$i]['student_school_id'] = $schoolIdMap[$sid] ?? 'N/A';
        }

        $discounts = DiscountUsage::query()
            ->where('invoice_id', $invoiceId)
            ->where('parent_id', $parentId)
            ->where('school_year', $schoolYear)
            ->get()
            ->map(fn ($r) => $r->toArray())
            ->all();

        $refundsPaidTotal = (float) DB::table('refunds')
            ->selectRaw('COALESCE(SUM(refund_paid_amount),0) AS tot')
            ->where('invoice_id', $invoiceId)
            ->where('school_year', $schoolYear)
            ->whereIn('status', ['Partial', 'Paid'])
            ->value('tot');

        $acRows = AdditionalCharge::query()
            ->select('id', 'charge_type', 'title', 'description', 'amount', 'due_date', 'status', 'created_at')
            ->where('invoice_id', $invoiceId)
            ->where('status', '!=', 'void')
            ->orderBy('created_at')
            ->orderBy('id')
            ->get()
            ->map(fn ($r) => $r->toArray())
            ->all();

        $additionalChargeLines = [];
        $additionalChargesTotal = 0.0;
        foreach ($acRows as $ac) {
            $signed = (float) ($ac['amount'] ?? 0);
            $ctype = strtolower((string) ($ac['charge_type'] ?? ''));
            if ($ctype === 'deduct' && $signed > 0) {
                $signed = -$signed;
            } elseif ($ctype === 'add' && $signed < 0) {
                $signed = abs($signed);
            }

            $lineDate = !empty($ac['created_at'])
                ? date('Y-m-d', strtotime($ac['created_at']))
                : (!empty($invoice->created_at) ? local_date($invoice->created_at, 'Y-m-d') : local_date(utc_now(), 'Y-m-d'));

            $desc = $ac['description'] ?? '';
            if ($desc === '') {
                $desc = $ctype === 'deduct' ? 'Deduct' : 'Add';
            }

            $additionalChargesTotal += $signed;
            $additionalChargeLines[] = [
                'date' => $lineDate,
                'description' => $desc,
                'amount' => $signed,
            ];
        }

        return [
            'invoice' => $invoice->toArray(),
            'parent' => $parent->toArray(),
            'registeredKids' => $registeredKids,
            'withdrawnKids' => $withdrawnKids,
            'studentCharges' => $studentCharges,
            'events' => $eventsList,
            'students' => $students,
            'payments' => $payments,
            'discounts' => $discounts,
            'additionalChargeLines' => $additionalChargeLines,
            'additionalChargesTotal' => round($additionalChargesTotal, 2),
            'refundsPaidTotal' => $refundsPaidTotal,
        ];
    }

    private function renderPdfInvoice(array $data): string
    {
        $invoice = $data['invoice'];
        $parent = $data['parent'];
        $registeredKids = $data['registeredKids'] ?? [];
        $withdrawnKids = $data['withdrawnKids'] ?? [];
        $studentCharges = $data['studentCharges'] ?? [];
        $events = $data['events'] ?? [];
        $students = $data['students'] ?? [];
        $payments = $data['payments'] ?? [];
        $discounts = $data['discounts'] ?? [];
        $additionalChargeLines = $data['additionalChargeLines'] ?? [];
        $additionalChargesTotal = (float) ($data['additionalChargesTotal'] ?? 0);
        $refundsPaidTotal = (float) ($data['refundsPaidTotal'] ?? 0);

        $pdf = new \FPDF();
        $pdf->AddPage();
        $pdf->SetFont('Arial', 'B', 16);
        $pdf->Cell(0, 10, 'Invoice', 0, 1, 'C');
        $pdf->Ln(4);

        $pdf->SetFont('Arial', 'B', 12);
        $pdf->Cell(40, 6, 'Parent Name:', 0, 0, 'L');
        $pdf->SetFont('Arial', '', 12);
        $pdf->Cell(0, 6, ($parent['firstname'] ?? '') . ' ' . ($parent['lastname'] ?? ''), 0, 1, 'L');

        $pdf->SetFont('Arial', 'B', 12);
        $pdf->Cell(40, 6, 'Invoice Number:', 0, 0, 'L');
        $pdf->SetFont('Arial', '', 12);
        $pdf->Cell(0, 6, (string) ($invoice['invoice_number'] ?? ''), 0, 1, 'L');

        $pdf->Ln(8);
        $pdf->SetFont('Arial', 'B', 11);
        $pdf->Cell(30, 7, 'Date', 1);
        $pdf->Cell(130, 7, 'Description', 1);
        $pdf->Cell(30, 7, 'Amount', 1, 1, 'R');

        $transactions = [];
        $seq = 0;
        $push = function (\DateTimeImmutable $dt, string $desc, float $amount, string $cat) use (&$transactions, &$seq) {
            $transactions[] = [
                'dt' => $dt,
                'description' => $desc,
                'amount' => $amount,
                'cat' => $cat,
                'seq' => $seq++,
            ];
        };

        $tz = new \DateTimeZone($this->config->getTimezone());
        $toLocal = function (?string $raw) use ($tz): \DateTimeImmutable {
            if (!$raw) {
                return new \DateTimeImmutable('now', $tz);
            }
            try {
                return new \DateTimeImmutable($raw, $tz);
            } catch (\Throwable $e) {
                return new \DateTimeImmutable('now', $tz);
            }
        };

        foreach ($registeredKids as $student) {
            $id = $student['student_id'];
            $unit = (float) ($studentCharges[$id]['unit_fee'] ?? 0.0);
            $name = $student['student_firstname'] . ' ' . $student['student_lastname'];
            $gradeName = ClassSection::getClassSectionNameByClassId($student['grade']) ?? $student['grade'];
            $push($toLocal($invoice['created_at'] ?? null), 'Registration of "' . $name . '" in ' . $gradeName, $unit, 'registration');
        }

        foreach ($events as $event) {
            $studentName = 'N/A';
            if (!empty($event['student_id'])) {
                foreach ($students as $st) {
                    if (($st['student_id'] ?? null) == $event['student_id']) {
                        $studentName = $st['student_firstname'] . ' ' . $st['student_lastname'];
                        break;
                    }
                }
            }
            $amount = (float) ($event['charged'] ?? 0.0);
            $eventName = $event['event_name'] ?? 'Event';
            $push($toLocal($event['created_at'] ?? null), 'Event ' . $eventName . ' for "' . $studentName . '"', $amount, 'event');
        }

        foreach ($additionalChargeLines as $line) {
            $push($toLocal($line['date'] ?? null), (string) ($line['description'] ?? 'Additional Charge'), (float) ($line['amount'] ?? 0), 'additional');
        }

        $totalPaid = 0.0;
        foreach ($payments as $payment) {
            $amount = (float) ($payment['paid_amount'] ?? 0.0);
            $totalPaid += $amount;
            $push($toLocal($payment['payment_date'] ?? null), 'Payment (' . ($payment['payment_method'] ?? 'Payment') . ')', -$amount, 'payment');
        }

        $totalDiscount = 0.0;
        foreach ($discounts as $discount) {
            $amount = (float) ($discount['discount_amount'] ?? 0.0);
            $totalDiscount += $amount;
            $push($toLocal($discount['used_at'] ?? null), 'Discount applied', -$amount, 'discount');
        }

        usort($transactions, function ($a, $b) {
            $dayA = $a['dt']->format('Y-m-d');
            $dayB = $b['dt']->format('Y-m-d');
            if ($dayA !== $dayB) {
                return $a['dt'] <=> $b['dt'];
            }
            $pri = [
                'registration' => 10,
                'event' => 20,
                'additional' => 25,
                'discount' => 30,
                'payment' => 90,
                'other' => 50,
            ];
            $pa = $pri[$a['cat']] ?? 50;
            $pb = $pri[$b['cat']] ?? 50;
            if ($pa !== $pb) {
                return $pa <=> $pb;
            }
            $cmp = $a['dt'] <=> $b['dt'];
            return $cmp !== 0 ? $cmp : ($a['seq'] <=> $b['seq']);
        });

        $pdf->SetFont('Arial', '', 10);
        foreach ($transactions as $t) {
            $pdf->Cell(30, 7, $t['dt']->format('m-d-Y'), 1);
            $pdf->Cell(130, 7, $t['description'], 1);
            $pdf->Cell(30, 7, ($t['amount'] < 0 ? '-$' : '$') . number_format(abs($t['amount']), 2), 1, 1, 'R');
        }

        $tuitionSubtotal = 0.0;
        foreach ($studentCharges as $sc) {
            $tuitionSubtotal += (float) ($sc['unit_fee'] ?? 0.0);
        }
        $eventSubtotal = 0.0;
        foreach ($events as $ev) {
            $eventSubtotal += (float) ($ev['charged'] ?? 0.0);
        }

        $totalAmount = round($tuitionSubtotal + $eventSubtotal + $additionalChargesTotal, 2);
        $balance = $totalAmount - $totalPaid - $totalDiscount - $refundsPaidTotal;
        if ($balance < 0) {
            $balance = 0.0;
        }

        $pdf->Ln(5);
        $pdf->SetFont('Arial', 'B', 12);
        $pdf->Cell(160, 7, 'Total Charges:', 0, 0, 'R');
        $pdf->SetFont('Arial', '', 12);
        $pdf->Cell(30, 7, '$' . number_format($totalAmount, 2), 0, 1, 'R');

        $pdf->SetFont('Arial', 'B', 12);
        $pdf->Cell(160, 7, 'Total Paid:', 0, 0, 'R');
        $pdf->SetFont('Arial', '', 12);
        $pdf->Cell(30, 7, '$' . number_format($totalPaid, 2), 0, 1, 'R');

        if ($totalDiscount > 0) {
            $pdf->SetFont('Arial', 'B', 12);
            $pdf->Cell(160, 7, 'Total Discount:', 0, 0, 'R');
            $pdf->SetFont('Arial', '', 12);
            $pdf->Cell(30, 7, '$' . number_format($totalDiscount, 2), 0, 1, 'R');
        }

        if ($refundsPaidTotal > 0) {
            $pdf->SetFont('Arial', 'B', 12);
            $pdf->Cell(160, 7, 'Refunds Paid:', 0, 0, 'R');
            $pdf->SetFont('Arial', '', 12);
            $pdf->Cell(30, 7, '$' . number_format($refundsPaidTotal, 2), 0, 1, 'R');
        }

        $pdf->SetFont('Arial', 'B', 12);
        $pdf->Cell(160, 7, 'Balance Due:', 0, 0, 'R');
        $pdf->SetFont('Arial', '', 12);
        $pdf->Cell(30, 7, '$' . number_format($balance, 2), 0, 1, 'R');

        return $pdf->Output('S');
    }

    private function renderErrorPdf(string $message): string
    {
        $pdf = new \FPDF();
        $pdf->AddPage();
        $pdf->SetFont('Arial', 'B', 16);
        $pdf->Cell(0, 10, 'Error', 0, 1, 'C');
        $pdf->SetFont('Arial', '', 12);
        $pdf->Ln(10);
        $pdf->MultiCell(0, 10, $message);

        return $pdf->Output('S');
    }
}
