<?php

namespace App\Services\SchoolYears;

use App\Models\AuditLog;
use App\Models\Configuration;
use App\Models\ParentAccount;
use App\Models\ParentBalanceTransfer;
use App\Models\SchoolYear;
use App\Services\Promotions\LevelProgressionService;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Validation\ValidationException;
use RuntimeException;

class SchoolYearClosureService
{
    public function __construct(
        private SchoolYearResolver $resolver,
        private LevelProgressionService $progression
    ) {
    }

    public function list(): array
    {
        $this->resolver->ensureCurrentTracked();

        return SchoolYear::query()
            ->orderByDesc('start_date')
            ->orderByDesc('id')
            ->get()
            ->map(fn (SchoolYear $year) => $this->presentSchoolYear($year))
            ->all();
    }

    public function createYear(array $payload, ?int $actorId = null): array
    {
        $name = trim((string) ($payload['name'] ?? ''));
        $startDate = (string) ($payload['start_date'] ?? '');
        $endDate = (string) ($payload['end_date'] ?? '');

        $errors = [];
        if ($name === '') {
            $errors[] = 'School year name is required.';
        }
        if ($startDate === '' || $endDate === '') {
            $errors[] = 'Start date and end date are required.';
        }
        if ($startDate !== '' && $endDate !== '' && $startDate >= $endDate) {
            $errors[] = 'Start date must be before end date.';
        }
        if (SchoolYear::query()->where('name', $name)->exists()) {
            $errors[] = sprintf('School year %s already exists.', $name);
        }
        if ($this->hasDateOverlap($startDate, $endDate)) {
            $errors[] = 'School year dates overlap an existing year.';
        }

        if ($errors !== []) {
            throw ValidationException::withMessages(['school_year' => $errors]);
        }

        $year = SchoolYear::query()->create([
            'name' => $name,
            'start_date' => $startDate,
            'end_date' => $endDate,
            'status' => SchoolYear::STATUS_DRAFT,
            'is_current' => false,
        ]);

        $this->logAudit(
            $actorId,
            'school_year_created',
            'school_years',
            (int) $year->id,
            null,
            $year->toArray(),
            ['mode' => 'draft']
        );

        return $this->presentSchoolYear($year);
    }

    public function updateYear(int $id, array $payload, ?int $actorId = null): array
    {
        $year = $this->findOrFail($id);
        $oldState = $year->toArray();
        $name = trim((string) ($payload['name'] ?? $year->name));
        $startDate = (string) ($payload['start_date'] ?? optional($year->start_date)->toDateString());
        $endDate = (string) ($payload['end_date'] ?? optional($year->end_date)->toDateString());

        $errors = [];
        if ($startDate === '' || $endDate === '') {
            $errors[] = 'Start date and end date are required.';
        }
        if ($startDate !== '' && $endDate !== '' && $startDate >= $endDate) {
            $errors[] = 'Start date must be before end date.';
        }
        if ($name === '') {
            $errors[] = 'School year name is required.';
        }
        if (SchoolYear::query()->where('name', $name)->where('id', '!=', $year->id)->exists()) {
            $errors[] = sprintf('School year %s already exists.', $name);
        }
        if ($this->hasDateOverlap($startDate, $endDate, (int) $year->id)) {
            $errors[] = 'School year dates overlap an existing year.';
        }
        if ($name !== $year->name && $this->schoolYearHasOperationalRecords($year->name)) {
            $errors[] = 'This school year name is already referenced by operational records and cannot be renamed safely.';
        }

        if ($errors !== []) {
            throw ValidationException::withMessages(['school_year' => $errors]);
        }

        $year->name = $name;
        $year->start_date = $startDate;
        $year->end_date = $endDate;
        $year->save();

        if ($year->is_current) {
            Configuration::setConfigValueByKey('school_year', $year->name);
        }

        $this->logAudit(
            $actorId,
            'school_year_updated',
            'school_years',
            (int) $year->id,
            $oldState,
            $year->fresh()?->toArray(),
            null
        );

        return $this->presentSchoolYear($year->fresh());
    }

    public function summary(int $id): array
    {
        $year = $this->findOrFail($id);
        $balances = $this->parentBalances($id);
        $promotions = $this->promotionPreview($id);

        return [
            'school_year' => $this->presentSchoolYear($year),
            'totals' => [
                'students_considered' => $promotions['summary']['total_students'],
                'students_blocked' => $promotions['summary']['hold'],
                'parents_with_balances' => $balances['summary']['parents_with_non_zero_balance'],
                'net_balance_to_transfer' => $balances['summary']['net_balance_to_transfer'],
            ],
        ];
    }

