<?php

namespace App\Services\Ai;

/**
 * Continuous, math-driven decoration.
 *
 * Elements (blurred blobs, dotted-pattern boxes, highlight shapes, geometry) are
 * scattered once across a VIRTUAL STRIP the width of the whole set (N screens =
 * width N×100%). Each screen i renders the elements offset by −i×100%, so a shape
 * that overflows screen i's right edge continues onto screen i+1's left edge —
 * the scatter reads as designed continuity across the set, not disconnected random.
 *
 * The field geometry is seeded once (reproducible → /ai/shuffle re-rolls it);
 * colours are applied per screen, harmonized to the surface (accent on light,
 * lightened accent on dark, low opacity so it never goes muddy).
 *
 * `screen()` returns elements as edge rects (`left` may be negative / >100 to
 * bleed); ThemeGenerator materialises them per factor.
 */
class DecoGenerator
{
    public const FAMILIES = ['none', 'blobs', 'dots'];
    public const DENSITIES = ['minimal', 'medium', 'rich'];

    /** Legacy / descriptive names → current families. No circular-outline family
     *  any more (rings/ellipse outlines read as generic corner circles), so the
     *  geometric-leaning aliases fall back to soft blobs. */
    private const ALIASES = [
        'spotlight' => 'blobs', 'corners' => 'blobs', 'wave' => 'blobs', 'glow' => 'blobs',
        'orbs' => 'blobs', 'mesh' => 'blobs', 'rings' => 'blobs', 'pattern' => 'dots',
        'mix' => 'blobs', 'flow' => 'blobs', 'confetti' => 'dots', 'scatter' => 'blobs',
        'geometric' => 'blobs',
    ];

    /** @var array<int, array{type:string,x:float,y:float,w:float,r:float,rot:float,ci:int}> */
    private array $field = [];

    public function __construct(
        SeededRandom $rng,
        string $family,
        string $density = 'medium',
        int $screenCount = 1,
        private bool $orbs = true,
        private bool $sparkles = true,
    ) {
        $resolved = in_array($family, self::FAMILIES, true) ? $family : (self::ALIASES[$family] ?? 'blobs');
        if ($resolved === 'none') {
            return;
        }

        // Refined vocabulary — ONLY soft blobs / washes + dot fields (+ a subtle
        // sparkle glint). Circular outline shapes (rings, outline ellipses, arcs)
        // were removed: at corner scale they read as generic circles, not art. A
        // soft `mesh` wash carries presence so a screen never looks empty.
        $types = match ($resolved) {
            'blobs' => ['blob', 'mesh'],
            'dots'  => ['dotsbox', 'sparkle', 'mesh'],
            default => ['blob', 'mesh'],
        };

        $n = max(1, $screenCount);

        // Blobs match the reference app verbatim: per screen, a couple of large
        // accent circles whose TOP-LEFT corner is pushed beyond a canvas corner
        // so only a soft arc washes in (never a full visible disc). This is a
        // different placement model from the seam-bridge continuity used by the
        // other families, so it gets its own builder.
        if ($resolved === 'blobs') {
            $this->buildBlobField($rng, $n, $density);
            return;
        }

        // Big single shapes (blob/mesh/dotsbox) straddle the seams for continuity;
        // the sparkle glint is a small accent, never a bridge.
        $bridgeTypes = array_values(array_diff($types, ['sparkle'])) ?: $types;

        $topY = fn () => $rng->round(-16.0, 2.0);   // top corner band (above device for device-bottom layouts)
        $botY = fn () => $rng->round(82.0, 100.0);  // bottom corner band (below device for device-top layouts)

        $add = function (string $type, float $x, float $y) use ($rng) {
            [$wMin, $wMax, $rMin, $rMax] = $this->sizeFor($type);
            $w = $rng->round($wMin, $wMax);
            $this->field[] = [
                'type' => $type,
                'x'    => round($x, 2),
                'y'    => round($y, 2),
                'w'    => $w,
                'r'    => $rng->round($rMin, $rMax),
                'rot'  => $rng->round(0.0, 360.0),
                'ci'   => $rng->int(0, 1),
            ];
        };

        // ── BRIDGES: centred on every seam (incl. outer edges) at top + bottom
        // bands → continuity across the set, always a visible half on each side. ──
        $ti = 0;
        for ($k = 0; $k <= $n; $k++) {
            foreach ([true, false] as $isTop) {
                $type = $bridgeTypes[$ti++ % count($bridgeTypes)];
                $w    = $this->sizeFor($type)[1];
                $add($type, $k * 100.0 - $w / 2 + $rng->jitter(5.0), $isTop ? $topY() : $botY());
            }
        }

        // ── PRESENCE: every screen gets ONE large, very soft `mesh` wash hugging
        // a corner — depth, so a thin family never reads as a few lonely marks on
        // an empty field. (`mesh` is a soft radial field, not a ring outline.) ──
        for ($i = 0; $i < $n; $i++) {
            $w        = $this->sizeFor('mesh')[1];
            $leftSide = $rng->bool();
            $x        = $leftSide ? -$w * 0.35 : 100.0 - $w * 0.65;
            $add('mesh', $i * 100.0 + $x, $rng->bool() ? $topY() : $botY());
        }

        // ── ACCENTS: extra elements INSIDE each screen (top/bottom corners) for
        // density, so no screen looks empty. Count scales with density. ──
        $perScreen = match ($density) { 'minimal' => 2, 'rich' => 5, default => 3 };
        for ($i = 0; $i < $n; $i++) {
            for ($a = 0; $a < $perScreen; $a++) {
                $type = $types[$rng->int(0, count($types) - 1)];
                $w    = $this->sizeFor($type)[0];
                // somewhere across the screen width, fully/mostly inside
                $localX = $rng->round(4.0, 96.0 - $w * 0.6);
                $add($type, $i * 100.0 + $localX, $rng->bool() ? $topY() : $botY());
            }
        }
    }

