<?php

declare(strict_types=1);

namespace App\Services\Badges;

use chillerlan\QRCode\Output\QRGdImagePNG;
use chillerlan\QRCode\Output\QRMarkupSVG;
use chillerlan\QRCode\QRCode;
use chillerlan\QRCode\QROptions;
use Dompdf\Dompdf;
use Dompdf\Options;
use Illuminate\Http\Response;
use Throwable;

class BadgePdfService
{
    private const BADGE_WIDTH_IN = 2.36;

    private const BADGE_HEIGHT_IN = 3.54;

    private const CELL_WIDTH_IN = 2.55;

    private const CELL_HEIGHT_IN = 3.85;

    private const PAGE_MARGIN_IN = 0.18;

    /** @var list<string> */
    private array $tempQrFiles = [];

    public function __construct(
        protected BadgeStudentLookupService $studentLookupService,
        protected BadgeUserLookupService $userLookupService,
        protected BadgePrintLogService $printLogService,
        protected BadgeTextFormatter $formatter,
    ) {
    }

    /**
     * Letter landscape PDF, 8 badges per page — students and/or staff (FPDF + QR PNG).
     *
     * @param  int[]  $studentIds  Primary keys on `students.id`
     * @param  int[]  $userIds     Staff/admin/teacher keys on `users.id`
     */
    public function generate(
        array $studentIds,
        array $userIds,
        ?string $schoolYear,
        array $rolesMap = [],
        array $classesMap = [],
        ?int $actorId = null
    ): Response {
        $studentIds = $this->formatter->normalizeIds($studentIds);
        $userIds = $this->formatter->normalizeIds($userIds);

        $schoolName = (string) config('badges.school_name', 'Al Rahma Sunday School');
        $motto = (string) config('badges.motto', '');
        $studentRoleLabel = (string) config('badges.role_label', 'Student');
        $footerBlock = $this->footerBlock($schoolName);
        $logoPath = $this->preparedLogoPath();

        $rows = [];

        $students = $this->studentLookupService->fetchForBadges($studentIds, $schoolYear);
        foreach ($students as $row) {
            try {
                $qrPayload = (string) ($row['school_id'] ?? '');
                $rows[] = array_merge($row, [
                    '_badge_kind' => 'student',
                    'role_subline' => $studentRoleLabel,
                    'show_grade' => true,
                    'grade_label' => 'Grade',
                    '_qr_src' => $this->renderQrSvgDataUri($qrPayload),
                    '_qr_base64' => $this->renderQrPngBase64($qrPayload),
                ]);
            } catch (Throwable) {
                continue;
            }
        }

        foreach ($userIds as $uid) {
            $staffRow = $this->buildStaffBadgeRow((int) $uid, $schoolYear, $rolesMap, $classesMap);
            if ($staffRow === null) {
                continue;
            }

            try {
                $qrPayload = (string) ($staffRow['user_id_for_qr'] ?? $staffRow['user_id'] ?? '');
                if ($qrPayload === '' || !preg_match('/^[A-Za-z0-9_-]+$/', $qrPayload)) {
                    $qrPayload = (string) (int) ($staffRow['user_id'] ?? 0);
                }

                $rows[] = array_merge($staffRow, [
                    '_badge_kind' => 'staff',
                    '_qr_src' => $this->renderQrSvgDataUri($qrPayload),
                    '_qr_base64' => $this->renderQrPngBase64($qrPayload),
                ]);
            } catch (Throwable) {
                continue;
            }
        }

        try {
            // Dompdf is substantially heavier here and can fatally exhaust memory on larger badge runs.
            // Prefer the fixed-coordinate FPDF renderer first, and keep Dompdf only as a secondary fallback.
            $binary = $this->renderWithFpdf(
                $rows,
                $schoolName,
                $motto,
                $studentRoleLabel,
                $footerBlock,
                $logoPath
            );
        } catch (Throwable) {
            $binary = $this->renderWithDompdf(
                $rows,
                $schoolName,
                $motto,
                $footerBlock
            );
        }

        $logIds = array_values(array_unique(array_merge($studentIds, $userIds)));
        $this->printLogService->logSafely(
            userIds: $logIds,
            actorId: $actorId,
            schoolYear: $schoolYear,
            rolesMap: $rolesMap,
            classesMap: $classesMap,
            copies: 1
        );

        return response((string) $binary, 200, [
            'Content-Type' => 'application/pdf',
            'Content-Disposition' => 'inline; filename="Badges.pdf"',
        ]);
    }

    protected function renderWithDompdf(
        array $rows,
        string $schoolName,
        string $motto,
        string $footerBlock
    ): string {
        $html = $this->buildHtmlDocument($rows, $schoolName, $motto, $footerBlock);

        $options = new Options();
        $options->set('isHtml5ParserEnabled', true);
        $options->set('isRemoteEnabled', true);
        $options->set('defaultFont', 'DejaVu Sans');

        $dompdf = new Dompdf($options);
        $dompdf->loadHtml($html, 'UTF-8');
        $dompdf->setPaper('A4', 'landscape');
        $dompdf->render();

        return $dompdf->output();
    }

