<?php

namespace App\Services\Promotions;

use App\Models\Configuration;
use App\Models\Enrollment;
use App\Models\Student;
use App\Models\StudentPromotionRecord;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Support\Facades\DB;
use RuntimeException;

/**
 * Implements the parent-side enrollment workflow described in plan
 * sections 6, 8, 9 and 12. Tracks the parent's progress through the
 * checklist and finalises the promotion + creates the next-year
 * enrollment record once everything is complete.
 */
class PromotionEnrollmentService
{
    public const ALLOWED_STEPS = [
        'info_confirmed',
        'documents_uploaded',
        'agreement_accepted',
        'payment_completed',
    ];

    public function __construct(
        private PromotionStatusService $statusService,
        private PromotionAuditService $audit
    ) {
    }

    /**
     * Lists actionable promotion records for a parent (the parent
     * portal landing page in plan section 12).
     */
    public function overviewForParent(int $parentId, ?string $nextSchoolYear = null): array
    {
        $year = $nextSchoolYear ?: $this->guessNextSchoolYear();

        $query = StudentPromotionRecord::query()->forParent($parentId);
        if ($year !== null) {
            $query->forNextYear($year);
        }
        $records = $query
            ->orderByRaw('CASE WHEN promotion_status IN (?, ?, ?) THEN 0 ELSE 1 END', [
                StudentPromotionRecord::STATUS_AWAITING_PARENT,
                StudentPromotionRecord::STATUS_ELIGIBLE,
                StudentPromotionRecord::STATUS_ENROLLMENT_STARTED,
            ])
            ->orderBy('promotion_id', 'desc')
            ->get();

        $studentIds = $records->pluck('student_id')->unique()->all();
        /** @var EloquentCollection<int,Student> $studentsCollection */
        $studentsCollection = !empty($studentIds)
            ? Student::query()->whereIn('id', $studentIds)->get()
            : new EloquentCollection();
        $studentsById = $studentsCollection->keyBy('id');

        $payload = $records->map(function (StudentPromotionRecord $record) use ($studentsById) {
            return $this->presentRecord($record, $studentsById->get($record->student_id));
        })->all();

        return [
            'records' => $payload,
            'next_school_year' => $year,
            'message' => $this->parentBannerMessage($records),
        ];
    }

    /**
     * Mark the start of the enrollment process for a single promotion
     * record.
     */
    public function startEnrollment(StudentPromotionRecord $record, int $parentId, ?int $userId = null): StudentPromotionRecord
    {
        $this->assertParentOwns($record, $parentId);
        $this->assertActionable($record);

        return DB::transaction(function () use ($record, $parentId, $userId) {
            if (!$record->enrollment_started_at) {
                $record->enrollment_started_at = now();
            }
            $record->enrollment_status = StudentPromotionRecord::ENROLLMENT_IN_PROGRESS;
            $record->parent_id = $record->parent_id ?: $parentId;
            $record->updated_by = $userId;
            $record->save();

            if ($record->promotion_status !== StudentPromotionRecord::STATUS_ENROLLMENT_STARTED) {
                $this->statusService->transition(
                    $record,
                    StudentPromotionRecord::STATUS_ENROLLMENT_STARTED,
                    $userId,
                    'Parent started enrollment'
                );
            }
            $this->audit->logEnrollmentStarted($record, $userId);

            return $record->refresh();
        });
    }

