<?php

declare(strict_types=1);

namespace App\Services\Certificates;

use App\Models\CertificateRecord;
use App\Models\Configuration;
use App\Models\StudentDecision;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

class CertificateAdminService
{
    private bool $hasVerificationTokenColumn;

    public function __construct()
    {
        $this->hasVerificationTokenColumn = Schema::hasTable('certificate_records')
            && Schema::hasColumn('certificate_records', 'verification_token');
    }

    public function dashboard(?string $schoolYear = null): array
    {
        $selectedYear = $this->resolveSchoolYear($schoolYear);

        $allEnrolled = DB::table('student_class as sc')
            ->select(
                's.id as student_id',
                's.firstname',
                's.lastname',
                'sc.class_section_id',
                'cs.class_section_name'
            )
            ->join('students as s', 's.id', '=', 'sc.student_id')
            ->join('classSection as cs', 'cs.class_section_id', '=', 'sc.class_section_id')
            ->where('s.is_active', 1)
            ->where('sc.school_year', $selectedYear)
            ->orderBy('s.firstname', 'ASC')
            ->orderBy('s.lastname', 'ASC')
            ->get()
            ->map(fn ($row) => (array) $row)
            ->all();

        $studentIds = array_values(array_unique(array_map(
            static fn (array $row): int => (int) ($row['student_id'] ?? 0),
            $allEnrolled
        )));
        $studentIds = array_values(array_filter($studentIds, static fn (int $id): bool => $id > 0));

        $decisionsByStudent = [];
        if ($studentIds !== []) {
            $decisionRows = StudentDecision::query()
                ->whereIn('student_id', $studentIds)
                ->where('school_year', $selectedYear)
                ->get();

            foreach ($decisionRows as $decisionRow) {
                $sid = (int) ($decisionRow->student_id ?? 0);
                if ($sid <= 0) {
                    continue;
                }

                $decision = trim((string) ($decisionRow->decision ?? ''));
                $source = trim((string) ($decisionRow->source ?? ''));
                if ($source === '') {
                    $source = $decision === '' ? 'pending' : 'manual';
                }

                $decisionsByStudent[$sid][] = [
                    'decision' => $decision,
                    'source' => $source,
                    'notes' => (string) ($decisionRow->notes ?? ''),
                    'year_score' => is_numeric($decisionRow->year_score) ? round((float) $decisionRow->year_score, 2) : null,
                ];
            }
        }

        $certsByStudent = [];
        if ($studentIds !== []) {
            $certRows = DB::table('certificate_records')
                ->select('student_id', 'certificate_number', 'issued_at')
                ->whereIn('student_id', $studentIds)
                ->where('school_year', $selectedYear)
                ->orderByDesc('issued_at')
                ->get();

            foreach ($certRows as $record) {
                $sid = (int) ($record->student_id ?? 0);
                if ($sid > 0 && ! isset($certsByStudent[$sid])) {
                    $certsByStudent[$sid] = (string) ($record->certificate_number ?? '');
                }
            }
        }

        $statsPerClass = [];
        $studentsByClass = [];

        foreach ($allEnrolled as $row) {
            $sid = (int) ($row['student_id'] ?? 0);
            $classSectionId = (int) ($row['class_section_id'] ?? 0);
            $classSectionName = (string) ($row['class_section_name'] ?? ('Section '.$classSectionId));

            if (! isset($statsPerClass[$classSectionId])) {
                $statsPerClass[$classSectionId] = [
                    'name' => $classSectionName,
                    'total' => 0,
                    'pass' => 0,
                    'cert' => 0,
                ];
            }

            $statsPerClass[$classSectionId]['total']++;

            $studentDecisionRows = $decisionsByStudent[$sid] ?? [];
            $decisionState = $this->normalizeDecisionState($studentDecisionRows);

            if ($decisionState['eligible']) {
                $statsPerClass[$classSectionId]['pass']++;
            }

            $certificateNumber = $certsByStudent[$sid] ?? null;
            if ($certificateNumber !== null && $certificateNumber !== '') {
                $statsPerClass[$classSectionId]['cert']++;
            }

            $studentsByClass[$classSectionId][] = [
                'student_id' => $sid,
                'firstname' => (string) ($row['firstname'] ?? ''),
                'lastname' => (string) ($row['lastname'] ?? ''),
                'class_section_id' => $classSectionId,
                'class_section_name' => $classSectionName,
                'year_score' => $decisionState['year_score'],
                'eligible' => $decisionState['eligible'],
                'decision_state' => $decisionState['status'],
                'decision_labels' => $decisionState['display_decisions'],
                'decision_rows' => $studentDecisionRows,
                'certificate_number' => $certificateNumber,
            ];
        }

        $gradeGroups = $this->buildGradeGroups($statsPerClass, $studentsByClass);

        return [
            'school_year' => $selectedYear,
            'cert_date' => date('m/d/Y'),
            'grade_groups' => array_values($gradeGroups),
            'stats_per_class' => $statsPerClass,
            'default_group_key' => array_key_first($gradeGroups),
        ];
    }

