<?php

declare(strict_types=1);

namespace App\Services\NovaCards;

use App\Models\NovaCard;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Storage;
use RuntimeException;

class NovaCardRenderService
{
    public function render(NovaCard $card): array
    {
        if (! function_exists('imagecreatetruecolor')) {
            throw new RuntimeException('Nova card rendering requires the GD extension.');
        }

        $format = Arr::get(config('nova_cards.formats'), $card->format, config('nova_cards.formats.square'));
        $width = (int) Arr::get($format, 'width', 1080);
        $height = (int) Arr::get($format, 'height', 1080);
        $project = is_array($card->project_json) ? $card->project_json : [];

        $image = imagecreatetruecolor($width, $height);
        imagealphablending($image, true);
        imagesavealpha($image, true);

        $this->paintBackground($image, $card, $width, $height, $project);
        $this->paintOverlay($image, $project, $width, $height);
        $this->paintText($image, $card, $project, $width, $height);
        $this->paintDecorations($image, $project, $width, $height);
        $this->paintAssets($image, $project, $width, $height);

        $disk = Storage::disk((string) config('nova_cards.storage.public_disk', 'public'));
        $basePath = trim((string) config('nova_cards.storage.preview_prefix', 'cards/previews'), '/') . '/' . $card->user_id;
        $previewPath = $basePath . '/' . $card->uuid . '.webp';
        $ogPath = $basePath . '/' . $card->uuid . '-og.jpg';

        ob_start();
        imagewebp($image, null, (int) config('nova_cards.render.preview_quality', 86));
        $webpBinary = (string) ob_get_clean();

        ob_start();
        imagejpeg($image, null, (int) config('nova_cards.render.og_quality', 88));
        $jpgBinary = (string) ob_get_clean();

        imagedestroy($image);

        // Store publicly so CDN / direct S3 URLs are accessible without signing.
        $disk->put($previewPath, $webpBinary, 'public');
        $disk->put($ogPath, $jpgBinary, 'public');

        $card->forceFill([
            'preview_path'    => $previewPath,
            'preview_width'   => $width,
            'preview_height'  => $height,
            'last_rendered_at' => now(),
        ])->save();

        return [
            'preview_path' => $previewPath,
            'og_path'      => $ogPath,
            'width'        => $width,
            'height'       => $height,
        ];
    }

    // ─── Background ──────────────────────────────────────────────────────────

    private function paintBackground($image, NovaCard $card, int $width, int $height, array $project): void
    {
        $background = Arr::get($project, 'background', []);
        $type = (string) Arr::get($background, 'type', $card->background_type ?: 'gradient');

        if ($type === 'solid') {
            $color = $this->allocateHex($image, (string) Arr::get($background, 'solid_color', '#111827'));
            imagefilledrectangle($image, 0, 0, $width, $height, $color);

            return;
        }

        if ($type === 'upload' && $card->backgroundImage?->processed_path) {
            $focalPosition = (string) Arr::get($project, 'background.focal_position', 'center');
            $blurLevel = (int) Arr::get($project, 'background.blur_level', 0);
            $this->paintImageBackground($image, $card->backgroundImage->processed_path, $width, $height, $focalPosition, $blurLevel);
        } else {
            $colors = Arr::wrap(Arr::get($background, 'gradient_colors', ['#0f172a', '#1d4ed8']));
            $from = (string) Arr::get($colors, 0, '#0f172a');
            $to = (string) Arr::get($colors, 1, '#1d4ed8');
            $this->paintVerticalGradient($image, $width, $height, $from, $to);
        }
    }