    /**
     * Update one or more checklist steps. Returns the (possibly
     * finalised) record.
     */
    public function updateSteps(
        StudentPromotionRecord $record,
        int $parentId,
        array $steps,
        ?int $userId = null
    ): StudentPromotionRecord {
        $this->assertParentOwns($record, $parentId);
        $this->assertActionable($record);

        $sanitized = [];
        foreach ($steps as $field => $value) {
            if (!in_array($field, self::ALLOWED_STEPS, true)) {
                continue;
            }
            $sanitized[$field] = (bool) $value;
        }
        if (empty($sanitized)) {
            return $record;
        }

        return DB::transaction(function () use ($record, $parentId, $sanitized, $userId) {
            $changed = [];
            foreach ($sanitized as $field => $value) {
                if ((bool) $record->{$field} !== $value) {
                    $record->{$field} = $value;
                    $changed[] = $field;
                }
            }

            if (empty($changed)) {
                return $record;
            }

            if (!$record->enrollment_started_at) {
                $record->enrollment_started_at = now();
            }
            $record->enrollment_status = StudentPromotionRecord::ENROLLMENT_IN_PROGRESS;
            $record->parent_id = $record->parent_id ?: $parentId;
            $record->updated_by = $userId;
            $record->save();

            foreach ($changed as $field) {
                $this->audit->logEnrollmentStepCompleted($record, $field, $userId);
            }

            if ($record->promotion_status !== StudentPromotionRecord::STATUS_ENROLLMENT_STARTED) {
                if (in_array($record->promotion_status, [
                    StudentPromotionRecord::STATUS_ELIGIBLE,
                    StudentPromotionRecord::STATUS_AWAITING_PARENT,
                ], true)) {
                    $this->statusService->transition(
                        $record,
                        StudentPromotionRecord::STATUS_ENROLLMENT_STARTED,
                        $userId,
                        'Enrollment progress recorded'
                    );
                }
            }

            // Auto-finalise once all checklist items are complete.
            if ($record->enrollmentChecklistComplete()) {
                $record = $this->finalize($record, $userId);
            }

            return $record;
        });
    }

    /**
     * Submit the enrollment, asserting the checklist is complete.
     */
    public function submitEnrollment(
        StudentPromotionRecord $record,
        int $parentId,
        ?int $userId = null
    ): StudentPromotionRecord {
        $this->assertParentOwns($record, $parentId);
        $this->assertActionable($record);

        if (!$record->enrollmentChecklistComplete()) {
            throw new RuntimeException('Enrollment checklist is incomplete.');
        }

        return $this->finalize($record, $userId);
    }

    /**
     * Mark expired records (past deadline, still incomplete) as
     * "not_enrolled_for_next_year" (plan section 15).
     *
     * @return array{ expired:int, ids:array<int,int> }
     */
    public function markExpiredDeadlines(?\DateTimeInterface $now = null, ?int $userId = null): array
    {
        $now = $now ?: now();
        $records = StudentPromotionRecord::query()
            ->whereIn('promotion_status', [
                StudentPromotionRecord::STATUS_ELIGIBLE,
                StudentPromotionRecord::STATUS_AWAITING_PARENT,
                StudentPromotionRecord::STATUS_ENROLLMENT_STARTED,
            ])
            ->whereNotNull('enrollment_deadline')
            ->whereDate('enrollment_deadline', '<', $now)
            ->get();

        $ids = [];
        foreach ($records as $record) {
            DB::transaction(function () use ($record, $userId, &$ids) {
                $record->enrollment_status = StudentPromotionRecord::ENROLLMENT_EXPIRED;
                $record->save();
                $this->audit->logDeadlineExpired($record);
                $this->statusService->forceStatus(
                    $record,
                    StudentPromotionRecord::STATUS_NOT_ENROLLED,
                    $userId,
                    'Deadline expired without enrollment'
                );
                $ids[] = (int) $record->getKey();
            });
        }

        return [
            'expired' => count($ids),
            'ids' => $ids,
        ];
    }

    /**
     * Finalise promotion + create the next-year enrollment record
     * (plan sections 5, 6 step 6 and section 9).
     */
    private function finalize(StudentPromotionRecord $record, ?int $userId): StudentPromotionRecord
    {
        if ($record->promotion_status === StudentPromotionRecord::STATUS_PROMOTED_AND_ENROLLED) {
            return $record;
        }

        return DB::transaction(function () use ($record, $userId) {
            $record->enrollment_status = StudentPromotionRecord::ENROLLMENT_COMPLETED;
            $record->enrollment_completed_at = $record->enrollment_completed_at ?: now();
            $record->promotion_finalized_at = now();
            $record->updated_by = $userId;

            $enrollmentId = $this->ensureNextYearEnrollment($record);
            if ($enrollmentId !== null) {
                $record->enrollment_id = $enrollmentId;
            }
            $record->save();

            $this->audit->logEnrollmentCompleted($record, $userId);

            $this->statusService->transition(
                $record,
                StudentPromotionRecord::STATUS_PROMOTED_AND_ENROLLED,
                $userId,
                'Enrollment checklist complete'
            );
            $this->audit->logPromotionFinalized($record->refresh(), $userId);

            return $record->refresh();
        });
    }