    public function validateClose(int $id, array $payload): array
    {
        $year = $this->findOrFail($id);
        $newYear = $payload['new_school_year'] ?? [];
        $targetName = trim((string) ($newYear['name'] ?? $this->resolver->suggestNextName($year->name) ?? ''));
        $promotionPreview = $this->buildPromotionPreview($year->name, $targetName);
        $balances = $this->buildParentBalanceRows($year->name, $targetName);

        $errors = [];
        $warnings = [];

        if ($year->status !== SchoolYear::STATUS_ACTIVE) {
            $errors[] = sprintf('Only an active school year can be closed. %s is currently %s.', $year->name, $year->status);
        }

        $errors = array_merge($errors, $this->validateNewYearPayload($year, $newYear));

        if ($promotionPreview['summary']['hold'] > 0) {
            $errors[] = sprintf('%d students are missing promotion decisions or target placement.', $promotionPreview['summary']['hold']);
        }

        $pendingPayments = $this->countPendingPayments($year->name);
        if ($pendingPayments > 0) {
            $errors[] = sprintf('%d payments are not posted or still pending.', $pendingPayments);
        }

        $draftInvoices = $this->countDraftInvoices($year->name);
        if ($draftInvoices > 0) {
            $errors[] = sprintf('%d invoices are still in draft status.', $draftInvoices);
        }

        $invalidInvoices = $this->countFinancialInconsistencies($year->name);
        if ($invalidInvoices > 0) {
            $errors[] = sprintf('%d invoices have inconsistent amount, paid amount, and balance values.', $invalidInvoices);
        }

        $duplicateAccounts = $this->countDuplicateParentAccounts();
        if ($duplicateAccounts > 0) {
            $errors[] = sprintf('%d duplicate parent account rows exist.', $duplicateAccounts);
        }

        if ($balances['summary']['existing_transfer_conflicts'] > 0) {
            $errors[] = sprintf(
                '%d parent balance transfers already exist for the requested year transition.',
                $balances['summary']['existing_transfer_conflicts']
            );
        }

        if ($balances['summary']['parents_with_credit_balance'] > 0) {
            $warnings[] = sprintf(
                '%d parents have a credit balance that will be carried into the new year.',
                $balances['summary']['parents_with_credit_balance']
            );
        }

        return [
            'can_close' => $errors === [],
            'errors' => $errors,
            'warnings' => $warnings,
            'promotion_summary' => $promotionPreview['summary'],
            'financial_summary' => $balances['summary'],
        ];
    }

    public function previewClose(int $id, array $payload): array
    {
        $year = $this->findOrFail($id);
        $newYear = $payload['new_school_year'] ?? [];
        $targetName = trim((string) ($newYear['name'] ?? $this->resolver->suggestNextName($year->name) ?? ''));

        return [
            'school_year' => $this->presentSchoolYear($year),
            'validation' => $this->validateClose($id, $payload),
            'promotion_preview' => $this->buildPromotionPreview($year->name, $targetName),
            'parent_balances' => $this->buildParentBalanceRows($year->name, $targetName),
        ];
    }

