<?php

namespace App\Domain\Shared\Branding;

/**
 * A full 50…900 shade ramp derived from one tenant colour, plus a foreground
 * colour that actually contrasts with the base.
 *
 * The ERP hardcoded `#fff` as the text colour on the brand accent
 * (components/layouts/sidebar.blade.php:184). A tenant whose brand is yellow
 * got white text on yellow — about 1.3:1, unreadable, and no amount of design
 * work downstream can rescue it because the value is baked into the markup.
 *
 * The ramp is generated in OKLCH, where lightness is perceptual: stepping L
 * evenly produces shades that look evenly spaced, which stepping HSL lightness
 * does not. It is computed once per branding_version in PHP and emitted as
 * custom properties, never with color-mix() at render time — a color-mix()
 * against a non-inline @theme variable resolves against the build-time copy,
 * not the tenant's runtime value.
 */
final readonly class ColorRamp
{
    /**
     * @param  array<int, string>  $shades  50…900 → oklch() strings
     * @param  string  $foreground  black or white, whichever contrasts with base
     */
    public function __construct(
        public string $base,
        public array $shades,
        public string $foreground,
    ) {}

    /**
     * step => [how far to travel from the base colour, share of its chroma].
     *
     * The lightness is anchored on the tenant's own colour and travels toward
     * a near-white or near-black limit, rather than aiming at fixed targets.
     * Fixed targets break for any brand that is inherently light or dark: a
     * yellow sits at L 0.86, so a table that puts 400 at L 0.70 makes 400
     * *darker* than 500 and the ramp stops meaning anything.
     *
     * The chroma share is deliberately steep at the pale end. At L 0.97 a
     * chroma of 0.10 is outside sRGB entirely, and the browser clips it to
     * something muddy — the pale shades of a real ramp keep only a few percent
     * of the base saturation.
     *
     * 500 is the tenant's own colour, untouched.
     */
    private const LIGHT_LIMIT = 0.985;
    private const DARK_LIMIT = 0.180;

    private const STEPS = [
        50 => [-0.92, 0.06],
        100 => [-0.82, 0.12],
        200 => [-0.66, 0.22],
        300 => [-0.46, 0.40],
        400 => [-0.24, 0.70],
        500 => [0.00, 1.00],
        600 => [0.13, 0.98],
        700 => [0.30, 0.90],
        800 => [0.50, 0.78],
        900 => [0.70, 0.62],
    ];

    public static function fromHex(string $hex): self
    {
        [$r, $g, $b] = self::hexToRgb($hex);
        [$l, $c, $h] = self::rgbToOklch($r, $g, $b);

        $shades = [];

        foreach (self::STEPS as $step => [$travel, $chromaShare]) {
            $lightness = $travel < 0
                ? $l + (self::LIGHT_LIMIT - $l) * -$travel
                : $l - ($l - self::DARK_LIMIT) * $travel;

            $shades[$step] = self::oklch($lightness, $c * $chromaShare, $h);
        }

        return new self(
            base: self::normalizeHex($hex),
            shades: $shades,
            foreground: self::readableForeground($r, $g, $b),
        );
    }

    /**
     * WCAG relative luminance, then whichever of black or white gives the
     * higher contrast ratio against it. Both candidates are checked rather
     * than assuming a luminance threshold, because the threshold sits at a
     * different place for different hues.
     */
    public static function readableForeground(int $r, int $g, int $b): string
    {
        $luminance = self::relativeLuminance($r, $g, $b);

        $againstWhite = 1.05 / ($luminance + 0.05);
        $againstBlack = ($luminance + 0.05) / 0.05;

        return $againstBlack >= $againstWhite ? '#111827' : '#ffffff';
    }

    public static function contrastRatio(string $hexA, string $hexB): float
    {
        [$r1, $g1, $b1] = self::hexToRgb($hexA);
        [$r2, $g2, $b2] = self::hexToRgb($hexB);

        $l1 = self::relativeLuminance($r1, $g1, $b1);
        $l2 = self::relativeLuminance($r2, $g2, $b2);

        [$light, $dark] = $l1 >= $l2 ? [$l1, $l2] : [$l2, $l1];

        return ($light + 0.05) / ($dark + 0.05);
    }

    private static function relativeLuminance(int $r, int $g, int $b): float
    {
        $channel = function (int $v): float {
            $s = $v / 255;

            return $s <= 0.03928 ? $s / 12.92 : (($s + 0.055) / 1.055) ** 2.4;
        };

        return 0.2126 * $channel($r) + 0.7152 * $channel($g) + 0.0722 * $channel($b);
    }

    /** @return array{int, int, int} */
    public static function hexToRgb(string $hex): array
    {
        $hex = ltrim(trim($hex), '#');

        if (strlen($hex) === 3) {
            $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
        }

        if (! preg_match('/^[0-9a-fA-F]{6}$/', $hex)) {
            $hex = '2563eb';
        }

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

    public static function normalizeHex(string $hex): string
    {
        [$r, $g, $b] = self::hexToRgb($hex);

        return sprintf('#%02x%02x%02x', $r, $g, $b);
    }

    /**
     * sRGB → linear → CIE XYZ (D65) → OKLab → OKLCH.
     *
     * @return array{float, float, float} [L 0..1, C, H degrees]
     */
    public static function rgbToOklch(int $r, int $g, int $b): array
    {
        $lin = function (int $v): float {
            $s = $v / 255;

            return $s <= 0.04045 ? $s / 12.92 : (($s + 0.055) / 1.055) ** 2.4;
        };

        $lr = $lin($r);
        $lg = $lin($g);
        $lb = $lin($b);

        $l = 0.4122214708 * $lr + 0.5363325363 * $lg + 0.0514459929 * $lb;
        $m = 0.2119034982 * $lr + 0.6806995451 * $lg + 0.1073969566 * $lb;
        $s = 0.0883024619 * $lr + 0.2817188376 * $lg + 0.6299787005 * $lb;

        $l_ = $l ** (1 / 3);
        $m_ = $m ** (1 / 3);
        $s_ = $s ** (1 / 3);

        $okL = 0.2104542553 * $l_ + 0.7936177850 * $m_ - 0.0040720468 * $s_;
        $okA = 1.9779984951 * $l_ - 2.4285922050 * $m_ + 0.4505937099 * $s_;
        $okB = 0.0259040371 * $l_ + 0.7827717662 * $m_ - 0.8086757660 * $s_;

        $chroma = sqrt($okA ** 2 + $okB ** 2);
        $hue = rad2deg(atan2($okB, $okA));

        if ($hue < 0) {
            $hue += 360;
        }

        return [$okL, $chroma, $hue];
    }

    private static function oklch(float $l, float $c, float $h): string
    {
        return sprintf('oklch(%.4f %.4f %.2f)', max(0, min(1, $l)), max(0, $c), $h);
    }
}