    public function auditLog(?string $schoolYear = null): array
    {
        $selectedYear = trim((string) ($schoolYear ?? ''));

        $recordsQuery = DB::table('certificate_records as cr')
            ->select(
                'cr.*',
                'u.firstname as admin_firstname',
                'u.lastname as admin_lastname'
            )
            ->leftJoin('users as u', 'u.id', '=', 'cr.issued_by')
            ->orderByDesc('cr.issued_at');

        if ($selectedYear !== '') {
            $recordsQuery->where('cr.school_year', $selectedYear);
        }

        $records = $recordsQuery->get()->map(fn ($row) => (array) $row)->all();

        $yearSummary = DB::table('certificate_records')
            ->select('school_year', DB::raw('COUNT(*) as total'))
            ->groupBy('school_year')
            ->orderBy('school_year', 'DESC')
            ->get()
            ->map(fn ($row) => (array) $row)
            ->all();

        return [
            'school_year' => $selectedYear,
            'records' => $records,
            'year_summary' => $yearSummary,
        ];
    }

    /**
     * @param  list<int>  $studentIds
     * @return array{students: list<array<string, mixed>>, cert_date_display: string, school_year: string}
     */
    public function issueCertificates(
        array $studentIds,
        string $certDate,
        ?int $classSectionId,
        ?string $schoolYear,
        ?int $issuedBy
    ): array {
        $studentIds = array_values(array_filter(array_map('intval', $studentIds), static fn (int $id): bool => $id > 0));
        if ($studentIds === []) {
            throw new \InvalidArgumentException('Please select at least one student.');
        }

        $selectedYear = $this->resolveSchoolYear($schoolYear);
        $certDateDisplay = $this->normalizeDisplayCertDate($certDate);
        $certDateStorage = $this->parseCertDate($certDateDisplay);
        $students = $this->fetchStudentsForIssuance($studentIds, $classSectionId, $selectedYear);

        if ($students === []) {
            throw new \RuntimeException('No valid students found.');
        }

        $rosterMap = [];
        foreach ($students as $student) {
            $rosterMap[(int) $student['id']] = $student;
        }

        $existingByStudent = DB::table('certificate_records')
            ->whereIn('student_id', array_keys($rosterMap))
            ->where('school_year', $selectedYear)
            ->orderByDesc('issued_at')
            ->get()
            ->map(fn ($row) => (array) $row)
            ->groupBy('student_id')
            ->map(fn ($rows) => $rows[0])
            ->all();

        $baseCount = (int) DB::table('certificate_records')
            ->where('school_year', $selectedYear)
            ->count();
        $nextOffset = 0;

        DB::beginTransaction();

        try {
            foreach ($students as &$student) {
                $sid = (int) ($student['id'] ?? 0);
                $existing = $existingByStudent[$sid] ?? null;

                if (is_array($existing)) {
                    $student['cert_number'] = (string) ($existing['certificate_number'] ?? '');
                    $student['verify_token'] = $this->ensureVerificationToken($existing);
                } else {
                    $nextOffset++;
                    $certificateNumber = $this->formatCertificateNumber($selectedYear, $baseCount + $nextOffset);
                    $verificationToken = $this->hasVerificationTokenColumn ? $this->generateVerificationToken() : null;
                    $grade = $this->formatGrade((string) ($student['grade'] ?? ''));
                    $studentName = trim((string) ($student['firstname'] ?? '').' '.(string) ($student['lastname'] ?? ''));

                    $payload = [
                        'certificate_number' => $certificateNumber,
                        'student_id' => $sid,
                        'student_name' => $studentName,
                        'grade' => $grade,
                        'cert_date' => $certDateStorage,
                        'school_year' => $selectedYear,
                        'class_section_id' => $classSectionId,
                        'issued_by' => $issuedBy,
                        'issued_at' => now(),
                        'created_at' => now(),
                        'updated_at' => now(),
                    ];

                    if ($this->hasVerificationTokenColumn) {
                        $payload['verification_token'] = $verificationToken;
                    }

                    DB::table('certificate_records')->insert($payload);

                    $student['cert_number'] = $certificateNumber;
                    $student['verify_token'] = $verificationToken;
                }

                $student['grade'] = $this->formatGrade((string) ($student['grade'] ?? ''));
                $student['verify_url'] = ! empty($student['verify_token'])
                    ? url('/api/certificates/verify/'.rawurlencode((string) $student['verify_token']))
                    : null;
            }
            unset($student);

            DB::commit();
        } catch (\Throwable $e) {
            DB::rollBack();
            throw $e;
        }

        return [
            'students' => $students,
            'cert_date_display' => $certDateDisplay,
            'school_year' => $selectedYear,
        ];
    }