    private function paintImageBackground($image, string $path, int $width, int $height, string $focalPosition = 'center', int $blurLevel = 0): void
    {
        $disk = Storage::disk((string) config('nova_cards.storage.public_disk', 'public'));
        if (! $disk->exists($path)) {
            $this->paintVerticalGradient($image, $width, $height, '#0f172a', '#1d4ed8');

            return;
        }

        $blob = $disk->get($path);
        if (! $blob) {
            $this->paintVerticalGradient($image, $width, $height, '#0f172a', '#1d4ed8');

            return;
        }

        $background = @imagecreatefromstring($blob);
        if ($background === false) {
            $this->paintVerticalGradient($image, $width, $height, '#0f172a', '#1d4ed8');

            return;
        }

        $srcW = imagesx($background);
        $srcH = imagesy($background);

        // Implement CSS background-size: cover / object-fit: cover:
        // scale the source so both dimensions FILL the destination, then crop to focal point.
        $scaleX  = $width / max(1, $srcW);
        $scaleY  = $height / max(1, $srcH);
        $scale   = max($scaleX, $scaleY);

        // How many source pixels map to the entire destination.
        $sampleW = max(1, (int) round($width / $scale));
        $sampleH = max(1, (int) round($height / $scale));

        // Focal ratios (0 = left/top, 0.5 = centre, 1 = right/bottom).
        [$focalX, $focalY] = $this->resolveFocalRatios($focalPosition);
        $srcX = max(0, min($srcW - $sampleW, (int) round(($srcW - $sampleW) * $focalX)));
        $srcY = max(0, min($srcH - $sampleH, (int) round(($srcH - $sampleH) * $focalY)));

        imagecopyresampled($image, $background, 0, 0, $srcX, $srcY, $width, $height, $sampleW, $sampleH);

        for ($index = 0; $index < (int) floor($blurLevel / 4); $index++) {
            imagefilter($image, IMG_FILTER_GAUSSIAN_BLUR);
        }

        imagedestroy($background);
    }

    private function paintOverlay($image, array $project, int $width, int $height): void
    {
        $style = (string) Arr::get($project, 'background.overlay_style', 'dark-soft');
        if ($style === 'none') {
            return;
        }

        // Respect the opacity slider (0–100 %) that the CSS preview applies to the overlay div.
        $opacityPct = max(0, min(100, (int) Arr::get($project, 'background.opacity', 50)));
        $scale      = $opacityPct / 100.0;

        // Top/bottom gradient stop opacities — matches overlayStyle() in NovaCardCanvasPreview.jsx:
        //   dark-soft:   linear-gradient(180deg, rgba(2,6,23,0.18), rgba(2,6,23,0.48))
        //   dark-strong: linear-gradient(180deg, rgba(2,6,23,0.38), rgba(2,6,23,0.68))
        //   light-soft:  linear-gradient(180deg, rgba(255,255,255,0.08), rgba(255,255,255,0.22))
        [$topA, $botA] = match ($style) {
            'dark-strong' => [0.38, 0.68],
            'light-soft'  => [0.08, 0.22],
            default       => [0.18, 0.48], // dark-soft
        };
        $rgb = $style === 'light-soft' ? [255, 255, 255] : [0, 0, 0];

        // Draw a scanline gradient to match the CSS linear-gradient overlay.
        for ($y = 0; $y < $height; $y++) {
            $alpha   = ($topA + ($botA - $topA) * ($y / $height)) * $scale;
            $gdAlpha = max(0, min(127, (int) round((1.0 - $alpha) * 127)));
            $color   = imagecolorallocatealpha($image, $rgb[0], $rgb[1], $rgb[2], $gdAlpha);
            imageline($image, 0, $y, $width - 1, $y, $color);
        }
    }

    // ─── Text rendering (FreeType / GD fallback) ─────────────────────────────