    /**
     * Reference-faithful blob field. Each screen gets a big top-corner blob and
     * a smaller blob hugging the OPPOSITE bottom corner (mirrors the reference's
     * two <Blob>s at x=-15,y=-10,size=55 and x=70,y=75,size=45). Top-left corner
     * is pushed off the near edge so only a diffuse arc bleeds in. `op` carries
     * the reference per-blob opacity (primary brighter than secondary).
     */
    private function buildBlobField(SeededRandom $rng, int $n, string $density): void
    {
        $extra = match ($density) { 'minimal' => 0, 'rich' => 1, default => 0 };

        // All blobs share ONE accent (`ci => 0`) — the reference uses
        // `theme.accent` for every <Blob>; mixing accent + accent2 made the two
        // blobs read as different colours.
        for ($i = 0; $i < $n; $i++) {
            $base     = $i * 100.0;
            $leftTop  = $rng->bool();

            // Primary — large, hugging a TOP corner, top-left corner off-edge.
            $w1 = $rng->round(52.0, 62.0);
            $x1 = $leftTop
                ? $rng->round(-22.0, -10.0)
                : $rng->round(100.0 - $w1 + 6.0, 100.0 - $w1 + 16.0);
            $this->field[] = [
                'type' => 'blob', 'si' => $i, 'x' => round($base + $x1, 2), 'y' => $rng->round(-14.0, -4.0),
                'w' => $w1, 'r' => 1.0, 'rot' => 0.0, 'ci' => 0, 'op' => 0.42,
            ];

            // Secondary — smaller, OPPOSITE bottom corner.
            $w2 = $rng->round(42.0, 52.0);
            $x2 = $leftTop
                ? $rng->round(100.0 - $w2 + 6.0, 100.0 - $w2 + 16.0)
                : $rng->round(-20.0, -8.0);
            $this->field[] = [
                'type' => 'blob', 'si' => $i, 'x' => round($base + $x2, 2), 'y' => $rng->round(66.0, 80.0),
                'w' => $w2, 'r' => 1.0, 'rot' => 0.0, 'ci' => 0, 'op' => 0.33,
            ];

            // Rich density adds one extra mid-edge wash.
            for ($e = 0; $e < $extra; $e++) {
                $w3 = $rng->round(40.0, 50.0);
                $this->field[] = [
                    'type' => 'blob', 'si' => $i,
                    'x'    => round($base + ($rng->bool() ? -$rng->round(10.0, 18.0) : 100.0 - $w3 + $rng->round(6.0, 14.0)), 2),
                    'y'    => $rng->round(28.0, 56.0),
                    'w'    => $w3, 'r' => 1.0, 'rot' => 0.0, 'ci' => 0, 'op' => 0.20,
                ];
            }

            // A small gradient ORB tucked into the secondary corner gives the
            // wash real dimension (a glowing sphere vs. a flat disc). Gated by the
            // deck's enrichment plan + density. `ci => 1` so screen() also tints
            // it with the harmonised blob colour (see the orb branch there).
            if ($this->orbs && $density !== 'minimal') {
                $ow = $rng->round(26.0, 38.0);
                $ox = $leftTop ? $rng->round(70.0, 80.0) : $rng->round(-6.0, 4.0);
                $this->field[] = [
                    'type' => 'orb', 'si' => $i, 'x' => round($base + $ox, 2), 'y' => $rng->round(58.0, 70.0),
                    'w' => $ow, 'r' => 1.0, 'rot' => 0.0, 'ci' => 1, 'op' => 0.34,
                ];
            }

            // Premium shine: two small sparkles hugging the blob corners (kept in
            // the corner bands so they never land on the headline or device).
            if ($this->sparkles) {
                $this->field[] = [
                    'type' => 'sparkle', 'si' => $i,
                    'x'    => round($base + ($leftTop ? $rng->round(5.0, 13.0) : $rng->round(83.0, 91.0)), 2),
                    'y'    => $rng->round(7.0, 16.0),
                    'w'    => $rng->round(7.0, 12.0), 'r' => 1.0, 'rot' => $rng->round(0.0, 90.0), 'ci' => 0,
                ];
                $this->field[] = [
                    'type' => 'sparkle', 'si' => $i,
                    'x'    => round($base + ($leftTop ? $rng->round(80.0, 90.0) : $rng->round(6.0, 16.0)), 2),
                    'y'    => $rng->round(82.0, 91.0),
                    'w'    => $rng->round(6.0, 10.0), 'r' => 1.0, 'rot' => $rng->round(0.0, 90.0), 'ci' => 0,
                ];
            }
        }
    }