    /**
     * @return array{student: array<string, mixed>, cert_date_display: string}|null
     */
    public function reprintPayload(string $certificateNumber): ?array
    {
        $record = DB::table('certificate_records')
            ->where('certificate_number', strtoupper(trim($certificateNumber)))
            ->first();

        if (! $record) {
            return null;
        }

        $row = (array) $record;
        $studentName = trim((string) ($row['student_name'] ?? ''));
        [$firstname, $lastname] = $this->splitStudentName($studentName);
        $verifyToken = $this->ensureVerificationToken($row);

        return [
            'student' => [
                'id' => (int) ($row['student_id'] ?? 0),
                'firstname' => $firstname,
                'lastname' => $lastname,
                'grade' => (string) ($row['grade'] ?? ''),
                'cert_number' => (string) ($row['certificate_number'] ?? ''),
                'verify_token' => $verifyToken,
                'verify_url' => $verifyToken !== '' ? url('/api/certificates/verify/'.rawurlencode($verifyToken)) : null,
            ],
            'cert_date_display' => $this->displayDateFromStorage($row['cert_date'] ?? null),
        ];
    }

    /**
     * @return array<string, mixed>|null
     */
    public function verifyPayload(string $tokenOrNumber): ?array
    {
        $lookup = strtoupper(trim($tokenOrNumber));

        $query = DB::table('certificate_records as cr')
            ->select(
                'cr.certificate_number',
                'cr.student_name',
                'cr.grade',
                'cr.cert_date',
                'cr.school_year',
                'cr.issued_at',
                'u.firstname as admin_firstname',
                'u.lastname as admin_lastname'
            )
            ->leftJoin('users as u', 'u.id', '=', 'cr.issued_by');

        if ($this->hasVerificationTokenColumn) {
            $query->where(function ($builder) use ($tokenOrNumber, $lookup): void {
                $builder->where('cr.verification_token', trim($tokenOrNumber))
                    ->orWhere('cr.certificate_number', $lookup);
            });
        } else {
            $query->where('cr.certificate_number', $lookup);
        }

        $record = $query->first();
        if (! $record) {
            return null;
        }

        return (array) $record;
    }

    private function resolveSchoolYear(?string $schoolYear): string
    {
        $selectedYear = trim((string) ($schoolYear ?? ''));

        if ($selectedYear !== '') {
            return $selectedYear;
        }

        return trim((string) (Configuration::getConfig('school_year') ?? ''));
    }