    private function paintText($image, NovaCard $card, array $project, int $width, int $height): void
    {
        $fontPreset  = (string) Arr::get($project, 'typography.font_preset', 'modern-sans');
        $fontFile    = $this->resolveFont($fontPreset);
        // Apply text_opacity (10–100 %) to both text and accent colours, matching CSS blockStyle().
        $textOpacityPct = max(10, min(100, (int) Arr::get($project, 'typography.text_opacity', 100)));
        $textAlpha      = (int) round((1.0 - $textOpacityPct / 100.0) * 127);
        [$tr, $tg, $tb] = $this->hexToRgb((string) Arr::get($project, 'typography.text_color', '#ffffff'));
        [$ar, $ag, $ab] = $this->hexToRgb((string) Arr::get($project, 'typography.accent_color', Arr::get($project, 'typography.text_color', '#ffffff')));
        $textColor   = imagecolorallocatealpha($image, $tr, $tg, $tb, $textAlpha);
        $accentColor = imagecolorallocatealpha($image, $ar, $ag, $ab, $textAlpha);
        $alignment   = (string) Arr::get($project, 'layout.alignment', 'center');
        $lhMulti     = (float) Arr::get($project, 'typography.line_height', 1.2);
        $shadow      = (string) Arr::get($project, 'typography.shadow_preset', 'soft');

        $paddingRatio = match ((string) Arr::get($project, 'layout.padding', 'comfortable')) {
            'tight' => 0.08,
            'airy'  => 0.15,
            default => 0.11,
        };
        $xPadding = (int) round($width * $paddingRatio);

        // Respect the quote_width typography slider first; fall back to the layout max_width preset.
        $quoteWidthPct = (float) Arr::get($project, 'typography.quote_width', 0);
        $maxLineWidth  = ($quoteWidthPct >= 30 && $quoteWidthPct <= 100)
            ? (int) round($width * $quoteWidthPct / 100)
            : match ((string) Arr::get($project, 'layout.max_width', 'balanced')) {
                'compact' => (int) round($width * 0.52),
                'wide'    => (int) round($width * 0.80),
                default   => (int) round($width * 0.66),
            };

        // Font sizes come from the editor sliders (stored in project_json.typography).
        // Slider values are normalised to a 1080-px-wide canvas; scale up for wider formats
        // (e.g. landscape 1920 px) so proportions match the CSS preview at any container size.
        $fontScale  = $width / 1080.0;
        $quoteSize  = (float) max(10, (float) Arr::get($project, 'typography.quote_size', 72)) * $fontScale;
        $authorSize = (float) max(14, (float) Arr::get($project, 'typography.author_size', 28)) * $fontScale;

        $sizeMap = [
            'title'   => max(16, $quoteSize * 0.48),
            'quote'   => $quoteSize,
            'author'  => $authorSize,
            'source'  => max(12, $authorSize * 0.82),
            'body'    => max(16, $quoteSize * 0.54),
            'caption' => max(12, $authorSize * 0.74),
        ];

        $allBlocks  = $this->resolveTextBlocks($card, $project);
        // Blocks with both pos_x and pos_y set were dragged to a free position; others flow normally.
        $flowBlocks = array_values(array_filter($allBlocks, fn ($b) => Arr::get($b, 'pos_x') === null || Arr::get($b, 'pos_y') === null));
        $freeBlocks = array_values(array_filter($allBlocks, fn ($b) => Arr::get($b, 'pos_x') !== null && Arr::get($b, 'pos_y') !== null));

        // ── Flow blocks: vertically stacked, centred by the layout position ───
        $blockData   = [];
        $totalHeight = 0;
        foreach ($flowBlocks as $block) {
            $type   = (string) ($block['type'] ?? 'quote');
            $size   = (float) ($sizeMap[$type] ?? $quoteSize);
            $prefix = $type === 'author' ? '— ' : '';
            $raw    = $type === 'title' ? strtoupper($prefix . (string) ($block['text'] ?? '')) : $prefix . (string) ($block['text'] ?? '');
            $lines  = $this->wrapLines($raw, $size, $fontFile, $maxLineWidth);
            $lineH  = $this->lineHeight($size, $fontFile, $lhMulti);
            $gap    = (int) round($size * 0.55);
            $totalHeight += count($lines) * $lineH + $gap;
            $blockData[] = compact('type', 'size', 'lines', 'lineH', 'gap');
        }

        $position = (string) Arr::get($project, 'layout.position', 'center');
        $startY = match ($position) {
            'top'          => (int) round($height * 0.13),
            'upper-middle' => (int) round($height * 0.27),
            'lower-middle' => (int) round($height * 0.55),
            'bottom'       => max($xPadding, $height - $totalHeight - (int) round($height * 0.10)),
            default        => (int) round(($height - $totalHeight) / 2),
        };

        foreach ($blockData as $bdata) {
            $color = in_array($bdata['type'], ['author', 'source', 'title'], true) ? $accentColor : $textColor;
            foreach ($bdata['lines'] as $line) {
                $lw = $this->measureLine($line, $bdata['size'], $fontFile);
                $x  = $this->resolveAlignedX($alignment, $width, $xPadding, $lw);
                $this->drawLine($image, $bdata['size'], $x, $startY, $line, $color, $fontFile, $shadow);
                $startY += $bdata['lineH'];
            }
            $startY += $bdata['gap'];
        }

        // ── Free-positioned blocks: absolute x/y percentages from canvas origin ─
        foreach ($freeBlocks as $block) {
            $type      = (string) ($block['type'] ?? 'quote');
            $size      = (float) ($sizeMap[$type] ?? $quoteSize);
            $prefix    = $type === 'author' ? '— ' : '';
            $raw       = $type === 'title' ? strtoupper($prefix . (string) ($block['text'] ?? '')) : $prefix . (string) ($block['text'] ?? '');
            $blockMaxW = Arr::get($block, 'pos_width') !== null
                ? max(20, (int) round((float) $block['pos_width'] / 100 * $width))
                : $maxLineWidth;
            $lines     = $this->wrapLines($raw, $size, $fontFile, $blockMaxW);
            $lineH     = $this->lineHeight($size, $fontFile, $lhMulti);
            $x         = (int) round((float) Arr::get($block, 'pos_x', 0) / 100 * $width);
            $y         = (int) round((float) Arr::get($block, 'pos_y', 0) / 100 * $height);
            $color     = in_array($type, ['author', 'source', 'title'], true) ? $accentColor : $textColor;

            foreach ($lines as $line) {
                $this->drawLine($image, $size, $x, $y, $line, $color, $fontFile, $shadow);
                $y += $lineH;
            }
        }
    }