    /**
     * Nudge a mark colour until it has clear VALUE contrast from the surface, so
     * refined linework (rings, dots, outline ellipses) actually reads instead of
     * dissolving into a same-tone background. Deeper on light surfaces, lighter on
     * dark — capped iterations so it never runs to pure black/white.
     */
    private function ensurePresence(string $hex, string $surfaceRef, bool $light): string
    {
        $bg = PaletteFactory::relativeLuminance($surfaceRef);
        for ($k = 0; $k < 8 && abs(PaletteFactory::relativeLuminance($hex) - $bg) < 0.22; $k++) {
            $hex = $light ? PaletteFactory::darken($hex, 0.10) : PaletteFactory::lighten($hex, 0.10);
        }

        return $hex;
    }

    /** @return array{0:float,1:float,2:float,3:float} [wMin,wMax,rMin,rMax] */
    private function sizeFor(string $type): array
    {
        return match ($type) {
            'blob'    => [46.0, 84.0, 1.0, 1.0],
            'orb'     => [40.0, 72.0, 1.0, 1.0],
            'mesh'    => [72.0, 112.0, 1.0, 1.0],
            'dotsbox' => [34.0, 60.0, 0.7, 1.2],
            'sparkle' => [8.0, 18.0, 1.0, 1.0],
            default   => [28.0, 52.0, 0.7, 1.1], // generic fallback
        };
    }