    public function close(int $id, array $payload, ?int $actorId): array
    {
        $year = $this->findOrFail($id);
        $validation = $this->validateClose($id, $payload);
        if (!$validation['can_close']) {
            throw ValidationException::withMessages(['school_year' => $validation['errors']]);
        }

        $confirmation = trim((string) ($payload['confirmation'] ?? ''));
        $expected = sprintf('CLOSE %s', $year->name);
        if ($confirmation !== $expected) {
            throw ValidationException::withMessages([
                'confirmation' => [sprintf('Confirmation must exactly match "%s".', $expected)],
            ]);
        }

        $newYearPayload = $payload['new_school_year'];
        $promotionPreview = $this->buildPromotionPreview($year->name, $newYearPayload['name']);
        $balances = !empty($payload['transfer_unpaid_balances'])
            ? $this->buildParentBalanceRows($year->name, $newYearPayload['name'])
            : ['rows' => [], 'summary' => ['net_balance_to_transfer' => 0]];

        return DB::transaction(function () use ($year, $payload, $actorId, $newYearPayload, $promotionPreview, $balances) {
            $newYear = SchoolYear::query()->firstOrCreate(
                ['name' => $newYearPayload['name']],
                [
                    'start_date' => $newYearPayload['start_date'],
                    'end_date' => $newYearPayload['end_date'],
                    'status' => SchoolYear::STATUS_DRAFT,
                    'is_current' => false,
                ]
            );

            if ($newYear->status === SchoolYear::STATUS_CLOSED) {
                throw new RuntimeException('The requested new school year already exists as closed and cannot be activated.');
            }

            SchoolYear::query()->where('is_current', true)->update([
                'is_current' => false,
                'updated_at' => now(),
            ]);

            $oldState = $year->toArray();
            $year->status = SchoolYear::STATUS_CLOSED;
            $year->is_current = false;
            $year->closed_at = now();
            $year->closed_by = $actorId;
            $year->save();

            $newYear->start_date = $newYearPayload['start_date'];
            $newYear->end_date = $newYearPayload['end_date'];
            $newYear->status = SchoolYear::STATUS_ACTIVE;
            $newYear->is_current = true;
            $newYear->closed_at = null;
            $newYear->closed_by = null;
            $newYear->save();

            Configuration::setConfigValueByKey('school_year', $newYear->name);

            $promotionCounts = $this->applyPromotions($promotionPreview['rows'], $newYear->name, $newYear->start_date, $actorId);
            $balanceCounts = !empty($payload['transfer_unpaid_balances'])
                ? $this->applyBalanceTransfers($balances['rows'], $year->name, $newYear->name, $actorId)
                : [
                    'transferred_parents' => 0,
                    'transferred_amount' => 0.0,
                    'credit_parents' => 0,
                    'credit_amount' => 0.0,
                ];

            $this->logAudit(
                $actorId,
                'school_year_closed',
                'school_years',
                (int) $year->id,
                $oldState,
                $year->fresh()?->toArray(),
                [
                    'new_school_year' => $newYear->name,
                    'promotion_counts' => $promotionCounts,
                    'balance_counts' => $balanceCounts,
                ]
            );

            return [
                'closed_school_year' => $this->presentSchoolYear($year->fresh()),
                'new_school_year' => $this->presentSchoolYear($newYear->fresh()),
                'promotion_counts' => $promotionCounts,
                'balance_counts' => $balanceCounts,
            ];
        });
    }

    public function reopen(int $id, ?int $actorId): array
    {
        $year = $this->findOrFail($id);

        return DB::transaction(function () use ($year, $actorId) {
            $currentActive = SchoolYear::query()->where('is_current', true)->where('id', '!=', $year->id)->first();
            if ($currentActive) {
                $currentActive->status = SchoolYear::STATUS_DRAFT;
                $currentActive->is_current = false;
                $currentActive->save();
            }

            $oldState = $year->toArray();
            $year->status = SchoolYear::STATUS_ACTIVE;
            $year->is_current = true;
            $year->closed_at = null;
            $year->closed_by = null;
            $year->save();

            Configuration::setConfigValueByKey('school_year', $year->name);

            $this->logAudit(
                $actorId,
                'school_year_reopened',
                'school_years',
                (int) $year->id,
                $oldState,
                $year->fresh()?->toArray(),
                ['previous_active_year' => $currentActive?->name]
            );

            return $this->presentSchoolYear($year->fresh());
        });
    }

    public function parentBalances(int $id, ?string $toSchoolYear = null): array
    {
        $year = $this->findOrFail($id);

        return $this->buildParentBalanceRows(
            $year->name,
            $toSchoolYear ?: ($this->resolver->suggestNextName($year->name) ?? $year->name)
        );
    }

    public function promotionPreview(int $id, ?string $toSchoolYear = null): array
    {
        $year = $this->findOrFail($id);

        return $this->buildPromotionPreview(
            $year->name,
            $toSchoolYear ?: ($this->resolver->suggestNextName($year->name) ?? $year->name)
        );
    }

    public function findOrFail(int $id): SchoolYear
    {
        $this->resolver->ensureCurrentTracked();

        return SchoolYear::query()->findOrFail($id);
    }