    /** Resolve the TTF font file path for a given preset key. */
    private function resolveFont(string $preset): string
    {
        $dir     = rtrim((string) config('nova_cards.render.fonts_dir', storage_path('app/fonts')), '/\\');
        $custom  = $dir . DIRECTORY_SEPARATOR . $preset . '.ttf';
        if (file_exists($custom) && function_exists('imagettftext')) {
            return $custom;
        }

        $default = $dir . DIRECTORY_SEPARATOR . 'default.ttf';
        if (file_exists($default) && function_exists('imagettftext')) {
            return $default;
        }

        return ''; // falls back to GD built-in fonts
    }

    /**
     * Resolve the best TTF font for Unicode decoration glyphs (✦ ♥ ☾ …).
     * Looks for symbols.ttf first (place a NotoSansSymbols or similar file there),
     * then falls back to the preset font or default.ttf.
     */
    private function resolveSymbolFont(string $preset): string
    {
        $dir = rtrim((string) config('nova_cards.render.fonts_dir', storage_path('app/fonts')), '/\\');
        foreach (['symbols.ttf', $preset . '.ttf', 'default.ttf'] as $candidate) {
            $path = $dir . DIRECTORY_SEPARATOR . $candidate;
            if (file_exists($path) && function_exists('imagettftext')) {
                return $path;
            }
        }

        return '';
    }

    /** Wrap text into lines that fit $maxWidth pixels. */
    private function wrapLines(string $text, float $size, string $fontFile, int $maxWidth): array
    {
        if ($text === '') {
            return [];
        }

        // Split on explicit newlines first, then wrap each segment.
        $paragraphs = preg_split('/\r\n|\r|\n/', $text) ?: [$text];
        $lines = [];
        foreach ($paragraphs as $para) {
            $words   = preg_split('/\s+/u', trim($para)) ?: [trim($para)];
            $current = '';
            foreach ($words as $word) {
                $candidate = $current === '' ? $word : $current . ' ' . $word;
                if ($this->measureLine($candidate, $size, $fontFile) <= $maxWidth) {
                    $current = $candidate;
                } else {
                    if ($current !== '') {
                        $lines[] = $current;
                    }
                    $current = $word;
                }
            }
            if ($current !== '') {
                $lines[] = $current;
            }
        }

        return $lines ?: [$text];
    }

    /** Measure the pixel width of a single line of text. */
    private function measureLine(string $text, float $size, string $fontFile): int
    {
        if ($fontFile !== '' && function_exists('imagettfbbox')) {
            $bbox = @imagettfbbox($size, 0, $fontFile, $text);
            if ($bbox !== false) {
                return abs($bbox[4] - $bbox[0]);
            }
        }

        // GD built-in fallback: font 5 is ~9px wide per character.
        return strlen($text) * imagefontwidth(5);
    }

    /** Calculate the line height (pixels) for a given font size. */
    private function lineHeight(float $size, string $fontFile, float $multiplier): int
    {
        if ($fontFile !== '' && function_exists('imagettfbbox')) {
            $bbox = @imagettfbbox($size, 0, $fontFile, 'Ágjy');
            if ($bbox !== false) {
                return (int) round(abs($bbox[1] - $bbox[7]) * $multiplier);
            }
        }

        return (int) round($size * $multiplier);
    }