    /**
     * @param  array<int, array{name: string, total: int, pass: int, cert: int}>  $statsPerClass
     * @param  array<int, list<array<string, mixed>>>  $studentsByClass
     * @return array<string, array<string, mixed>>
     */
    private function buildGradeGroups(array $statsPerClass, array $studentsByClass): array
    {
        $gradeGroups = [];

        foreach ($statsPerClass as $classSectionId => $stats) {
            [$label, $sortKey, $slug] = $this->classGroupMeta((string) ($stats['name'] ?? ''));

            if (! isset($gradeGroups[$sortKey])) {
                $gradeGroups[$sortKey] = [
                    'key' => $sortKey,
                    'label' => $label,
                    'slug' => $slug,
                    'total' => 0,
                    'pass' => 0,
                    'cert' => 0,
                    'sections' => [],
                ];
            }

            $total = (int) ($stats['total'] ?? 0);
            $pass = (int) ($stats['pass'] ?? 0);
            $cert = (int) ($stats['cert'] ?? 0);

            $gradeGroups[$sortKey]['total'] += $total;
            $gradeGroups[$sortKey]['pass'] += $pass;
            $gradeGroups[$sortKey]['cert'] += $cert;
            $gradeGroups[$sortKey]['sections'][] = [
                'section_id' => (int) $classSectionId,
                'section_name' => (string) ($stats['name'] ?? ''),
                'student_count' => $total,
                'pass_count' => $pass,
                'cert_count' => $cert,
                'remaining_count' => max(0, $pass - $cert),
                'students' => $studentsByClass[$classSectionId] ?? [],
            ];
        }

        ksort($gradeGroups);

        foreach ($gradeGroups as &$group) {
            $group['fully_done'] = $group['pass'] > 0 && $group['cert'] >= $group['pass'];
            $group['has_pass'] = $group['pass'] > 0;
        }
        unset($group);

        return $gradeGroups;
    }

    /**
     * @param  list<array<string, mixed>>  $studentDecisionRows
     * @return array{eligible: bool, status: string, display_decisions: list<string>, year_score: float|null}
     */
    private function normalizeDecisionState(array $studentDecisionRows): array
    {
        if ($studentDecisionRows === []) {
            return [
                'eligible' => false,
                'status' => 'pending',
                'display_decisions' => [],
                'year_score' => null,
            ];
        }

        $allDecisions = array_map(
            static fn (array $row): string => trim((string) ($row['decision'] ?? '')),
            $studentDecisionRows
        );

        $hasPending = in_array('', $allDecisions, true);
        $unique = array_values(array_unique(array_filter(
            $allDecisions,
            static fn (string $decision): bool => $decision !== ''
        )));

        $eligible = ! $hasPending && $unique === ['Pass'];
        $displayDecisions = $eligible
            ? ['Pass']
            : array_values(array_filter($unique, static fn (string $decision): bool => $decision !== 'Pass'));

        $yearScore = null;
        foreach ($studentDecisionRows as $row) {
            if (array_key_exists('year_score', $row) && $row['year_score'] !== null && $row['year_score'] !== '') {
                $yearScore = round((float) $row['year_score'], 2);
                break;
            }
        }

        return [
            'eligible' => $eligible,
            'status' => ($hasPending || $displayDecisions === []) ? 'pending' : ($eligible ? 'pass' : 'decision'),
            'display_decisions' => $displayDecisions,
            'year_score' => $yearScore,
        ];
    }

    /**
     * @param  list<int>  $studentIds
     * @return list<array<string, mixed>>
     */
    private function fetchStudentsForIssuance(array $studentIds, ?int $classSectionId, string $schoolYear): array
    {
        $dashboard = $this->dashboard($schoolYear);
        $eligibleMap = [];

        foreach ($dashboard['grade_groups'] as $group) {
            foreach (($group['sections'] ?? []) as $section) {
                foreach (($section['students'] ?? []) as $student) {
                    $sid = (int) ($student['student_id'] ?? 0);
                    if ($sid > 0) {
                        $eligibleMap[$sid] = $student;
                    }
                }
            }
        }

        $filtered = [];
        foreach ($studentIds as $studentId) {
            $student = $eligibleMap[$studentId] ?? null;
            if (! is_array($student) || empty($student['eligible'])) {
                continue;
            }

            if ($classSectionId !== null && (int) ($student['class_section_id'] ?? 0) !== $classSectionId) {
                continue;
            }

            $filtered[] = [
                'id' => $studentId,
                'firstname' => (string) ($student['firstname'] ?? ''),
                'lastname' => (string) ($student['lastname'] ?? ''),
                'grade' => (string) ($student['class_section_name'] ?? ''),
            ];
        }

        return $filtered;
    }