    private function validateNewYearPayload(SchoolYear $currentYear, array $newYear): array
    {
        $errors = [];
        $name = trim((string) ($newYear['name'] ?? ''));
        $startDate = $newYear['start_date'] ?? null;
        $endDate = $newYear['end_date'] ?? null;

        if ($name === '') {
            $errors[] = 'New school year name is required.';
        }

        if (!$startDate || !$endDate) {
            $errors[] = 'New school year start_date and end_date are required.';
            return $errors;
        }

        if ($startDate >= $endDate) {
            $errors[] = 'New school year start date must be before end date.';
        }

        $duplicateName = SchoolYear::query()
            ->where('name', $name)
            ->where('id', '!=', $currentYear->id)
            ->exists();
        if ($duplicateName) {
            $errors[] = sprintf('School year %s already exists.', $name);
        }

        $overlap = $this->hasDateOverlap($startDate, $endDate, (int) $currentYear->id);

        if ($overlap) {
            $errors[] = 'New school year dates overlap an existing school year.';
        }

        $otherActive = SchoolYear::query()
            ->where('status', SchoolYear::STATUS_ACTIVE)
            ->where('id', '!=', $currentYear->id)
            ->exists();

        if ($otherActive) {
            $errors[] = 'Another active school year already exists.';
        }

        return $errors;
    }

    private function hasDateOverlap(string $startDate, string $endDate, ?int $ignoreId = null): bool
    {
        if ($startDate === '' || $endDate === '') {
            return false;
        }

        return SchoolYear::query()
            ->when($ignoreId !== null, fn ($query) => $query->where('id', '!=', $ignoreId))
            ->whereDate('start_date', '<=', $endDate)
            ->whereDate('end_date', '>=', $startDate)
            ->exists();
    }

    private function schoolYearHasOperationalRecords(string $schoolYear): bool
    {
        $tables = [
            ['table' => 'invoices', 'column' => 'school_year'],
            ['table' => 'enrollments', 'column' => 'school_year'],
            ['table' => 'student_class', 'column' => 'school_year'],
            ['table' => 'payments', 'column' => 'school_year'],
            ['table' => 'student_decisions', 'column' => 'school_year'],
            ['table' => 'classSection', 'column' => 'school_year'],
            ['table' => 'classes', 'column' => 'school_year'],
        ];

        foreach ($tables as $mapping) {
            if (!Schema::hasTable($mapping['table'])) {
                continue;
            }
            if (DB::table($mapping['table'])->where($mapping['column'], $schoolYear)->exists()) {
                return true;
            }
        }

        return false;
    }