    /** Draw a single text line with optional shadow. Baseline is $y. */
    private function drawLine($image, float $size, int $x, int $y, string $text, int $color, string $fontFile, string $shadowPreset): void
    {
        if ($fontFile !== '' && function_exists('imagettftext')) {
            // FreeType: $y is the baseline.
            $baseline = $y + (int) round($size);
            if ($shadowPreset !== 'none') {
                $offset      = $shadowPreset === 'strong' ? 4 : 2;
                $shadowAlpha = $shadowPreset === 'strong' ? 46 : 80;
                $shadowColor = imagecolorallocatealpha($image, 0, 0, 0, $shadowAlpha);
                imagettftext($image, $size, 0, $x + $offset, $baseline + $offset, $shadowColor, $fontFile, $text);
            }
            imagettftext($image, $size, 0, $x, $baseline, $color, $fontFile, $text);

            return;
        }

        // GD built-in fallback (bitmap fonts, $y is the top of the glyph).
        if ($shadowPreset !== 'none') {
            $offset      = $shadowPreset === 'strong' ? 3 : 1;
            $shadowColor = imagecolorallocatealpha($image, 0, 0, 0, $shadowPreset === 'strong' ? 46 : 80);
            imagestring($image, 5, $x + $offset, $y + $offset, $text, $shadowColor);
        }
        imagestring($image, 5, $x, $y, $text, $color);
    }

    // ─── Decorations & assets ────────────────────────────────────────────────

    private function paintDecorations($image, array $project, int $width, int $height): void
    {
        $decorations = Arr::wrap(Arr::get($project, 'decorations', []));
        $fontScale   = $width / 1080.0;
        $symbolFont  = $this->resolveSymbolFont((string) Arr::get($project, 'typography.font_preset', 'modern-sans'));
        [$accentR, $accentG, $accentB] = $this->hexToRgb((string) Arr::get($project, 'typography.accent_color', '#ffffff'));

        foreach (array_slice($decorations, 0, (int) config('nova_cards.validation.max_decorations', 6)) as $index => $decoration) {
            $glyph = (string) Arr::get($decoration, 'glyph', '•');

            // pos_x / pos_y are stored as percentages (0–100); when absent, fall back to
            // `placement` field — mirroring placementStyles in NovaCardCanvasPreview.jsx.
            $xPct = Arr::get($decoration, 'pos_x');
            $yPct = Arr::get($decoration, 'pos_y');
            if ($xPct !== null && $yPct !== null) {
                $x = (int) round((float) $xPct / 100 * $width);
                $y = (int) round((float) $yPct / 100 * $height);
            } else {
                $placement = (string) Arr::get($decoration, 'placement', 'top-right');
                $x = str_contains($placement, 'left')   ? (int) round(0.12 * $width)
                   : (str_contains($placement, 'right') ? (int) round(0.88 * $width)
                   : (int) round(0.50 * $width));
                $y = str_contains($placement, 'top')      ? (int) round(0.12 * $height)
                   : (str_contains($placement, 'bottom')  ? (int) round(0.88 * $height)
                   : (int) round(0.50 * $height));
            }

            // Canvas clamp: max(18, min(size, 64)) matching NovaCardCanvasPreview.
            $rawSize = max(18, min((int) Arr::get($decoration, 'size', 28), 64));
            $size    = (float) ($rawSize * $fontScale);

            // Opacity: 10–100 integer percent → GD alpha 0 (opaque)–127 (transparent).
            $opacityPct = max(10, min(100, (int) Arr::get($decoration, 'opacity', 85)));
            $alpha      = (int) round((1 - $opacityPct / 100) * 127);
            $color      = imagecolorallocatealpha($image, $accentR, $accentG, $accentB, $alpha);

            if ($symbolFont !== '' && function_exists('imagettftext')) {
                // Render the Unicode glyph; baseline = y + size (same as drawLine).
                imagettftext($image, $size, 0, $x, (int) ($y + $size), $color, $symbolFont, $glyph);
            } else {
                // No TTF font available — draw a small filled ellipse as a generic marker.
                $d = max(4, (int) round($size * 0.6));
                imagefilledellipse($image, $x, (int) ($y + $size / 2), $d, $d, $color);
            }
        }
    }