    protected function renderWithFpdf(
        array $rows,
        string $schoolName,
        string $motto,
        string $studentRoleLabel,
        string $footerBlock,
        ?string $logoPath
    ): string {
        $this->tempQrFiles = [];

        try {
            if (!class_exists('FPDF', false)) {
                require_once base_path('app/ThirdParty/fpdf/fpdf.php');
            }

            $pdf = new \FPDF('L', 'in', 'Letter');
            $pdf->SetAutoPageBreak(false);

            $pages = array_chunk($rows, 8);

            if ($pages === []) {
                $pdf->AddPage('L', 'Letter');
                $pdf->SetFont('Helvetica', '', 12);
                $pdf->SetTextColor(0, 0, 0);
                $pdf->SetXY(self::PAGE_MARGIN_IN, self::PAGE_MARGIN_IN);
                $pdf->MultiCell(
                    $pdf->GetPageWidth() - 2 * self::PAGE_MARGIN_IN,
                    0.3,
                    $this->formatter->toPdf(
                        'No valid badges: check user/student ids, roles, or school ids for QR.'
                    ),
                    0,
                    'L'
                );
            } else {
                foreach ($pages as $pageRows) {
                    $pdf->AddPage('L', 'Letter');
                    $slots = array_pad($pageRows, 8, null);

                    for ($gridRow = 0; $gridRow < 2; $gridRow++) {
                        for ($gridCol = 0; $gridCol < 4; $gridCol++) {
                            $idx = ($gridRow * 4) + $gridCol;
                            $row = $slots[$idx];
                            if (!is_array($row)) {
                                continue;
                            }

                            $qrBase64 = trim((string) ($row['_qr_base64'] ?? ''));
                            if ($qrBase64 === '') {
                                continue;
                            }

                            $tmpPath = $this->writeTempQrPng(base64_decode($qrBase64, true) ?: '');
                            if ($tmpPath === null) {
                                continue;
                            }

                            $row['_qr_tmp'] = $tmpPath;

                            $cellX = self::PAGE_MARGIN_IN + $gridCol * self::CELL_WIDTH_IN;
                            $cellY = self::PAGE_MARGIN_IN + $gridRow * self::CELL_HEIGHT_IN;
                            $badgeX = $cellX + (self::CELL_WIDTH_IN - self::BADGE_WIDTH_IN) / 2;
                            $badgeY = $cellY + (self::CELL_HEIGHT_IN - self::BADGE_HEIGHT_IN) / 2;

                            $this->drawBadge(
                                $pdf,
                                $row,
                                $badgeX,
                                $badgeY,
                                self::BADGE_WIDTH_IN,
                                self::BADGE_HEIGHT_IN,
                                $schoolName,
                                $motto,
                                $studentRoleLabel,
                                $footerBlock,
                                $logoPath
                            );
                        }
                    }
                }
            }

            return $pdf->Output('S');
        } finally {
            foreach ($this->tempQrFiles as $path) {
                if (is_string($path) && $path !== '' && is_file($path)) {
                    @unlink($path);
                }
            }
            $this->tempQrFiles = [];
        }
    }

    /**
     * @param  array<string, mixed>  $rolesMap
     * @param  array<string, mixed>  $classesMap
     * @return ?array<string, mixed>
     */
    protected function buildStaffBadgeRow(
        int $userId,
        ?string $schoolYear,
        array $rolesMap,
        array $classesMap
    ): ?array {
        $info = $this->userLookupService->getUserInfoById($userId, $schoolYear);
        if (!$info || !is_array($info)) {
            return null;
        }

        $resolvedId = (int) ($info['user_id'] ?? $userId);

        $postedRole = $rolesMap[$resolvedId] ?? null;
        $roleResolved = $postedRole !== null
            ? (string) $postedRole
            : $this->formatter->resolveRole($info);

        $roleResolved = $this->formatter->formatRole($roleResolved);
        $info['role_resolved'] = $roleResolved;

        if (!empty($classesMap[$resolvedId])) {
            $info['class_section_name'] = (string) $classesMap[$resolvedId];
        }

        $class = $this->formatter->norm($info['class_section_name'] ?? '');
        $class = $this->formatter->formatClass($class);

        $detectSrc = strtolower($this->formatter->norm(
            ($info['roles'] ?? '') !== '' ? (string) $info['roles'] : $roleResolved
        ));
        $isTeacherish = str_contains($detectSrc, 'teacher') || preg_match('/\bta\b/', $detectSrc);
        $isAdmin = str_contains($detectSrc, 'admin');

        if ($isTeacherish && $class !== '') {
            if (strtolower($class) === 'youth') {
                $display = 'Youth ' . $roleResolved;
            } elseif ($class === 'KG') {
                $display = 'KG ' . $roleResolved;
            } else {
                $display = 'Grade ' . $class . ' ' . $roleResolved;
            }
        } elseif ($roleResolved !== '') {
            $display = $roleResolved;
        } elseif ($class !== '') {
            $display = $class;
        } else {
            $display = 'STAFF';
        }

        $gradeCol = $class !== '' ? $class : '—';
        $year = $this->formatter->norm((string) ($info['school_year'] ?? ($schoolYear ?? '')));
        $schoolId = trim((string) ($info['school_id'] ?? ''));

        if ($schoolId === '' || !preg_match('/^[A-Za-z0-9_-]+$/', $schoolId)) {
            return null;
        }

        return [
            'user_id' => $resolvedId,
            'user_id_for_qr' => $schoolId,
            'fullname' => $info['name'] ?? 'STAFF',
            'grade' => $gradeCol,
            'academic_year' => $year !== '' ? $year : '—',
            'school_id' => $schoolId,
            'role_subline' => $display,
            'show_grade' => true,
            'grade_label' => 'Class / Grade',
        ];
    }

    protected function buildHtmlDocument(
        array $rows,
        string $schoolName,
        string $motto,
        string $footerBlock
    ): string {
        $logoDataUri = $this->logoDataUri();
        $badgesHtml = '';
        $pages = array_chunk($rows, 8);

        if ($pages === []) {
            $pages = [[]];
        }

        foreach ($pages as $pageRows) {
            $badgesHtml .= '<div class="page"><table class="grid">';

            for ($rowIndex = 0; $rowIndex < 2; $rowIndex++) {
                $badgesHtml .= '<tr>';

                for ($colIndex = 0; $colIndex < 4; $colIndex++) {
                    $slot = ($rowIndex * 4) + $colIndex;
                    $badgesHtml .= '<td>';

                    if (isset($pageRows[$slot]) && is_array($pageRows[$slot])) {
                        $badgesHtml .= $this->buildBadgeHtml($pageRows[$slot], $schoolName, $motto, $footerBlock, $logoDataUri);
                    }

                    $badgesHtml .= '</td>';
                }

                $badgesHtml .= '</tr>';
            }

            $badgesHtml .= '</table></div>';
        }

        return '<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Badges PDF</title><style>'
            . $this->badgeCss()
            . '</style></head><body>'
            . $badgesHtml
            . '</body></html>';
    }