    private function formatGrade(string $raw): string
    {
        $clean = trim($raw);
        $lower = strtolower($clean);

        if (preg_match('/^grade\s*(\d+)/i', $clean, $matches)) {
            return 'Grade '.(int) $matches[1];
        }

        if ($lower === 'youth') {
            return 'Youth';
        }

        if ($lower === 'kg' || $lower === 'kindergarten') {
            return 'Kindergarten';
        }

        if (preg_match('/^(\d+)([- ][A-Za-z0-9]+)?$/', $clean, $matches)) {
            return 'Grade '.(int) $matches[1];
        }

        return $clean;
    }

    private function normalizeDisplayCertDate(string $certDate): string
    {
        $clean = trim($certDate);
        if ($clean === '') {
            return date('m/d/Y');
        }

        if (preg_match('#^\d{4}-\d{2}-\d{2}$#', $clean) === 1) {
            $timestamp = strtotime($clean);
            if ($timestamp !== false) {
                return date('m/d/Y', $timestamp);
            }
        }

        if (preg_match('#^\d{2}/\d{2}/\d{4}$#', $clean) === 1) {
            return $clean;
        }

        return date('m/d/Y');
    }

    private function parseCertDate(string $certDate): ?string
    {
        if (preg_match('#^(\d{2})/(\d{2})/(\d{4})$#', $certDate, $matches) === 1) {
            return $matches[3].'-'.$matches[1].'-'.$matches[2];
        }

        if (preg_match('#^\d{4}-\d{2}-\d{2}$#', $certDate) === 1) {
            return $certDate;
        }

        return null;
    }

    private function displayDateFromStorage(mixed $value): string
    {
        if ($value === null || trim((string) $value) === '') {
            return date('m/d/Y');
        }

        $timestamp = strtotime((string) $value);

        return $timestamp !== false ? date('m/d/Y', $timestamp) : date('m/d/Y');
    }

    private function formatCertificateNumber(string $schoolYear, int $sequence): string
    {
        return 'ARSS-'.$schoolYear.'-'.str_pad((string) $sequence, 4, '0', STR_PAD_LEFT);
    }

    private function generateVerificationToken(): string
    {
        if (! $this->hasVerificationTokenColumn) {
            return '';
        }

        do {
            $token = bin2hex(random_bytes(16));
            $exists = DB::table('certificate_records')
                ->where('verification_token', $token)
                ->exists();
        } while ($exists);

        return $token;
    }

    /**
     * @param  array<string, mixed>  $record
     */
    private function ensureVerificationToken(array $record): string
    {
        if (! $this->hasVerificationTokenColumn) {
            return '';
        }

        $token = trim((string) ($record['verification_token'] ?? ''));
        if ($token !== '') {
            return $token;
        }

        $recordId = (int) ($record['id'] ?? 0);
        if ($recordId <= 0) {
            return '';
        }

        $token = $this->generateVerificationToken();
        DB::table('certificate_records')
            ->where('id', $recordId)
            ->update([
                'verification_token' => $token,
                'updated_at' => now(),
            ]);

        return $token;
    }

    /**
     * @return array{0: string, 1: string, 2: string}
     */
    private function classGroupMeta(string $name): array
    {
        $lower = strtolower(trim($name));

        if (preg_match('/grade\s*(\d+)/i', $name, $matches) === 1) {
            $grade = (int) $matches[1];
            return [
                'Grade '.$grade,
                '1_'.str_pad((string) $grade, 3, '0', STR_PAD_LEFT),
                'grade'.$grade,
            ];
        }

        if (str_contains($lower, 'kg') || str_contains($lower, 'kindergarten')) {
            return ['KG', '0_kg', 'kg'];
        }

        if (str_contains($lower, 'arabic')) {
            return ['Arabic', '2_arabic', 'arabic'];
        }

        if (str_contains($lower, 'youth')) {
            return ['Youth', '5_youth', 'youth'];
        }

        return [
            $name,
            '3_'.$lower,
            (string) preg_replace('/[^a-z0-9]+/', '-', $lower),
        ];
    }

    /**
     * @return array{0: string, 1: string}
     */
    private function splitStudentName(string $studentName): array
    {
        $parts = preg_split('/\s+/', trim($studentName), 2) ?: [];

        return [
            $parts[0] ?? '',
            $parts[1] ?? '',
        ];
    }
}