    /**
     * Create or update an `enrollments` row for the next school year so
     * the student is officially placed in the new year (plan section 9
     * record-update rule).
     */
    private function ensureNextYearEnrollment(StudentPromotionRecord $record): ?int
    {
        $existing = Enrollment::query()
            ->where('student_id', $record->student_id)
            ->where('school_year', $record->next_school_year)
            ->first();

        $payload = [
            'student_id' => $record->student_id,
            'parent_id' => $record->parent_id,
            'school_year' => $record->next_school_year,
            'enrollment_status' => 'enrolled',
            'admission_status' => 'accepted',
            'is_withdrawn' => false,
            'enrollment_date' => now()->toDateString(),
        ];

        if ($existing) {
            $existing->fill($payload);
            $existing->save();
            return (int) $existing->getKey();
        }

        $created = Enrollment::query()->create($payload);
        return $created ? (int) $created->getKey() : null;
    }

    private function presentRecord(StudentPromotionRecord $record, ?Student $student): array
    {
        return [
            'promotion_id' => (int) $record->getKey(),
            'student_id' => (int) $record->student_id,
            'student_name' => $student
                ? trim((string) $student->firstname . ' ' . (string) $student->lastname)
                : null,
            'current_level' => $record->current_level_name,
            'promoted_level' => $record->promoted_level_name,
            'current_school_year' => $record->current_school_year,
            'next_school_year' => $record->next_school_year,
            'promotion_status' => $record->promotion_status,
            'enrollment_status' => $record->enrollment_status,
            'enrollment_deadline' => $record->enrollment_deadline?->toDateString(),
            'enrollment_started_at' => $record->enrollment_started_at?->toDateTimeString(),
            'enrollment_completed_at' => $record->enrollment_completed_at?->toDateTimeString(),
            'parent_action_required' => in_array(
                $record->promotion_status,
                StudentPromotionRecord::parentActionableStatuses(),
                true
            ),
            'checklist' => [
                'info_confirmed' => (bool) $record->info_confirmed,
                'documents_uploaded' => (bool) $record->documents_uploaded,
                'agreement_accepted' => (bool) $record->agreement_accepted,
                'payment_completed' => (bool) $record->payment_completed,
            ],
            'final_average' => $record->final_average !== null ? (float) $record->final_average : null,
            'passed_current_level' => $record->passed_current_level,
        ];
    }

    private function parentBannerMessage(EloquentCollection $records): ?string
    {
        $actionable = $records->first(function (StudentPromotionRecord $r) {
            return in_array(
                $r->promotion_status,
                StudentPromotionRecord::parentActionableStatuses(),
                true
            );
        });
        if (!$actionable) {
            return null;
        }
        $level = $actionable->promoted_level_name ?: 'the next level';
        $deadline = $actionable->enrollment_deadline?->toDateString();
        $deadlineText = $deadline ? 'by ' . $deadline : 'as soon as possible';
        return sprintf(
            'Your child is eligible for promotion to %s. Please complete enrollment for the new school year %s.',
            $level,
            $deadlineText
        );
    }

    private function assertParentOwns(StudentPromotionRecord $record, int $parentId): void
    {
        if ($parentId <= 0) {
            throw new RuntimeException('Authenticated parent is required.');
        }
        if ((int) $record->parent_id !== $parentId) {
            $studentParent = (int) (Student::query()->where('id', $record->student_id)->value('parent_id') ?? 0);
            if ($studentParent !== $parentId) {
                throw new RuntimeException('You are not authorised to act on this promotion record.');
            }
        }
    }

    private function assertActionable(StudentPromotionRecord $record): void
    {
        $actionable = StudentPromotionRecord::parentActionableStatuses();
        if (!in_array($record->promotion_status, $actionable, true)) {
            throw new RuntimeException(sprintf(
                'Promotion record is not actionable in status "%s".',
                $record->promotion_status
            ));
        }
    }

    private function guessNextSchoolYear(): ?string
    {
        $current = (string) (Configuration::getConfigValueByKey('school_year') ?? '');
        if (!preg_match('/^(\d{4})-(\d{4})$/', $current, $m)) {
            return null;
        }
        return ((int) $m[1] + 1) . '-' . ((int) $m[2] + 1);
    }
}