    private function paintAssets($image, array $project, int $width, int $height): void
    {
        $items = Arr::wrap(Arr::get($project, 'assets.items', []));
        if ($items === []) {
            return;
        }

        $accent = $this->allocateHex($image, (string) Arr::get($project, 'typography.accent_color', '#ffffff'));

        foreach (array_slice($items, 0, 6) as $index => $item) {
            $type = (string) Arr::get($item, 'type', 'glyph');
            if ($type === 'glyph') {
                $glyph = (string) Arr::get($item, 'glyph', Arr::get($item, 'label', '✦'));
                imagestring($image, 5, (int) round($width * (0.08 + (($index % 3) * 0.28))), (int) round($height * (0.08 + (intdiv($index, 3) * 0.74))), $glyph, $accent);
                continue;
            }

            if ($type === 'frame') {
                $lineY = $index % 2 === 0 ? (int) round($height * 0.08) : (int) round($height * 0.92);
                imageline($image, (int) round($width * 0.12), $lineY, (int) round($width * 0.88), $lineY, $accent);
            }
        }
    }

    // ─── Helpers ─────────────────────────────────────────────────────────────

    private function resolveTextBlocks(NovaCard $card, array $project): array
    {
        $blocks = collect(Arr::wrap(Arr::get($project, 'text_blocks', [])))
            ->filter(fn ($block): bool => is_array($block) && (bool) Arr::get($block, 'enabled', true) && trim((string) Arr::get($block, 'text', '')) !== '')
            ->values();

        if ($blocks->isNotEmpty()) {
            return $blocks->all();
        }

        // Fallback: use top-level card fields.
        return array_values(array_filter([
            trim((string) $card->title) !== ''        ? ['type' => 'title',  'text' => trim((string) $card->title)]        : null,
            trim((string) $card->quote_text) !== ''   ? ['type' => 'quote',  'text' => trim((string) $card->quote_text)]   : null,
            trim((string) $card->quote_author) !== '' ? ['type' => 'author', 'text' => trim((string) $card->quote_author)] : null,
            trim((string) $card->quote_source) !== '' ? ['type' => 'source', 'text' => trim((string) $card->quote_source)] : null,
        ]));
    }

    private function resolveAlignedX(string $alignment, int $width, int $padding, int $lineWidth): int
    {
        return match ($alignment) {
            'left'  => $padding,
            'right' => max($padding, $width - $padding - $lineWidth),
            default => max(0, (int) round(($width - $lineWidth) / 2)),
        };
    }

    private function resolveFocalRatios(string $focalPosition): array
    {
        $x = match ($focalPosition) {
            'left', 'top-left', 'bottom-left'    => 0.0,
            'right', 'top-right', 'bottom-right' => 1.0,
            default                               => 0.5,
        };
        $y = match ($focalPosition) {
            'top', 'top-left', 'top-right'          => 0.0,
            'bottom', 'bottom-left', 'bottom-right' => 1.0,
            default                                  => 0.5,
        };

        return [$x, $y];
    }

    private function paintVerticalGradient($image, int $width, int $height, string $fromHex, string $toHex): void
    {
        [$r1, $g1, $b1] = $this->hexToRgb($fromHex);
        [$r2, $g2, $b2] = $this->hexToRgb($toHex);

        for ($y = 0; $y < $height; $y++) {
            $ratio = $height > 1 ? $y / ($height - 1) : 0;
            $red   = (int) round($r1 + (($r2 - $r1) * $ratio));
            $green = (int) round($g1 + (($g2 - $g1) * $ratio));
            $blue  = (int) round($b1 + (($b2 - $b1) * $ratio));
            $color = imagecolorallocate($image, $red, $green, $blue);
            imageline($image, 0, $y, $width, $y, $color);
        }
    }

    private function allocateHex($image, string $hex): int
    {
        [$r, $g, $b] = $this->hexToRgb($hex);

        return imagecolorallocate($image, $r, $g, $b);
    }

    private function hexToRgb(string $hex): array
    {
        $normalized = ltrim($hex, '#');
        if (strlen($normalized) === 3) {
            $normalized = preg_replace('/(.)/', '$1$1', $normalized) ?: 'ffffff';
        }

        if (strlen($normalized) !== 6) {
            $normalized = 'ffffff';
        }

        return [
            (int) hexdec(substr($normalized, 0, 2)),
            (int) hexdec(substr($normalized, 2, 2)),
            (int) hexdec(substr($normalized, 4, 2)),
        ];
    }
}