    private function buildPromotionPreview(string $currentYear, string $targetYear): array
    {
        $studentIds = collect(DB::table('student_class')->where('school_year', $currentYear)->pluck('student_id'))
            ->merge(DB::table('enrollments')->where('school_year', $currentYear)->pluck('student_id'))
            ->map(fn ($id) => (int) $id)
            ->unique()
            ->values();

        $students = DB::table('students')
            ->whereIn('id', $studentIds->all())
            ->get()
            ->keyBy('id');

        $assignments = DB::table('student_class')
            ->where('school_year', $currentYear)
            ->orderByDesc('updated_at')
            ->orderByDesc('id')
            ->get();

        $assignmentByStudent = [];
        foreach ($assignments as $assignment) {
            $studentId = (int) $assignment->student_id;
            if (!isset($assignmentByStudent[$studentId])) {
                $assignmentByStudent[$studentId] = $assignment;
            }
        }

        $classSections = DB::table('classSection')->get()->keyBy('class_section_id');
        $classes = DB::table('classes')->get()->keyBy('id');

        $enrollments = DB::table('enrollments')
            ->where('school_year', $currentYear)
            ->orderByDesc('updated_at')
            ->orderByDesc('id')
            ->get();
        $enrollmentByStudent = [];
        foreach ($enrollments as $enrollment) {
            $studentId = (int) $enrollment->student_id;
            if (!isset($enrollmentByStudent[$studentId])) {
                $enrollmentByStudent[$studentId] = $enrollment;
            }
        }

        $decisions = Schema::hasTable('student_decisions')
            ? DB::table('student_decisions')
                ->where('school_year', $currentYear)
                ->orderByDesc('updated_at')
                ->orderByDesc('id')
                ->get()
            : collect();
        $decisionByStudent = [];
        foreach ($decisions as $decision) {
            $studentId = (int) $decision->student_id;
            if (!isset($decisionByStudent[$studentId])) {
                $decisionByStudent[$studentId] = $decision;
            }
        }

        $rows = [];
        $summary = [
            'total_students' => 0,
            'promote' => 0,
            'repeat' => 0,
            'graduate' => 0,
            'transfer' => 0,
            'withdraw' => 0,
            'hold' => 0,
        ];

        foreach ($studentIds as $studentId) {
            $student = $students->get($studentId);
            if (!$student) {
                continue;
            }

            $assignment = $assignmentByStudent[$studentId] ?? null;
            $enrollment = $enrollmentByStudent[$studentId] ?? null;
            $decision = $decisionByStudent[$studentId] ?? null;
            $section = $assignment ? $classSections->get($assignment->class_section_id) : null;
            $currentClass = $section ? $classes->get($section->class_id) : null;
            $progression = $assignment
                ? $this->progression->resolveByCurrentClassSectionId((int) $assignment->class_section_id)
                : null;

            $decisionText = strtolower(trim((string) ($decision->decision ?? '')));
            $score = $decision && isset($decision->year_score) ? (float) $decision->year_score : null;
            $enrollmentStatus = strtolower(trim((string) ($enrollment->enrollment_status ?? '')));
            $warnings = [];

            $action = 'hold';
            if (in_array($enrollmentStatus, ['withdrawn', 'refund pending', 'withdraw under review', 'denied'], true)) {
                $action = 'withdraw';
            } elseif (str_contains($decisionText, 'transfer')) {
                $action = 'transfer';
            } elseif (str_contains($decisionText, 'withdraw')) {
                $action = 'withdraw';
            } elseif (str_contains($decisionText, 'graduat')) {
                $action = 'graduate';
            } elseif (in_array($decisionText, ['retained', 'repeat', 'repeated', 'not promoted'], true) || ($score !== null && $score < 60)) {
                $action = 'repeat';
            } elseif (($progression['is_terminal'] ?? false) === true) {
                $action = 'graduate';
            } elseif ($decisionText !== '' || $score !== null) {
                $action = 'promote';
            }

            $targetLevelId = null;
            $targetLevelName = null;
            if ($action === 'repeat') {
                $targetLevelId = $progression['current_level_id'] ?? ($section->class_id ?? null);
                $targetLevelName = $progression['current_level_name'] ?? ($currentClass->class_name ?? null);
            } elseif ($action === 'promote') {
                $targetLevelId = $progression['next_level_id'] ?? null;
                $targetLevelName = $progression['next_level_name'] ?? null;
                if ($targetLevelId === null) {
                    $action = 'hold';
                    $warnings[] = 'No next grade mapping exists for this student.';
                }
            }

            if ($action === 'hold' && $warnings === []) {
                $warnings[] = 'Missing promotion decision or score.';
            }

            [$targetClassSectionId, $targetClassSectionName] = $this->resolveTargetSection(
                $targetLevelId,
                $targetYear
            );

            $row = [
                'student_id' => $studentId,
                'student_name' => trim((string) ($student->firstname ?? '') . ' ' . (string) ($student->lastname ?? '')),
                'parent_id' => (int) ($student->parent_id ?? 0) ?: null,
                'current_grade' => $progression['current_level_name'] ?? ($currentClass->class_name ?? null),
                'current_class' => $section->class_section_name ?? null,
                'promotion_action' => $action,
                'target_grade' => $targetLevelName,
                'target_class' => $targetClassSectionName,
                'target_class_section_id' => $targetClassSectionId,
                'warnings' => $warnings,
                'decision' => $decision->decision ?? null,
                'score' => $score,
            ];

            $rows[] = $row;
            $summary['total_students']++;
            $summary[$action]++;
        }

        usort($rows, static fn (array $a, array $b) => strcmp($a['student_name'], $b['student_name']));

        return [
            'current_school_year' => $currentYear,
            'target_school_year' => $targetYear,
            'rows' => $rows,
            'summary' => $summary,
        ];
    }