    protected function buildBadgeHtml(
        array $row,
        string $schoolName,
        string $motto,
        string $footerBlock,
        ?string $logoDataUri
    ): string
    {
        $kind = (string) ($row['_badge_kind'] ?? 'student');
        $idLabel = $kind === 'staff' ? 'User ID' : 'School ID';
        $title = $this->escapeHtml((string) ($row['fullname'] ?? ''));
        $role = $this->escapeHtml((string) ($row['role_subline'] ?? 'Student'));
        $gradeLabel = $this->escapeHtml((string) ($row['grade_label'] ?? ($kind === 'staff' ? 'Class / Assignment' : 'Grade')));
        $grade = $this->escapeHtml((string) ($row['grade'] ?? '—'));
        $year = $this->escapeHtml((string) ($row['academic_year'] ?? '—'));
        $schoolId = $this->escapeHtml((string) ($row['school_id'] ?? '—'));
        $qrSrc = trim((string) ($row['_qr_src'] ?? ''));
        $footer = nl2br($this->escapeHtml($footerBlock));
        $barcodeValue = preg_replace('/[^A-Za-z0-9]/', '', (string) ($row['school_id'] ?? ''));
        $barcodeValue = $barcodeValue !== '' ? $barcodeValue : 'ID0000';
        $barcodeText = $this->escapeHtml(str_repeat($barcodeValue, 4));
        $showGrade = (bool) ($row['show_grade'] ?? ($kind === 'student'));
        $headerMainTitle = $this->headerMainTitle($schoolName);
        $schoolLevel = $this->headerSubtitle($schoolName);
        $headerMotto = trim($motto) !== ''
            ? trim($motto)
            : trim((string) config('badges.motto_fallback', 'Learn • Lead • Succeed'));

        $infoRows = '';
        if ($showGrade) {
            $infoRows .= '
                                <div class="info-row">
                                    <span class="icon-badge">G</span>
                                    <span class="info-copy">
                                        <span class="label">' . $gradeLabel . '</span>
                                        <span class="value">' . $grade . '</span>
                                    </span>
                                </div>';
        }

        $infoRows .= '
                                <div class="info-row">
                                    <span class="icon-badge">Y</span>
                                    <span class="info-copy">
                                        <span class="label">Academic Year</span>
                                        <span class="value">' . $year . '</span>
                                    </span>
                                </div>

                                <div class="info-row">
                                    <span class="icon-badge">ID</span>
                                    <span class="info-copy">
                                        <span class="label">' . $this->escapeHtml($idLabel) . '</span>
                                        <span class="value">' . $schoolId . '</span>
                                    </span>
                                </div>';

        return '
            <div class="badge">
                <div class="header-wrap">
                    <div class="header">
                        <div class="header-mark">'
                            . ($logoDataUri !== null
                                ? '<img src="' . $logoDataUri . '" alt="School Logo">'
                                : '<span>AS</span>') .
                        '</div>
                        <div class="header-copy">
                            <div class="school-name">' . $this->escapeHtml($headerMainTitle) . '</div>
                            ' . ($schoolLevel !== ''
                                ? '<div class="sub-school">' . $this->escapeHtml($schoolLevel) . '</div>'
                                : '') . '
                            ' . ($headerMotto !== ''
                                ? '<div class="motto">' . $this->escapeHtml($headerMotto) . '</div>'
                                : '') . '
                        </div>
                    </div>
                    <div class="header-accent"></div>
                    <div class="header-tail"></div>
                </div>

                <div class="body">
                    <div class="student-name">' . mb_strtoupper($title, 'UTF-8') . '</div>
                    <div class="name-divider">
                        <span class="divider-line"></span>
                        <span class="divider-dot"></span>
                        <span class="divider-line"></span>
                    </div>
                    <div class="role">' . mb_strtoupper($role, 'UTF-8') . '</div>

                    <table class="content">
                        <tr>
                            <td class="info">
                                ' . $infoRows . '
                            </td>

                            <td class="qr-col">
                                <div class="qr-box">'
                                    . ($qrSrc !== ''
                                        ? '<img src="' . $qrSrc . '" alt="QR Code">'
                                        : '') .
                                '</div>
                                <div class="scan-pill">Scan to verify</div>
                            </td>
                        </tr>
                    </table>

                    <div class="barcode-wrap">
                        <div class="barcode-bars">' . $this->buildBarcodeHtml($barcodeValue) . '</div>
                        <div class="barcode-text">' . $barcodeText . '</div>
                    </div>
                </div>

                <div class="footer">
                    <span class="footer-mark">'
                        . ($logoDataUri !== null
                            ? '<img src="' . $logoDataUri . '" alt="School Logo">'
                            : '<span>S</span>') .
                    '</span>
                    <span class="footer-copy">' . $footer . '</span>
                </div>
            </div>
        ';
    }

    protected function badgeCss(): string
    {
        return <<<'CSS'
@page {
    size: A4 landscape;
    margin: 0.18in;
}

* {
    box-sizing: border-box;
}

body {
    margin: 0;
    font-family: DejaVu Sans, Arial, Helvetica, sans-serif;
    color: #111111;
}

.page {
    width: 100%;
    page-break-after: always;
}

.page:last-child {
    page-break-after: auto;
}

.grid {
    width: 100%;
    border-collapse: collapse;
    table-layout: fixed;
}

.grid td {
    width: 25%;
    vertical-align: top;
    padding: 0.03in;
    height: 3.32in;
}

.badge {
    width: 2.18in;
    height: 3.26in;
    border: 1.2px solid #d8d8d8;
    border-radius: 0.18in;
    overflow: hidden;
    position: relative;
    margin: 0 auto;
    background: #ffffff;
    box-shadow: 0 0.03in 0.10in rgba(0, 0, 0, 0.16);
}

.header-wrap {
    position: relative;
    height: 0.96in;
}

.header {
    background: linear-gradient(135deg, #063979 0%, #082f68 55%, #05285a 100%);
    color: #ffffff;
    padding: 0.07in 0.08in 0.04in;
    height: 0.70in;
    border-radius: 0.18in 0.18in 0 0;
}

.header:after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 0.64in;
    height: 0.08in;
    background: #8db7e8;
    clip-path: polygon(0 0, 50% 100%, 100% 0, 100% 25%, 50% 100%, 0 25%);
}

.header-accent {
    position: absolute;
    left: 0;
    right: 0;
    top: 0.67in;
    height: 0.20in;
    background: #07336f;
    clip-path: polygon(0 0, 50% 100%, 100% 0);
}

.header-tail {
    display: none;
}

.header-mark {
    float: left;
    width: 0.39in;
    height: 0.39in;
    border: 2px solid #ffffff;
    border-radius: 0.02in;
    text-align: center;
    overflow: hidden;
    background: transparent;
    margin-top: 0.02in;
}

.header-mark img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.header-mark span {
    display: block;
    line-height: 0.38in;
    font-size: 11pt;
    font-weight: 800;
}

.header-copy {
    margin-left: 0.45in;
    text-align: center;
}

.school-name {
    font-size: 17.5pt;
    font-weight: 900;
    line-height: 0.90;
    margin: 0;
    text-transform: uppercase;
    letter-spacing: 1px;
    color: #ffffff;
}

.sub-school {
    font-size: 8.6pt;
    font-weight: 900;
    text-transform: uppercase;
    letter-spacing: 1.7px;
    margin-top: 0.03in;
    color: #ffffff;
}

.sub-school:before,
.sub-school:after {
    content: "";
    display: inline-block;
    width: 0.22in;
    border-top: 1px solid #ffffff;
    margin: 0 0.05in 0.025in;
}

.motto {
    font-size: 6.5pt;
    font-weight: 900;
    margin-top: 0.03in;
    letter-spacing: 0.8px;
    min-height: 0.08in;
    text-transform: uppercase;
    color: #ffd44a;
}

.motto:before,
.motto:after {
    content: "";
    display: inline-block;
    width: 0.20in;
    border-top: 1px solid #ffd44a;
    margin: 0 0.05in 0.02in;
}

.body {
    position: relative;
    height: 1.94in;
    padding: 0.09in 0.09in 0.39in;
}

.student-name {
    text-align: center;
    color: #052d6f;
    font-size: 14.0pt;
    font-weight: 900;
    line-height: 1.05;
    text-transform: uppercase;
    margin: 0.01in 0 0.03in;
    letter-spacing: 1px;
}

.name-divider {
    text-align: center;
    margin-bottom: 0.03in;
}

.divider-line {
    display: inline-block;
    width: 0.65in;
    border-top: 1px solid #5b92d1;
    vertical-align: middle;
}

.divider-dot {
    display: inline-block;
    width: 0.045in;
    height: 0.045in;
    margin: 0 0.045in;
    background: #064297;
    transform: rotate(45deg);
    vertical-align: middle;
}

.role {
    text-align: center;
    color: #063979;
    font-size: 7.4pt;
    font-weight: 900;
    text-transform: uppercase;
    letter-spacing: 1.5px;
    margin-bottom: 0.07in;
}

.content {
    width: 100%;
    border-collapse: collapse;
    table-layout: fixed;
    margin-top: 0.01in;
}

.content td {
    vertical-align: top;
}

.info {
    width: 52%;
    padding-right: 0.07in;
    border-right: 1px solid #cfcfcf;
}

.info-row {
    margin-bottom: 0.07in;
    padding-bottom: 0.04in;
    border-bottom: 1px solid #d7d7d7;
}

.info-row:last-child {
    border-bottom: 0;
    margin-bottom: 0;
    padding-bottom: 0;
}

.icon-badge {
    display: inline-block;
    width: 0.22in;
    height: 0.22in;
    line-height: 0.22in;
    margin-right: 0.05in;
    border-radius: 50%;
    background: #064297;
    color: #ffffff;
    text-align: center;
    font-size: 5.0pt;
    font-weight: 900;
    vertical-align: top;
}

.info-copy {
    display: inline-block;
    width: 0.70in;
}

.label {
    display: block;
    font-size: 4.7pt;
    font-weight: 900;
    color: #111111;
    margin-bottom: 0.01in;
    text-transform: uppercase;
}

.value {
    display: block;
    font-size: 8.2pt;
    font-weight: 900;
    color: #064297;
    word-wrap: break-word;
}

.qr-col {
    width: 48%;
    text-align: center;
    padding-left: 0.08in;
}

.qr-box {
    display: inline-block;
    border: 1.5px solid #064297;
    border-radius: 0.06in;
    padding: 0.04in;
    background: #ffffff;
    min-width: 0.78in;
    min-height: 0.78in;
}

.qr-box img {
    width: 0.70in;
    height: 0.70in;
    display: block;
}

.scan-pill {
    margin-top: 0.035in;
    display: inline-block;
    background: #064297;
    color: #ffffff;
    border-radius: 0.03in;
    font-size: 4.8pt;
    font-weight: 900;
    text-transform: uppercase;
    padding: 0.035in 0.07in 0.025in;
}

.barcode-wrap {
    position: absolute;
    left: 0.10in;
    right: 0.10in;
    bottom: 0.06in;
    text-align: center;
    border-top: 1px solid #3d7fc6;
    padding-top: 0.05in;
}

.barcode-bars {
    height: 0.14in;
    line-height: 0;
    margin: 0 auto;
    white-space: nowrap;
    overflow: hidden;
}

.barcode-bar {
    display: inline-block;
    height: 0.14in;
    margin-right: 1px;
    background: #111111;
}

.barcode-text {
    display: none;
}

.footer {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    background: linear-gradient(135deg, #064297 0%, #052b64 100%);
    color: #ffffff;
    padding: 0.055in 0.07in 0.05in;
    font-size: 5.2pt;
    font-weight: 700;
    line-height: 1.2;
}

.footer-mark {
    display: inline-block;
    width: 0.25in;
    height: 0.25in;
    line-height: 0.25in;
    margin-right: 0.07in;
    border: 1.5px solid #ffffff;
    border-radius: 50%;
    text-align: center;
    vertical-align: top;
    font-size: 7pt;
    overflow: hidden;
    background: transparent;
}

.footer-mark img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: 50%;
    display: block;
}

.footer-copy {
    display: inline-block;
    width: 1.65in;
    padding-left: 0.07in;
    border-left: 1px solid rgba(255,255,255,0.8);
    vertical-align: top;
}
CSS;
    }

    protected function buildBarcodeHtml(string $value): string
    {
        $seed = $value !== '' ? strtoupper($value) : 'ID0000';
        $bars = [];

        foreach (str_split($seed) as $char) {
            $width = match (ord($char) % 4) {
                0 => '1px',
                1 => '2px',
                2 => '3px',
                default => '4px',
            };

            $bars[] = '<span class="barcode-bar" style="width:' . $width . '"></span>';
            $bars[] = '<span class="barcode-bar" style="width:1px"></span>';
        }

        return implode('', $bars);
    }

    protected function escapeHtml(string $value): string
    {
        return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
    }

    protected function writeTempQrPng(string $png): ?string
    {
        if ($png === '') {
            return null;
        }

        $tmpPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'badge_qr_' . bin2hex(random_bytes(8)) . '.png';
        if (@file_put_contents($tmpPath, $png) === false) {
            return null;
        }

        $this->tempQrFiles[] = $tmpPath;

        return $tmpPath;
    }

    protected function drawBadge(
        \FPDF $pdf,
        array $student,
        float $x,
        float $y,
        float $w,
        float $h,
        string $schoolName,
        string $motto,
        string $defaultStudentRoleLabel,
        string $footerBlock,
        ?string $logoPath
    ): void {
        $templatePath = $this->badgeTemplatePath($student);

        if ($templatePath !== null) {
            $this->drawBadgeFromTemplate(
                $pdf,
                $student,
                $x,
                $y,
                $w,
                $h,
                $defaultStudentRoleLabel,
                $footerBlock,
                $templatePath
            );

            return;
        }

        // Safety fallback: if the template PNGs are not deployed yet, still render a usable badge.
        $this->drawBadgeGeneratedFallback(
            $pdf,
            $student,
            $x,
            $y,
            $w,
            $h,
            $schoolName,
            $motto,
            $defaultStudentRoleLabel,
            $footerBlock,
            $logoPath
        );
    }

    /**
     * Pick the static image template used as the badge background.
     *
     * Deploy these exact filenames to public/ or one of the supported public asset folders:
     * - student_template.png
     * - teacher_template.png
     * - admin_template.png
     */
    protected function badgeTemplatePath(array $row): ?string
    {
        $kind = strtolower((string) ($row['_badge_kind'] ?? 'student'));
        $role = strtolower(trim((string) ($row['role_subline'] ?? $row['role_resolved'] ?? $row['roles'] ?? '')));

        if ($kind === 'student') {
            return $this->firstExistingPublicTemplate('student_template.png');
        }

        if (str_contains($role, 'admin')) {
            return $this->firstExistingPublicTemplate('admin_template.png');
        }

        return $this->firstExistingPublicTemplate('teacher_template.png');
    }

    protected function firstExistingPublicTemplate(string $filename): ?string
    {
        return $this->formatter->firstExisting([
            public_path($filename),
            public_path('badges/' . $filename),
            public_path('assets/images/' . $filename),
            public_path('assets/images/badges/' . $filename),
            storage_path('app/public/' . $filename),
            storage_path('app/public/badges/' . $filename),
        ]);
    }

    protected function drawBadgeFromTemplate(
        \FPDF $pdf,
        array $row,
        float $x,
        float $y,
        float $w,
        float $h,
        string $defaultStudentRoleLabel,
        string $footerBlock,
        string $templatePath
    ): void {
        try {
            $pdf->Image($templatePath, $x, $y, $w, $h);
        } catch (Throwable) {
            return;
        }

        $kind = strtolower((string) ($row['_badge_kind'] ?? 'student'));
        $roleProbe = strtolower((string) ($row['role_subline'] ?? ''));
        $isAdmin = $kind !== 'student' && str_contains($roleProbe, 'admin');

        $name = strtoupper($this->formatter->toPdf((string) ($row['fullname'] ?? '')));
        $firstValue = $this->formatter->toPdf((string) ($row['grade'] ?? ''));
        $year = $this->formatter->toPdf((string) ($row['academic_year'] ?? ''));
        $id = $this->formatter->toPdf((string) ($row['school_id'] ?? ''));
        $qrPath = (string) ($row['_qr_tmp'] ?? '');
        $barcodeValue = preg_replace('/[^A-Za-z0-9]/', '', (string) ($row['school_id'] ?? ''));
        $barcodeValue = $barcodeValue !== '' ? $barcodeValue : 'ID0000';

        // Template coordinate map, in inches, relative to the badge's top-left corner.
        // The PNG owns the visual design. PHP only overlays dynamic data.
        $black = [18, 18, 18];
        $white = [255, 255, 255];

        // The new template leaves more white space above the name block, so start the overlay lower.
        $this->fitCenteredText($pdf, $name, $x + 0.15, $y + 1.24, $w - 0.30, 0.18, 15.0, 7.0, $black, 'B');

        // Values beside/under the static labels baked into the template.
        $valueX = $x + 0.48;
        $valueW = 0.78;
        $valueYOffset = 0.18;
        $firstValueYOffset = 0.01;
        $this->fitLeftText($pdf, $firstValue, $valueX, $y + 2.02 + $valueYOffset + $firstValueYOffset, $valueW, 0.08, 8.0, 5.0, $black, 'B');
        $this->fitLeftText($pdf, $year, $valueX, $y + 2.37 + $valueYOffset, $valueW, 0.08, 8.0, 5.0, $black, 'B');
        $this->fitLeftText($pdf, $id, $valueX, $y + 2.72 + $valueYOffset, $valueW, 0.08, 8.0, 4.8, $black, 'B');

        // QR overlay, inside the template frame.
        if ($qrPath !== '' && is_file($qrPath)) {
            try {
                $pdf->Image($qrPath, $x + 1.45, $y + 1.90, 0.66, 0.66, 'PNG');
            } catch (Throwable) {
            }
        }

        // Barcode overlay. Repeat seed so the barcode fills the long template box instead of becoming a sad tiny fence.
        $pdf->SetFillColor(17, 17, 17);
        $this->drawBarcodeBars($pdf, str_repeat($barcodeValue, 6), $x + 0.35, $y + 3.07, $w - 0.70, 0.13);

        // Footer dynamic text. The template already contains the logo and divider.
        $pdf->SetTextColor($white[0], $white[1], $white[2]);
        $pdf->SetFont('Helvetica', 'B', 4.2);
        $lines = preg_split('/\r\n|\r|\n/', $footerBlock) ?: [$footerBlock];
        $footerY = $y + $h - 0.25;
        foreach ($lines as $line) {
            $line = trim((string) $line);
            if ($line === '') {
                continue;
            }
            $pdf->SetXY($x + 0.57, $footerY);
            $pdf->Cell($w - 0.65, 0.05, $this->formatter->toPdf($line), 0, 1, 'L');
            $footerY += 0.055;
        }
    }

    /**
     * Shrink text until it fits in the available width. FPDF, bravely, does not do this for us.
     *
     * @param array{0:int,1:int,2:int} $rgb
     */
    protected function fitCenteredText(
        \FPDF $pdf,
        string $text,
        float $x,
        float $y,
        float $w,
        float $h,
        float $maxPt,
        float $minPt,
        array $rgb,
        string $style = ''
    ): float {
        $text = trim($text);
        if ($text === '') {
            return $y;
        }

        $size = $maxPt;
        $lines = $this->splitCenteredTextIntoTwoLines($text) ?? [$text];
        do {
            $pdf->SetFont('Helvetica', $style, $size);
            $maxLineWidth = 0.0;
            foreach ($lines as $line) {
                $maxLineWidth = max($maxLineWidth, $pdf->GetStringWidth($line));
            }
            if ($maxLineWidth <= $w || $size <= $minPt) {
                break;
            }
            $size -= 0.5;
        } while ($size >= $minPt);

        $pdf->SetFont('Helvetica', $style, $size);
        $pdf->SetTextColor($rgb[0], $rgb[1], $rgb[2]);
        $lineSpacing = $h * 1.18;

        foreach ($lines as $index => $line) {
            $pdf->SetXY($x, $y + ($index * $lineSpacing));
            $pdf->Cell($w, $h, $line, 0, 0, 'C');
        }

        return $y + ((count($lines) - 1) * $lineSpacing) + $h;
    }

    /**
     * @return array{0:string,1:string}|null
     */
    protected function splitCenteredTextIntoTwoLines(string $text): ?array
    {
        $words = array_values(array_filter(explode(' ', preg_replace('/\s+/', ' ', trim($text)) ?: ''), 'strlen'));
        if (count($words) < 2) {
            return null;
        }

        return [
            $words[0],
            implode(' ', array_slice($words, 1)),
        ];
    }

    /**
     * @param array{0:int,1:int,2:int} $rgb
     */
    protected function fitLeftText(
        \FPDF $pdf,
        string $text,
        float $x,
        float $y,
        float $w,
        float $h,
        float $maxPt,
        float $minPt,
        array $rgb,
        string $style = ''
    ): void {
        $text = trim($text);
        if ($text === '') {
            return;
        }

        $size = $maxPt;
        do {
            $pdf->SetFont('Helvetica', $style, $size);
            if ($pdf->GetStringWidth($text) <= $w || $size <= $minPt) {
                break;
            }
            $size -= 0.5;
        } while ($size >= $minPt);

        $pdf->SetTextColor($rgb[0], $rgb[1], $rgb[2]);
        $pdf->SetXY($x, $y);
        $pdf->Cell($w, $h, $text, 0, 0, 'L');
    }

    protected function drawBadgeGeneratedFallback(
        \FPDF $pdf,
        array $student,
        float $x,
        float $y,
        float $w,
        float $h,
        string $schoolName,
        string $motto,
        string $defaultStudentRoleLabel,
        string $footerBlock,
        ?string $logoPath
    ): void {
        $kind = (string) ($student['_badge_kind'] ?? 'student');
        $idLabel = $kind === 'staff' ? 'USER ID' : 'SCHOOL ID';
        $showGrade = (bool) ($student['show_grade'] ?? ($kind === 'student'));
        $gradeLabel = (string) ($student['grade_label'] ?? ($kind === 'staff' ? 'CLASS / ASSIGNMENT' : 'GRADE'));

        $green = [6, 66, 151];
        $greenDark = [5, 43, 100];
        $greenAccent = [255, 212, 74];
        $blueRole = [6, 57, 121];

        $qrPath = (string) ($student['_qr_tmp'] ?? '');
        $fullName = $this->formatter->toPdf((string) ($student['fullname'] ?? ''));
        $grade = $this->formatter->toPdf((string) ($student['grade'] ?? ''));
        $year = $this->formatter->toPdf((string) ($student['academic_year'] ?? ''));
        $schoolId = $this->formatter->toPdf((string) ($student['school_id'] ?? ''));
        $barcodeValue = preg_replace('/[^A-Za-z0-9]/', '', (string) ($student['school_id'] ?? ''));
        $barcodeValue = $barcodeValue !== '' ? $barcodeValue : 'ID0000';
        $barcodeText = $this->formatter->toPdf(str_repeat($barcodeValue, 4));

        $schoolNamePdf = $this->formatter->toPdf($this->headerMainTitle($schoolName));
        $headerSubtitlePdf = $this->formatter->toPdf($this->headerSubtitle($schoolName));
        $mottoValue = trim($motto) !== ''
            ? $motto
            : (string) config('badges.motto_fallback', 'Learn • Lead • Succeed');
        $mottoPdf = $this->formatter->toPdf($mottoValue);
        $rolePdf = $this->formatter->toPdf((string) ($student['role_subline'] ?? $defaultStudentRoleLabel));

        $pdf->SetDrawColor($green[0], $green[1], $green[2]);
        $pdf->SetLineWidth(0.01);
        $pdf->Rect($x, $y, $w, $h);

        $headerH = 0.66;
        $footerH = 0.36;

        $pdf->SetFillColor($greenDark[0], $greenDark[1], $greenDark[2]);
        $pdf->Rect($x, $y, $w, $headerH, 'F');
        $pdf->SetFillColor($green[0], $green[1], $green[2]);
        $pdf->Rect($x, $y + 0.04, $w, 0.20, 'F');
        $pdf->SetFillColor(141, 183, 232);
        $pdf->Rect($x, $y + $headerH - 0.03, $w, 0.03, 'F');

        $sealSize = 0.33;
        $sealX = $x + 0.09;
        $sealY = $y + 0.07;
        $pdf->SetFillColor(255, 255, 255);
        $pdf->Rect($sealX, $sealY, $sealSize, $sealSize, 'F');
        $pdf->SetDrawColor(255, 255, 255);
        $pdf->Rect($sealX, $sealY, $sealSize, $sealSize, 'D');
        if ($logoPath !== null && is_readable($logoPath)) {
            try {
                $pdf->Image($logoPath, $sealX + 0.02, $sealY + 0.02, $sealSize - 0.04, $sealSize - 0.04);
            } catch (Throwable) {
            }
        }

        $pdf->SetTextColor(255, 255, 255);
        $pdf->SetFont('Helvetica', 'B', 13.0);
        $pdf->SetXY($x + 0.45, $y + 0.10);
        $pdf->Cell($w - 0.55, 0.12, strtoupper($schoolNamePdf), 0, 1, 'C');

        $pdf->SetFont('Helvetica', 'B', 7.2);
        $pdf->SetXY($x + 0.45, $y + 0.27);
        $pdf->Cell($w - 0.55, 0.06, strtoupper($headerSubtitlePdf), 0, 1, 'C');

        if ($mottoPdf !== '') {
            $pdf->SetFont('Helvetica', 'B', 5.6);
            $pdf->SetTextColor($greenAccent[0], $greenAccent[1], $greenAccent[2]);
            $pdf->SetXY($x + 0.45, $y + 0.40);
            $pdf->Cell($w - 0.55, 0.06, strtoupper($mottoPdf), 0, 1, 'C');
        }

        $bodyTop = $y + $headerH + 0.02;
        $nameBottomY = $this->fitCenteredText(
            $pdf,
            strtoupper($fullName),
            $x + 0.07,
            $bodyTop,
            $w - 0.14,
            0.11,
            13.2,
            7.0,
            [18, 18, 18],
            'B'
        );

        $pdf->SetTextColor(18, 18, 18);
        $pdf->SetFont('Helvetica', 'B', 6);
        $roleTop = $nameBottomY + 0.01;
        $pdf->SetXY($x + 0.07, $roleTop);
        $pdf->Cell($w - 0.14, 0.07, strtoupper($rolePdf), 0, 1, 'C');

        $ruleY = $roleTop + 0.10;
        $pdf->SetDrawColor(210, 214, 210);
        $pdf->Line($x + 0.10, $ruleY, $x + 0.58, $ruleY);
        $pdf->Line($x + $w - 0.58, $ruleY, $x + $w - 0.10, $ruleY);

        $infoLeft = $x + 0.08;
        $infoW = $w * 0.45;
        $labelY = $ruleY + 0.05;
        $infoRows = [];
        if ($showGrade) {
            $infoRows[] = [$gradeLabel, $grade];
        }
        $infoRows[] = ['ACADEMIC YEAR', $year];
        $infoRows[] = [$idLabel, $schoolId];

        foreach ($infoRows as $index => [$label, $value]) {
            $iconY = $labelY + 0.03;
            $pdf->SetFillColor($green[0], $green[1], $green[2]);
            $pdf->SetDrawColor($green[0], $green[1], $green[2]);
            $pdf->Rect($infoLeft, $iconY - 0.07, 0.14, 0.14, 'F');
            $pdf->SetTextColor(255, 255, 255);
            $pdf->SetFont('Helvetica', 'B', 5);
            $iconLabel = $label === 'ACADEMIC YEAR' ? 'Y' : ($label === $idLabel ? 'ID' : 'G');
            $pdf->SetXY($infoLeft + 0.01, $iconY - 0.03);
            $pdf->Cell(0.12, 0.06, $iconLabel, 0, 0, 'C');

            $textLeft = $infoLeft + 0.16;
            $pdf->SetTextColor(51, 51, 51);
            $pdf->SetFont('Helvetica', 'B', 5);
            $pdf->SetXY($textLeft, $labelY);
            $pdf->Cell($infoW, 0.05, $label, 0, 1, 'L');

            $pdf->SetTextColor(17, 17, 17);
            $pdf->SetFont('Helvetica', 'B', 7.5);
            $pdf->SetX($textLeft);
            if ($index === array_key_last($infoRows)) {
                $pdf->MultiCell($infoW, 0.09, $value, 0, 'L');
                $labelY = $pdf->GetY() + 0.015;
            } else {
                $pdf->Cell($infoW, 0.08, $value, 0, 1, 'L');
                $labelY = $pdf->GetY() + 0.01;
            }
        }

        $dividerX = $x + ($w * 0.53);
        $pdf->SetDrawColor(214, 214, 214);
        $pdf->Line($dividerX, $ruleY + 0.02, $dividerX, $y + $h - $footerH - 0.30);

        $qrSize = 0.48;
        $qrX = $x + $w - 0.18 - $qrSize;
        $qrY = $ruleY + 0.07;

        if ($qrPath !== '' && is_file($qrPath)) {
            try {
                $pdf->SetDrawColor($green[0], $green[1], $green[2]);
                $pdf->Rect($qrX - 0.05, $qrY - 0.05, $qrSize + 0.10, $qrSize + 0.10, 'D');
                $pdf->Image($qrPath, $qrX, $qrY, $qrSize, $qrSize, 'PNG');
            } catch (Throwable) {
            }
        }

        $pillW = 0.56;
        $pillH = 0.10;
        $pillX = $qrX + ($qrSize / 2) - ($pillW / 2);
        $pillY = $qrY + $qrSize + 0.06;
        $pdf->SetFillColor($green[0], $green[1], $green[2]);
        $pdf->Rect($pillX, $pillY, $pillW, $pillH, 'F');
        $pdf->SetTextColor(255, 255, 255);
        $pdf->SetFont('Helvetica', 'B', 4.4);
        $pdf->SetXY($pillX, $pillY + 0.02);
        $pdf->Cell($pillW, 0.05, 'SCAN TO VERIFY', 0, 0, 'C');

        $barcodeY = $y + $h - $footerH - 0.34;
        $barsX = $x + 0.14;
        $barsW = $w - 0.28;
        $pdf->SetDrawColor(214, 221, 216);
        $pdf->Line($barsX, $barcodeY - 0.03, $barsX + $barsW, $barcodeY - 0.03);
        $this->drawBarcodeBars($pdf, $barcodeValue, $barsX, $barcodeY, $barsW, 0.14);
        $pdf->SetTextColor(17, 17, 17);
        $pdf->SetFont('Helvetica', 'B', 5.6);
        $pdf->SetXY($barsX, $barcodeY + 0.16);
        $pdf->Cell($barsW, 0.05, $barcodeText, 0, 0, 'C');

        $fy = $y + $h - $footerH;
        $pdf->SetFillColor($green[0], $green[1], $green[2]);
        $pdf->Rect($x, $fy, $w, $footerH, 'F');

        if ($logoPath !== null && is_readable($logoPath)) {
            try {
                $smallLogo = 0.16;
                $pdf->SetFillColor(255, 255, 255);
                $pdf->Rect($x + 0.09, $fy + 0.05, 0.18, 0.18, 'F');
                $pdf->Image($logoPath, $x + 0.10, $fy + 0.06, $smallLogo, $smallLogo);
            } catch (Throwable) {
            }
        }

        $pdf->SetTextColor(255, 255, 255);
        $pdf->SetFont('Helvetica', 'B', 4.3);
        $lines = preg_split('/\r\n|\r|\n/', $footerBlock) ?: [$footerBlock];
        $lineY = $fy + 0.08;
        $textX = $x + 0.30;
        $textW = $w - 0.36;
        foreach ($lines as $line) {
            $line = trim($line);
            if ($line === '') {
                continue;
            }
            $pdf->SetXY($textX, $lineY);
            $pdf->Cell($textW, 0.055, $this->formatter->toPdf($line), 0, 1, 'L');
            $lineY += 0.055;
        }
    }

    protected function drawBarcodeBars(
        \FPDF $pdf,
        string $value,
        float $x,
        float $y,
        float $maxWidth,
        float $height
    ): void {
        $seed = $value !== '' ? strtoupper($value) : 'ID0000';
        $gap = 0.01;
        $segments = [];
        $usedWidth = 0.0;

        foreach (str_split($seed) as $char) {
            $width = match (ord($char) % 4) {
                0 => 0.01,
                1 => 0.015,
                2 => 0.02,
                default => 0.025,
            };

            $barWidth = $width;
            $thinBarWidth = 0.01;
            $nextWidth = ($segments === [] ? 0.0 : $gap) + $barWidth + $gap + $thinBarWidth;

            if (($usedWidth + $nextWidth) > $maxWidth) {
                break;
            }

            if ($segments !== []) {
                $segments[] = ['gap', $gap];
                $usedWidth += $gap;
            }

            $segments[] = ['bar', $barWidth];
            $usedWidth += $barWidth;
            $segments[] = ['gap', $gap];
            $usedWidth += $gap;
            $segments[] = ['bar', $thinBarWidth];
            $usedWidth += $thinBarWidth;
        }

        $cursor = $x + max(($maxWidth - $usedWidth) / 2, 0.0);

        foreach ($segments as [$type, $segmentWidth]) {
            if ($type === 'gap') {
                $cursor += $segmentWidth;
                continue;
            }

            $pdf->SetFillColor(17, 17, 17);
            $pdf->Rect($cursor, $y, $segmentWidth, $height, 'F');
            $cursor += $segmentWidth;
        }
    }

    protected function renderQrPng(string $payload): string
    {
        $qrOptions = new QROptions([
            'version' => 5,
            'outputInterface' => QRGdImagePNG::class,
            'outputBase64' => false,
            'eccLevel' => QRCode::ECC_M,
            'scale' => 4,
        ]);

        $qr = new QRCode($qrOptions);

        return $qr->render($payload);
    }

    protected function renderQrPngBase64(string $payload): string
    {
        try {
            return base64_encode($this->renderQrPng($payload));
        } catch (Throwable) {
            return '';
        }
    }

    protected function renderQrSvgDataUri(string $payload): string
    {
        $qrOptions = new QROptions([
            'version' => 5,
            'outputInterface' => QRMarkupSVG::class,
            'outputBase64' => true,
            'eccLevel' => QRCode::ECC_M,
            'drawLightModules' => false,
            'svgAddXmlHeader' => false,
            'connectPaths' => true,
        ]);

        $qr = new QRCode($qrOptions);

        return $qr->render($payload);
    }

    protected function logoFilePath(): ?string
    {
        return $this->formatter->firstExisting([
            public_path('logo-circle.png'),
            public_path('logo.png'),
            public_path('assets/images/school_logo.png'),
            public_path('assets/images/logo.png'),
        ]);
    }

    protected function preparedLogoPath(): ?string
    {
        $path = $this->logoFilePath();
        if ($path === null || !is_readable($path)) {
            return null;
        }

        $png = $this->makeCircularPngBytesFromPath($path);
        if ($png === null) {
            return $path;
        }

        $tmpPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'badge_logo_' . bin2hex(random_bytes(8)) . '.png';
        if (@file_put_contents($tmpPath, $png) === false) {
            return $path;
        }

        $this->tempQrFiles[] = $tmpPath;

        return $tmpPath;
    }

    protected function logoDataUri(): ?string
    {
        $path = $this->logoFilePath();
        if ($path === null || !is_readable($path)) {
            return null;
        }

        $bytes = $this->makeCircularPngBytesFromPath($path);
        if ($bytes === null) {
            $bytes = @file_get_contents($path);
        }
        if ($bytes === false || $bytes === '') {
            return null;
        }

        return 'data:image/png;base64,' . base64_encode($bytes);
    }


    protected function headerMainTitle(string $schoolName): string
    {
        $name = trim($schoolName) !== '' ? trim($schoolName) : 'Al Rahma Sunday School';
        $normalized = preg_replace('/\s+/', ' ', $name) ?: $name;

        if (preg_match('/^(.*?)(\s+Sunday\s+School)$/i', $normalized, $matches)) {
            return trim($matches[1]);
        }

        return $normalized;
    }

    protected function headerSubtitle(string $schoolName): string
    {
        $configured = trim((string) config('badges.header_subtitle', ''));
        if ($configured !== '') {
            return $configured;
        }

        $name = trim($schoolName) !== '' ? trim($schoolName) : 'Al Rahma Sunday School';
        if (preg_match('/Sunday\s+School/i', $name)) {
            return 'Sunday School';
        }

        return 'High School';
    }

    protected function footerBlock(string $schoolName): string
    {
        $address = trim((string) config('badges.footer_address', ''));
        if ($address !== '') {
            return $schoolName . "\n" . $address;
        }

        return $schoolName;
    }

    protected function makeCircularPngBytesFromPath(string $path): ?string
    {
        if (!function_exists('imagecreatetruecolor') || !function_exists('imagepng')) {
            return null;
        }

        $bytes = @file_get_contents($path);
        if ($bytes === false || $bytes === '') {
            return null;
        }

        $source = @imagecreatefromstring($bytes);
        if ($source === false) {
            return null;
        }

        try {
            $srcW = imagesx($source);
            $srcH = imagesy($source);
            if ($srcW <= 0 || $srcH <= 0) {
                return null;
            }

            $size = min($srcW, $srcH);
            $srcX = (int) floor(($srcW - $size) / 2);
            $srcY = (int) floor(($srcH - $size) / 2);

            $square = imagecreatetruecolor($size, $size);
            if ($square === false) {
                return null;
            }

            imagealphablending($square, false);
            imagesavealpha($square, true);
            $transparent = imagecolorallocatealpha($square, 0, 0, 0, 127);
            imagefill($square, 0, 0, $transparent);
            imagecopyresampled($square, $source, 0, 0, $srcX, $srcY, $size, $size, $size, $size);

            $circle = imagecreatetruecolor($size, $size);
            if ($circle === false) {
                imagedestroy($square);
                return null;
            }

            imagealphablending($circle, false);
            imagesavealpha($circle, true);
            $circleTransparent = imagecolorallocatealpha($circle, 0, 0, 0, 127);
            imagefill($circle, 0, 0, $circleTransparent);

            $radius = $size / 2;
            for ($x = 0; $x < $size; $x++) {
                for ($y = 0; $y < $size; $y++) {
                    $dx = ($x + 0.5) - $radius;
                    $dy = ($y + 0.5) - $radius;
                    if ((($dx * $dx) + ($dy * $dy)) <= ($radius * $radius)) {
                        $color = imagecolorat($square, $x, $y);
                        imagesetpixel($circle, $x, $y, $color);
                    }
                }
            }

            ob_start();
            imagepng($circle);
            $png = ob_get_clean();

            imagedestroy($square);
            imagedestroy($circle);

            return is_string($png) && $png !== '' ? $png : null;
        } finally {
            imagedestroy($source);
        }
    }
}