    /**
     * Decoration visible on screen $index (0-based), sliced from the virtual strip.
     *
     * @param  array<string, mixed>  $palette
     * @return array<int, array{body: array<string, mixed>, rect: array<string, mixed>}>
     */
    public function screen(array $palette, bool $inverted, int $index): array
    {
        if ($this->field === []) {
            return [];
        }
        $accent  = $palette['accent'] ?? '#5B7CFA';
        $accent2 = $palette['accent2'] ?? PaletteFactory::shiftHue($accent, 32.0);
        $surface = $inverted ? ($palette['bgAlt'] ?? '#ffffff') : ($palette['bg'] ?? '#ffffff');
        $light   = PaletteFactory::isLight($surface);
        $c1 = $light ? $accent  : PaletteFactory::lighten($accent, 0.34);
        $c2 = $light ? $accent2 : PaletteFactory::lighten($accent2, 0.34);
        // Blob colour: POP + HARMONY, built in HSL so it ALWAYS (a) carries real
        // colour — never greys into the bg — and (b) sits at a clear value
        // contrast from the surface so it actually reads.
        //   HUE: a TINTED surface (cream, dusk, etc.) lends its temperature
        //        (warm bg → warm blob, like the reference cream→amber), nudged
        //        toward the accent for brand character; a NEUTRAL surface
        //        (white / grey) has no temperature so the accent hue is used.
        //   SAT: floored so it never desaturates to grey, capped so never neon.
        //   LIGHTNESS: pushed clearly away from the surface (darker on light bg,
        //        lighter on dark bg) — value contrast is what makes it pop even
        //        at the soft blob opacity.
        // `$surface` may be a gradient array — resolve a representative hex first.
        $surfaceRef = PaletteFactory::surfaceRef($surface);
        [$sh, , $sl] = PaletteFactory::hexToHsl($surfaceRef);
        [$ah, $as] = PaletteFactory::hexToHsl($accent);
        // Neutral test via RGB chroma (max-min), not HSL saturation — HSL sat is
        // unstable near white/black, which mis-flags pure-white surfaces.
        [$cr, $cg, $cb] = sscanf(ltrim($surfaceRef, '#'), '%02x%02x%02x');
        $surfaceTinted = (max($cr, $cg, $cb) - min($cr, $cg, $cb)) >= 12;
        if ($surfaceTinted) {
            // Shortest-path hue interpolation: surface temperature → accent (35%).
            $dh  = fmod($ah - $sh + 540.0, 360.0) - 180.0;
            $hue = fmod($sh + $dh * 0.35 + 360.0, 360.0);
        } else {
            $hue = $ah;
        }
        // HARSHNESS TEMPERING. Cool hues (~180–290°, peak ~230° = pure blue) read
        // far harsher than warm ones at the same saturation — a saturated blue/navy
        // blob looks cold and intense, while amber looks inviting. So for cool
        // hues we cut the saturation cap and lift the blob lighter on light
        // surfaces → a soft slate-blue wash, not a cold spot. Warm hues keep their
        // richer cap (the reference amber look).
        $cool   = ($hue >= 180.0 && $hue <= 290.0) ? max(0.0, 1.0 - abs($hue - 230.0) / 60.0) : 0.0;
        $satCap = 0.58 - 0.20 * $cool;                 // warm 0.58 → blue ~0.38
        $sat    = max(0.30, min($as, $satCap));
        $lit    = $light
            ? max(0.40, $sl - 0.26 + 0.07 * $cool)     // cool blobs sit lighter (softer)
            : min(0.82, $sl + 0.30);
        $blobColor = PaletteFactory::hslToHex($hue, $sat, $lit);

        // ART-DIRECTOR OVERRIDE: when the model supplied an explicit decoration
        // colour (it SAW the screenshots and chose a harmonised tone), trust it for
        // the soft washes — no heuristic colour math.
        $decoOverride = (isset($palette['decoration']) && is_string($palette['decoration']) && $palette['decoration'] !== '')
            ? $palette['decoration'] : null;
        if ($decoOverride !== null) {
            $blobColor = $decoOverride;
            $c1 = $c2 = $decoOverride;
        }

        // MARK COLOUR — the crisp marks (dot fields, sparkles) must READ. A near-bg
        // decoration tint vanished on a light deck; force clear VALUE contrast from
        // the surface so they register (the soft washes — blob/orb/mesh — stay
        // low-contrast on purpose).
        $markColor = $this->ensurePresence($decoOverride ?? $accent, $surfaceRef, $light);

        $out = [];
        foreach ($this->field as $e) {
            // Blobs are pinned to their home screen (`si`) — their off-edge
            // arcs bleed off-CANVAS, not into the neighbouring screen, so each
            // screen shows exactly its own corner washes (reference model).
            if (isset($e['si']) && $e['si'] !== $index) {
                continue;
            }
            $left = $e['x'] - $index * 100.0;
            // Require a meaningful slice INSIDE the screen (≥6% of canvas width) —
            // never emit an element that's fully (or all-but) off-screen.
            $visible = min(100.0, $left + $e['w']) - max(0.0, $left);
            if ($visible < 6.0) {
                continue;
            }
            $hex  = $e['ci'] === 0 ? $c1 : $c2;
            $hex2 = $e['ci'] === 0 ? $c2 : $c1;
            $rect = fn (float $w) => ['top' => $e['y'], 'left' => round($left, 2), 'w' => $w, 'r' => $e['r'], 'mode' => 'edge'];

            // Reference blob opacity: per-blob base (primary brighter than
            // secondary), dimmed on dark surfaces so the lightened accent never
            // glares. Mirrors the reference's ~0.32-light / ~0.25-dark values.
            $op          = (float) ($e['op'] ?? 0.28);
            $blobOpacity = $light ? $op : round($op * 0.72, 3);
            // Soft washes (blob/orb) use the harmonised low-contrast tone; crisp
            // marks (dots/sparkle) use the presence-ensured mark colour so they read.
            $soft  = in_array($e['type'], ['blob', 'orb'], true);
            $elHex = $soft ? $blobColor : $markColor;
            $body = $e['type'] === 'mesh' ? $this->mesh($hex, $hex2) : $this->body($e['type'], $elHex, $blobOpacity);
            if ($body === null) {
                continue;
            }
            $out[] = ['body' => $body, 'rect' => $rect($e['w'])];
        }

        return $out;
    }