    private function buildParentBalanceRows(string $fromSchoolYear, string $toSchoolYear): array
    {
        $invoices = DB::table('invoices')
            ->where('school_year', $fromSchoolYear)
            ->whereNotIn(DB::raw('LOWER(COALESCE(status, ""))'), ['void', 'voided', 'cancelled', 'canceled'])
            ->get();

        $byParent = [];
        foreach ($invoices as $invoice) {
            $parentId = (int) ($invoice->parent_id ?? 0);
            if ($parentId <= 0) {
                continue;
            }

            $balance = $invoice->balance !== null
                ? (float) $invoice->balance
                : (float) ($invoice->total_amount ?? 0) - (float) ($invoice->paid_amount ?? 0);

            if (!isset($byParent[$parentId])) {
                $existing = ParentBalanceTransfer::query()
                    ->where('parent_id', $parentId)
                    ->where('from_school_year', $fromSchoolYear)
                    ->where('to_school_year', $toSchoolYear)
                    ->first();

                $byParent[$parentId] = [
                    'parent_id' => $parentId,
                    'parent_name' => $this->resolveParentName($parentId),
                    'student_names' => $this->resolveStudentNames($parentId, $fromSchoolYear),
                    'unpaid_invoice_count' => 0,
                    'old_unpaid_balance' => 0.0,
                    'credit_balance' => 0.0,
                    'net_balance' => 0.0,
                    'amount_to_transfer' => 0.0,
                    'transfer_status' => $existing?->status,
                    'existing_transfer_id' => $existing?->id,
                    'source_invoice_ids' => [],
                ];
            }

            $byParent[$parentId]['source_invoice_ids'][] = (int) $invoice->id;
            $byParent[$parentId]['net_balance'] += $balance;
            if ($balance > 0) {
                $byParent[$parentId]['unpaid_invoice_count']++;
            }
        }

        $rows = [];
        $summary = [
            'parents_with_non_zero_balance' => 0,
            'parents_with_credit_balance' => 0,
            'existing_transfer_conflicts' => 0,
            'total_old_unpaid_balance' => 0.0,
            'total_credit_balance' => 0.0,
            'net_balance_to_transfer' => 0.0,
        ];

        foreach ($byParent as $row) {
            $net = round((float) $row['net_balance'], 2);
            if (abs($net) < 0.01) {
                continue;
            }

            $row['old_unpaid_balance'] = $net > 0 ? $net : 0.0;
            $row['credit_balance'] = $net < 0 ? abs($net) : 0.0;
            $row['amount_to_transfer'] = $net;
            $row['transfer_status'] = $row['transfer_status'] ?? ($net > 0 ? 'pending' : 'credit_pending');
            $rows[] = $row;

            $summary['parents_with_non_zero_balance']++;
            $summary['net_balance_to_transfer'] += $net;
            $summary['total_old_unpaid_balance'] += $row['old_unpaid_balance'];
            $summary['total_credit_balance'] += $row['credit_balance'];
            if ($row['credit_balance'] > 0) {
                $summary['parents_with_credit_balance']++;
            }
            if ($row['existing_transfer_id']) {
                $summary['existing_transfer_conflicts']++;
            }
        }

        usort($rows, static fn (array $a, array $b) => $b['amount_to_transfer'] <=> $a['amount_to_transfer']);

        $summary['net_balance_to_transfer'] = round($summary['net_balance_to_transfer'], 2);
        $summary['total_old_unpaid_balance'] = round($summary['total_old_unpaid_balance'], 2);
        $summary['total_credit_balance'] = round($summary['total_credit_balance'], 2);

        return [
            'from_school_year' => $fromSchoolYear,
            'to_school_year' => $toSchoolYear,
            'rows' => $rows,
            'summary' => $summary,
        ];
    }

    private function resolveTargetSection(?int $targetLevelId, string $schoolYear): array
    {
        if ($targetLevelId === null || $targetLevelId <= 0) {
            return [null, null];
        }

        $section = DB::table('classSection')
            ->where('class_id', $targetLevelId)
            ->where(function ($query) use ($schoolYear) {
                $query->whereNull('school_year')
                    ->orWhere('school_year', $schoolYear);
            })
            ->orderBy('class_section_id')
            ->first();

        if (!$section) {
            return [null, null];
        }

        return [(int) $section->class_section_id, $section->class_section_name];
    }

    private function applyPromotions(array $rows, string $newSchoolYear, string $enrollmentDate, ?int $actorId): array
    {
        $semester = trim((string) (Configuration::getConfig('semester') ?? ''));
        $counts = [
            'promote' => 0,
            'repeat' => 0,
            'graduate' => 0,
            'transfer' => 0,
            'withdraw' => 0,
        ];

        foreach ($rows as $row) {
            $action = $row['promotion_action'];
            if (!isset($counts[$action])) {
                continue;
            }

            $counts[$action]++;

            if (!in_array($action, ['promote', 'repeat'], true)) {
                $this->logAudit(
                    $actorId,
                    'school_year_promotion_decision_applied',
                    'students',
                    (int) $row['student_id'],
                    null,
                    null,
                    ['action' => $action, 'new_school_year' => $newSchoolYear]
                );
                continue;
            }

            $existingEnrollment = DB::table('enrollments')
                ->where('student_id', $row['student_id'])
                ->where('school_year', $newSchoolYear)
                ->first();

            if (!$existingEnrollment) {
                DB::table('enrollments')->insert([
                    'student_id' => $row['student_id'],
                    'class_section_id' => $row['target_class_section_id'],
                    'parent_id' => $row['parent_id'],
                    'enrollment_date' => $enrollmentDate,
                    'enrollment_status' => 'enrolled',
                    'withdrawal_date' => null,
                    'is_withdrawn' => 0,
                    'admission_status' => 'accepted',
                    'semester' => $semester !== '' ? $semester : null,
                    'school_year' => $newSchoolYear,
                    'created_at' => now(),
                    'updated_at' => now(),
                ]);
            }

            if ($row['target_class_section_id']) {
                $existingAssignment = DB::table('student_class')
                    ->where('student_id', $row['student_id'])
                    ->where('school_year', $newSchoolYear)
                    ->where('class_section_id', $row['target_class_section_id'])
                    ->exists();

                if (!$existingAssignment) {
                    DB::table('student_class')->insert([
                        'student_id' => $row['student_id'],
                        'class_section_id' => $row['target_class_section_id'],
                        'is_event_only' => 0,
                        'semester' => $semester !== '' ? $semester : 'Fall',
                        'school_year' => $newSchoolYear,
                        'created_at' => now(),
                        'updated_at' => now(),
                        'updated_by' => $actorId,
                    ]);
                }
            }

            if ($row['parent_id']) {
                ParentAccount::query()->firstOrCreate(
                    ['parent_id' => $row['parent_id'], 'school_year' => $newSchoolYear],
                    ['opening_balance' => 0, 'current_balance' => 0]
                );
            }

            $this->logAudit(
                $actorId,
                'school_year_promotion_decision_applied',
                'students',
                (int) $row['student_id'],
                null,
                null,
                [
                    'action' => $action,
                    'new_school_year' => $newSchoolYear,
                    'target_class_section_id' => $row['target_class_section_id'],
                ]
            );
        }

        return $counts;
    }

    private function applyBalanceTransfers(array $rows, string $fromSchoolYear, string $toSchoolYear, ?int $actorId): array
    {
        $counts = [
            'transferred_parents' => 0,
            'transferred_amount' => 0.0,
            'credit_parents' => 0,
            'credit_amount' => 0.0,
        ];

        foreach ($rows as $row) {
            $amount = round((float) $row['amount_to_transfer'], 2);
            if (abs($amount) < 0.01) {
                continue;
            }

            $transfer = ParentBalanceTransfer::query()->where([
                'parent_id' => $row['parent_id'],
                'from_school_year' => $fromSchoolYear,
                'to_school_year' => $toSchoolYear,
            ])->lockForUpdate()->first();

            if ($transfer) {
                throw new RuntimeException(sprintf(
                    'Parent %d already has a balance transfer for %s -> %s.',
                    $row['parent_id'],
                    $fromSchoolYear,
                    $toSchoolYear
                ));
            }

            ParentAccount::query()->updateOrCreate(
                ['parent_id' => $row['parent_id'], 'school_year' => $fromSchoolYear],
                ['opening_balance' => 0, 'current_balance' => $amount]
            );

            $newAccount = ParentAccount::query()->firstOrCreate(
                ['parent_id' => $row['parent_id'], 'school_year' => $toSchoolYear],
                ['opening_balance' => 0, 'current_balance' => 0]
            );
            $newAccount->opening_balance = round((float) $newAccount->opening_balance + $amount, 2);
            $newAccount->current_balance = round((float) $newAccount->current_balance + $amount, 2);
            $newAccount->save();

            $transfer = ParentBalanceTransfer::query()->create([
                'parent_id' => $row['parent_id'],
                'from_school_year' => $fromSchoolYear,
                'to_school_year' => $toSchoolYear,
                'amount' => $amount,
                'status' => 'transferred',
                'source_summary_json' => [
                    'source_invoice_ids' => $row['source_invoice_ids'],
                    'old_unpaid_balance' => $row['old_unpaid_balance'],
                    'credit_balance' => $row['credit_balance'],
                ],
                'created_by' => $actorId,
            ]);

            $newInvoiceId = null;
            if ($amount > 0) {
                $newInvoiceId = DB::table('invoices')->insertGetId([
                    'parent_id' => $row['parent_id'],
                    'invoice_number' => sprintf('OB-%s-%d', str_replace('-', '', $toSchoolYear), $transfer->id),
                    'total_amount' => $amount,
                    'balance' => $amount,
                    'paid_amount' => 0,
                    'has_discount' => 0,
                    'issue_date' => now()->toDateString(),
                    'due_date' => null,
                    'status' => 'unpaid',
                    'description' => sprintf('Opening balance transferred from %s', $fromSchoolYear),
                    'school_year' => $toSchoolYear,
                    'created_at' => now(),
                    'updated_at' => now(),
                    'updated_by' => $actorId,
                    'semester' => Configuration::getConfig('semester'),
                    'balance_transfer_id' => $transfer->id,
                ]);
                $transfer->new_invoice_id = $newInvoiceId;
                $transfer->save();

                $counts['transferred_parents']++;
                $counts['transferred_amount'] += $amount;
            } else {
                $counts['credit_parents']++;
                $counts['credit_amount'] += abs($amount);
            }

            $this->logAudit(
                $actorId,
                'school_year_balance_transferred',
                'parent_balance_transfers',
                (int) $transfer->id,
                null,
                $transfer->fresh()?->toArray(),
                ['new_invoice_id' => $newInvoiceId]
            );
        }

        $counts['transferred_amount'] = round($counts['transferred_amount'], 2);
        $counts['credit_amount'] = round($counts['credit_amount'], 2);

        return $counts;
    }