    /** @return array<string, mixed>|null */
    private function body(string $type, string $hex, float $blobOpacity = 0.28): ?array
    {
        return match ($type) {
            // Exact reference model: a SOLID disc + Gaussian blur + low opacity
            // (`<div … background:#d97706; border-radius:50%; filter:blur(79px);
            // opacity:0.32>`). Rendered via the renderer's NATIVE canvas blur
            // (`ctx.filter='blur()'`, the same engine CSS uses) — not Konva's
            // StackBlur, which rims. Blur is a constant ~6% of a nominal 1320px
            // canvas (the shared layer style can't carry a per-factor radius).
            'blob'    => [
                'type'  => 'shape',
                'shape' => 'ellipse',
                'style' => [
                    'fill'    => ['type' => 'solid', 'value' => $hex],
                    'opacity' => $blobOpacity,
                    'blur'    => round(1320.0 * 0.06), // ≈79px = reference cW*0.06
                ],
            ],
            // Gradient ORB: a dimensional sphere — a bright off-centre highlight
            // falling to the base tone and out to transparent. Reads as premium
            // 3-D depth where a flat blurred disc reads as a wash. No blur needed;
            // the radial falloff IS the softness (and dodges the StackBlur rim).
            'orb' => [
                'type'  => 'shape',
                'shape' => 'ellipse',
                'style' => [
                    'fill' => [
                        'type'   => 'radial',
                        'cx'     => 0.38,
                        'cy'     => 0.34,
                        'radius' => 0.62,
                        'stops'  => [
                            ['offset' => 0,    'color' => PaletteFactory::rgba(PaletteFactory::lighten($hex, 0.22), 0.95)],
                            ['offset' => 0.5,  'color' => PaletteFactory::rgba($hex, 0.7)],
                            ['offset' => 1,    'color' => PaletteFactory::rgba($hex, 0.0)],
                        ],
                    ],
                    'opacity' => min(0.95, $blobOpacity * 1.7),
                ],
            ],
            // SPARKLE: a sharp 4-point glint for premium shine. Small + scattered.
            'sparkle' => [
                'type'        => 'shape',
                'shape'       => 'star',
                'starPoints'  => 4,
                'innerRatio'  => 0.34,
                'style'       => ['fill' => ['type' => 'solid', 'value' => $hex], 'opacity' => 0.7],
            ],
            'dotsbox' => [
                'type'  => 'shape',
                'shape' => 'rect',
                'style' => [
                    'fill'    => ['type' => 'pattern', 'patternId' => 'dots', 'color' => $hex, 'scale' => 3.2],
                    'opacity' => 0.2,
                    'radius'  => 28,
                ],
            ],
            default => null,
        };
    }

    /** Gradient mesh: a soft two-colour radial field (accent → accent2 → clear). */
    private function mesh(string $c1, string $c2): array
    {
        return [
            'type'  => 'shape',
            'shape' => 'ellipse',
            'style' => [
                'fill' => [
                    'type'   => 'radial',
                    'cx'     => 0.5,
                    'cy'     => 0.5,
                    'radius' => 0.5,
                    'stops'  => [
                        ['offset' => 0,    'color' => PaletteFactory::rgba($c1, 0.22)],
                        ['offset' => 0.55, 'color' => PaletteFactory::rgba($c2, 0.14)],
                        ['offset' => 1,    'color' => PaletteFactory::rgba($c2, 0.0)],
                    ],
                ],
                'opacity' => 1.0,
            ],
        ];
    }
}