    private function countPendingPayments(string $schoolYear): int
    {
        if (!Schema::hasTable('payments')) {
            return 0;
        }

        return DB::table('payments')
            ->where('school_year', $schoolYear)
            ->whereIn(DB::raw('LOWER(COALESCE(status, ""))'), ['pending', 'processing', 'draft', 'unallocated'])
            ->count();
    }

    private function countDraftInvoices(string $schoolYear): int
    {
        return DB::table('invoices')
            ->where('school_year', $schoolYear)
            ->whereIn(DB::raw('LOWER(COALESCE(status, ""))'), ['draft', 'pending'])
            ->count();
    }

    private function countFinancialInconsistencies(string $schoolYear): int
    {
        return DB::table('invoices')
            ->where('school_year', $schoolYear)
            ->whereRaw('ABS(COALESCE(total_amount, 0) - COALESCE(paid_amount, 0) - COALESCE(balance, 0)) > 0.01')
            ->count();
    }

    private function countDuplicateParentAccounts(): int
    {
        if (!Schema::hasTable('parent_accounts')) {
            return 0;
        }

        return DB::table('parent_accounts')
            ->select('parent_id', 'school_year')
            ->groupBy('parent_id', 'school_year')
            ->havingRaw('COUNT(*) > 1')
            ->count();
    }

    private function resolveParentName(int $parentId): ?string
    {
        $row = DB::table('users')->where('id', $parentId)->first();
        if (!$row) {
            return null;
        }

        return trim((string) ($row->firstname ?? '') . ' ' . (string) ($row->lastname ?? ''));
    }

    private function resolveStudentNames(int $parentId, string $schoolYear): array
    {
        $studentIds = DB::table('enrollments')
            ->where('parent_id', $parentId)
            ->where('school_year', $schoolYear)
            ->pluck('student_id')
            ->map(fn ($id) => (int) $id)
            ->all();

        if ($studentIds === []) {
            $studentIds = DB::table('students')->where('parent_id', $parentId)->pluck('id')->map(fn ($id) => (int) $id)->all();
        }

        return DB::table('students')
            ->whereIn('id', $studentIds)
            ->get()
            ->map(fn ($student) => trim((string) ($student->firstname ?? '') . ' ' . (string) ($student->lastname ?? '')))
            ->filter()
            ->values()
            ->all();
    }

    private function presentSchoolYear(?SchoolYear $year): ?array
    {
        if (!$year) {
            return null;
        }

        return [
            'id' => (int) $year->id,
            'name' => $year->name,
            'start_date' => optional($year->start_date)->toDateString(),
            'end_date' => optional($year->end_date)->toDateString(),
            'status' => $year->status,
            'is_current' => (bool) $year->is_current,
            'closed_at' => optional($year->closed_at)->toDateTimeString(),
            'closed_by' => $year->closed_by,
        ];
    }

    private function logAudit(
        ?int $userId,
        string $action,
        ?string $tableName,
        ?int $recordId,
        ?array $oldValue,
        ?array $newValue,
        ?array $metadata = null
    ): void {
        if (!Schema::hasTable('audit_logs')) {
            return;
        }

        AuditLog::query()->create([
            'user_id' => $userId,
            'action' => $action,
            'table_name' => $tableName,
            'record_id' => $recordId,
            'old_value' => $oldValue,
            'new_value' => $newValue,
            'metadata' => $metadata,
            'created_at' => now(),
        ]);
    }
}
