<?php
declare(strict_types=1);

namespace App\Services;

use App\Core\Auth;
use App\Core\Database;
use App\Industry\Catalog;
use App\Models\AuditLog;
use RuntimeException;

final class SetupService
{
    private static ?array $draftCache = null;
    private static ?array $liveCache = null;

    public static function clearCache(): void
    {
        self::$draftCache = null;
        self::$liveCache = null;
    }

    public static function draft(): array
    {
        self::ensure();
        if (self::$draftCache !== null) {
            return self::$draftCache;
        }
        $row = self::row();
        self::$draftCache = self::withMissingModules(self::withCatalogFields(self::decode($row['draft'] ?? null) ?? SetupDefaults::payload()));
        return self::$draftCache;
    }

    public static function published(): array
    {
        self::ensure();
        if (self::$liveCache !== null) {
            return self::$liveCache;
        }
        $row = self::row();
        self::$liveCache = self::withMissingModules(self::withCatalogFields(self::decode($row['published'] ?? null) ?? SetupDefaults::payload()));
        return self::$liveCache;
    }

    public static function isDirty(): bool
    {
        return json_encode(self::draft()) !== json_encode(self::published());
    }

    public static function canUndo(): bool
    {
        $row = self::row();
        return self::decode($row['previous'] ?? null) !== null;
    }

    public static function publishedAt(): ?string
    {
        return self::row()['published_at'] ?? null;
    }

    public static function label(string $key, ?string $fallback = null): string
    {
        $labels = self::published()['labels'] ?? [];
        if (isset($labels[$key]) && trim((string) $labels[$key]) !== '') {
            return (string) $labels[$key];
        }
        return $fallback ?? (SetupDefaults::LABEL_KEYS[$key] ?? $key);
    }

    /** @return list<array<string, mixed>> */
    public static function fieldsFor(string $entity, bool $draft = false): array
    {
        $pack = $draft ? self::draft() : self::published();
        $out = [];
        foreach ($pack['fields'] ?? [] as $f) {
            if (($f['entity'] ?? '') === $entity && !empty($f['active'])) {
                $out[] = $f;
            }
        }
        if ($out === []) {
            $industry = Catalog::current();
            $key = $entity === 'account' ? 'account_fields' : ($entity === 'opportunity' ? 'opportunity_fields' : '');
            foreach ($industry[$key] ?? [] as $f) {
                $out[] = SetupDefaults::normalizeField($entity, $f, 'catalog');
            }
        }
        return $out;
    }

    public static function formShows(string $form, string $field): bool
    {
        if (in_array($field, SetupDefaults::LOCKED_FORM_FIELDS[$form] ?? [], true)) {
            return true;
        }
        $list = self::published()['forms'][$form] ?? null;
        if (!is_array($list) || $list === []) {
            return true;
        }
        return in_array($field, $list, true);
    }

    public static function dashOn(string $role, string $card): bool
    {
        $dash = self::published()['dashboard'] ?? [];
        if (!array_key_exists($role, $dash)) {
            return true;
        }
        return in_array($card, $dash[$role] ?? [], true);
    }

    public static function permission(string $role, string $module): string
    {
        $level = self::published()['permissions'][$role][$module] ?? null;
        if (in_array($level, ['hide', 'view', 'edit'], true)) {
            return $level;
        }
        $allowed = SetupDefaults::roleModules()[$module] ?? [];
        return in_array($role, $allowed, true) ? 'edit' : 'hide';
    }

    /** @return list<array<string, mixed>> */
    public static function automations(string $trigger): array
    {
        $out = [];
        foreach (self::published()['automations'] ?? [] as $rule) {
            if (!empty($rule['active']) && ($rule['trigger'] ?? '') === $trigger) {
                $out[] = $rule;
            }
        }
        return $out;
    }

    /** @return list<string> */
    public static function stageRequired(int|string $stageId): array
    {
        $needle = (string) $stageId;
        foreach (self::published()['stages'] ?? [] as $s) {
            $id = (string) ($s['id'] ?? '');
            $code = (string) ($s['code'] ?? '');
            if ($id === $needle || $code === $needle || 'stg_' . $code === $needle) {
                return array_values(array_filter(array_map('strval', $s['required_fields'] ?? [])));
            }
        }
        return [];
    }

    public static function saveSection(string $section, array $input): void
    {
        $draft = self::draft();
        $draft = match ($section) {
            'names' => self::patchNames($draft, $input),
            'fields' => self::patchFields($draft, $input),
            'stages' => self::patchStages($draft, $input),
            'forms' => self::patchForms($draft, $input),
            'alerts' => self::patchAlerts($draft, $input),
            'dashboard' => self::patchDashboard($draft, $input),
            'permissions' => self::patchPermissions($draft, $input),
            default => throw new RuntimeException('Unknown setup section.'),
        };
        self::writeDraft($draft);
        AuditLog::write('setup.draft', 'setup', null, ['section' => $section]);
    }

    public static function publish(): void
    {
        $draft = self::draft();
        self::assertPublishable($draft);
        $row = self::row();
        $previous = self::decode($row['published'] ?? null);
        self::applyStages($draft['stages'] ?? []);
        Database::instance()->query(
            'UPDATE setup_states
             SET published = :pub, previous = :prev, published_at = NOW(), published_by = :uid
             WHERE tenant_id = :tid',
            [
                'pub' => json_encode($draft, JSON_UNESCAPED_UNICODE),
                'prev' => $previous ? json_encode($previous, JSON_UNESCAPED_UNICODE) : null,
                'uid' => Auth::instance()->id(),
                'tid' => self::tenantId(),
            ]
        );
        self::clearCache();
        AuditLog::write('setup.publish', 'setup', self::tenantId());
    }

    public static function undo(): void
    {
        $row = self::row();
        $previous = self::decode($row['previous'] ?? null);
        if ($previous === null) {
            throw new RuntimeException('There is no published save to undo.');
        }
        self::applyStages($previous['stages'] ?? []);
        $json = json_encode($previous, JSON_UNESCAPED_UNICODE);
        Database::instance()->query(
            'UPDATE setup_states
             SET draft = :draft, published = :pub, draft_updated_at = NOW()
             WHERE tenant_id = :tid',
            ['draft' => $json, 'pub' => $json, 'tid' => self::tenantId()]
        );
        self::clearCache();
        AuditLog::write('setup.undo', 'setup', self::tenantId());
    }

    public static function onIndustryChanged(string $code): void
    {
        $industry = Catalog::get($code);
        $fresh = SetupDefaults::payload($industry);
        $current = self::rowExists() ? self::draft() : $fresh;
        $keepCustom = [];
        foreach ($current['fields'] ?? [] as $f) {
            if (($f['source'] ?? '') === 'custom') {
                $keepCustom[] = $f;
            }
        }
        $fresh['fields'] = array_merge($fresh['fields'], $keepCustom);
        $fresh['automations'] = $current['automations'] ?? $fresh['automations'];
        $fresh['dashboard'] = $current['dashboard'] ?? $fresh['dashboard'];
        $fresh['permissions'] = $current['permissions'] ?? $fresh['permissions'];
        self::writeDraft($fresh);
        $row = self::row();
        Database::instance()->query(
            'UPDATE setup_states SET published = :pub, published_at = NOW(), published_by = :uid WHERE tenant_id = :tid',
            [
                'pub' => json_encode($fresh, JSON_UNESCAPED_UNICODE),
                'uid' => Auth::instance()->id(),
                'tid' => self::tenantId(),
            ]
        );
        self::clearCache();
    }

    public static function ensure(): void
    {
        $tid = Auth::instance()->tenantId();
        if (!$tid) {
            return;
        }
        self::ensureSchema();
        if (self::rowExists()) {
            return;
        }
        $payload = SetupDefaults::payload();
        $json = json_encode($payload, JSON_UNESCAPED_UNICODE);
        Database::instance()->query(
            'INSERT INTO setup_states (tenant_id, draft, published, draft_updated_at, published_at, published_by)
             VALUES (:tid, :draft, :pub, NOW(), NOW(), :uid)',
            ['tid' => $tid, 'draft' => $json, 'pub' => $json, 'uid' => Auth::instance()->id()]
        );
        self::clearCache();
    }

    private static function ensureSchema(): void
    {
        static $ready = false;
        if ($ready) {
            return;
        }
        $db = Database::instance();
        $has = (int) $db->fetchColumn(
            "SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'setup_states'"
        );
        if ($has < 1) {
            $db->pdo()->exec("CREATE TABLE setup_states (
              tenant_id INT UNSIGNED NOT NULL,
              draft JSON NOT NULL,
              published JSON NULL,
              previous JSON NULL,
              draft_updated_at DATETIME NULL,
              published_at DATETIME NULL,
              published_by INT UNSIGNED NULL,
              PRIMARY KEY (tenant_id)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
        }
        $leadAttrs = (int) $db->fetchColumn(
            "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'leads' AND COLUMN_NAME = 'attrs'"
        );
        if ($leadAttrs < 1) {
            $db->pdo()->exec('ALTER TABLE leads ADD COLUMN attrs JSON NULL');
        }
        $ready = true;
    }

    private static function rowExists(): bool
    {
        return self::row() !== [];
    }

    /** @return array<string, mixed> */
    private static function row(): array
    {
        $tid = Auth::instance()->tenantId();
        if (!$tid) {
            return [];
        }
        return Database::instance()->fetch(
            'SELECT * FROM setup_states WHERE tenant_id = :tid',
            ['tid' => $tid]
        ) ?: [];
    }

    private static function tenantId(): int
    {
        $tid = Auth::instance()->tenantId();
        if (!$tid) {
            throw new RuntimeException('No company in session.');
        }
        return $tid;
    }

    private static function writeDraft(array $draft): void
    {
        Database::instance()->query(
            'UPDATE setup_states SET draft = :draft, draft_updated_at = NOW() WHERE tenant_id = :tid',
            ['draft' => json_encode($draft, JSON_UNESCAPED_UNICODE), 'tid' => self::tenantId()]
        );
        self::clearCache();
    }

    /** Keep new industry-default boxes on existing published setups. */
    private static function withCatalogFields(array $pack): array
    {
        $industry = Catalog::current();
        $have = [];
        foreach ($pack['fields'] ?? [] as $f) {
            $have[(string) ($f['entity'] ?? '') . ':' . (string) ($f['key'] ?? '')] = true;
        }
        foreach (['account' => 'account_fields', 'opportunity' => 'opportunity_fields'] as $entity => $key) {
            foreach ($industry[$key] ?? [] as $f) {
                $slot = $entity . ':' . (string) ($f['key'] ?? '');
                if (isset($have[$slot])) {
                    continue;
                }
                $pack['fields'][] = SetupDefaults::normalizeField($entity, $f, 'catalog');
                $have[$slot] = true;
            }
        }
        return $pack;
    }

    /** Add new menus (e.g. Documents) to older published setups. */
    private static function withMissingModules(array $pack): array
    {
        $map = SetupDefaults::roleModules();
        $perms = $pack['permissions'] ?? [];
        foreach (SetupDefaults::ROLES as $role) {
            foreach (array_keys(SetupDefaults::MODULES) as $module) {
                if (isset($perms[$role][$module])) {
                    continue;
                }
                $perms[$role][$module] = in_array($role, $map[$module] ?? [], true) ? 'edit' : 'hide';
            }
        }
        $pack['permissions'] = $perms;
        return $pack;
    }

    private static function decode(mixed $raw): ?array
    {
        if (is_array($raw)) {
            return $raw;
        }
        if (!is_string($raw) || $raw === '') {
            return null;
        }
        $decoded = json_decode($raw, true);
        return is_array($decoded) ? $decoded : null;
    }

    /** @param array<string, mixed> $draft */
    private static function patchNames(array $draft, array $input): array
    {
        $labels = $draft['labels'] ?? SetupDefaults::LABEL_KEYS;
        foreach (SetupDefaults::LABEL_KEYS as $key => $fallback) {
            $val = trim((string) ($input['labels'][$key] ?? $labels[$key] ?? $fallback));
            $labels[$key] = $val !== '' ? mb_substr($val, 0, 40) : $fallback;
        }
        $draft['labels'] = $labels;
        return $draft;
    }

    /** @param array<string, mixed> $draft */
    private static function patchFields(array $draft, array $input): array
    {
        $action = (string) ($input['action'] ?? 'update');
        $fields = $draft['fields'] ?? [];
        if ($action === 'add') {
            if (count($fields) >= 40) {
                throw new RuntimeException('You can add up to 40 extra boxes.');
            }
            $entity = (string) ($input['entity'] ?? '');
            if (!in_array($entity, ['lead', 'account', 'opportunity', 'service'], true)) {
                throw new RuntimeException('Pick where this box belongs.');
            }
            $key = self::safeKey((string) ($input['key'] ?? ''));
            foreach ($fields as $f) {
                if (($f['entity'] ?? '') === $entity && ($f['key'] ?? '') === $key) {
                    throw new RuntimeException('That box key is already used.');
                }
            }
            $fields[] = SetupDefaults::normalizeField($entity, [
                'id' => 'fld_' . bin2hex(random_bytes(4)),
                'key' => $key,
                'label' => trim((string) ($input['label'] ?? '')) ?: ucfirst(str_replace('_', ' ', $key)),
                'type' => (string) ($input['type'] ?? 'text'),
                'options' => self::parseOptions((string) ($input['options'] ?? '')),
            ]);
            $form = match ($entity) {
                'lead' => 'lead_create',
                'opportunity' => 'opportunity_create',
                default => 'visit',
            };
            if (!in_array($key, $draft['forms'][$form] ?? [], true)) {
                $draft['forms'][$form][] = $key;
            }
        } elseif ($action === 'delete') {
            $id = (string) ($input['id'] ?? '');
            $fields = array_values(array_filter($fields, static function ($f) use ($id) {
                return ($f['id'] ?? '') !== $id || ($f['source'] ?? '') === 'catalog';
            }));
        } else {
            $next = [];
            foreach ($fields as $f) {
                $id = (string) ($f['id'] ?? '');
                $row = $input['fields'][$id] ?? null;
                if (!is_array($row)) {
                    $next[] = $f;
                    continue;
                }
                $f['label'] = trim((string) ($row['label'] ?? $f['label']));
                $f['active'] = !empty($row['active']);
                if (($f['type'] ?? '') === 'select' && isset($row['options'])) {
                    $f['options'] = self::parseOptions((string) $row['options']);
                }
                $next[] = $f;
            }
            $fields = $next;
        }
        $draft['fields'] = $fields;
        return $draft;
    }

    /** @param array<string, mixed> $draft */
    private static function patchStages(array $draft, array $input): array
    {
        $action = (string) ($input['action'] ?? 'update');
        $stages = $draft['stages'] ?? [];
        if ($action === 'add') {
            if (count($stages) >= 20) {
                throw new RuntimeException('You can have up to 20 stages.');
            }
            $name = trim((string) ($input['name'] ?? ''));
            if ($name === '') {
                throw new RuntimeException('Enter a stage name.');
            }
            $code = self::safeKey($name);
            $stages[] = [
                'id' => 'stg_' . bin2hex(random_bytes(3)),
                'code' => $code,
                'name' => mb_substr($name, 0, 80),
                'probability' => max(0, min(100, (int) ($input['probability'] ?? 10))),
                'is_won' => 0,
                'is_lost' => 0,
                'sort_order' => count($stages) + 1,
                'required_fields' => [],
            ];
        } elseif ($action === 'delete') {
            $id = (string) ($input['id'] ?? '');
            $stages = array_values(array_filter($stages, static fn($s) => (string) ($s['id'] ?? '') !== $id));
        } elseif ($action === 'move') {
            $id = (string) ($input['id'] ?? '');
            $dir = (string) ($input['dir'] ?? '');
            $i = array_search($id, array_map(static fn($s) => (string) ($s['id'] ?? ''), $stages), true);
            if ($i !== false) {
                $j = $dir === 'up' ? $i - 1 : $i + 1;
                if (isset($stages[$j])) {
                    [$stages[$i], $stages[$j]] = [$stages[$j], $stages[$i]];
                }
            }
        } else {
            $next = [];
            foreach ($stages as $s) {
                $id = (string) ($s['id'] ?? '');
                $row = $input['stages'][$id] ?? null;
                if (!is_array($row)) {
                    $next[] = $s;
                    continue;
                }
                $s['name'] = trim((string) ($row['name'] ?? $s['name']));
                $s['probability'] = max(0, min(100, (int) ($row['probability'] ?? $s['probability'])));
                $flag = (string) ($row['flag'] ?? '');
                $s['is_won'] = $flag === 'won' ? 1 : 0;
                $s['is_lost'] = $flag === 'lost' ? 1 : 0;
                $req = $row['required_fields'] ?? [];
                $s['required_fields'] = is_array($req) ? array_values(array_map('strval', $req)) : [];
                $next[] = $s;
            }
            $stages = $next;
        }
        $sort = 1;
        foreach ($stages as &$s) {
            $s['sort_order'] = $sort++;
        }
        unset($s);
        $draft['stages'] = $stages;
        return $draft;
    }

    /** @param array<string, mixed> $draft */
    private static function patchForms(array $draft, array $input): array
    {
        $forms = $draft['forms'] ?? [];
        foreach (SetupDefaults::FORM_KEYS as $form) {
            $picked = $input['forms'][$form] ?? null;
            if (!is_array($picked)) {
                continue;
            }
            $list = array_values(array_filter(array_map('strval', $picked)));
            foreach (SetupDefaults::LOCKED_FORM_FIELDS[$form] ?? [] as $locked) {
                if (!in_array($locked, $list, true)) {
                    array_unshift($list, $locked);
                }
            }
            $forms[$form] = array_values(array_unique($list));
        }
        $draft['forms'] = $forms;
        return $draft;
    }

    /** @param array<string, mixed> $draft */
    private static function patchAlerts(array $draft, array $input): array
    {
        $action = (string) ($input['action'] ?? 'update');
        $rules = $draft['automations'] ?? [];
        if ($action === 'add') {
            if (count($rules) >= 20) {
                throw new RuntimeException('You can have up to 20 rules.');
            }
            $rules[] = [
                'id' => 'aut_' . bin2hex(random_bytes(3)),
                'active' => true,
                'trigger' => array_key_exists((string) ($input['trigger'] ?? ''), SetupDefaults::TRIGGERS) ? (string) $input['trigger'] : 'lead_created',
                'action' => array_key_exists((string) ($input['action_type'] ?? ''), SetupDefaults::ACTIONS) ? (string) $input['action_type'] : 'create_task',
                'title' => trim((string) ($input['title'] ?? 'Follow up')) ?: 'Follow up',
                'due_days' => max(0, min(90, (int) ($input['due_days'] ?? 1))),
            ];
        } elseif ($action === 'delete') {
            $id = (string) ($input['id'] ?? '');
            $rules = array_values(array_filter($rules, static fn($r) => (string) ($r['id'] ?? '') !== $id));
        } else {
            $next = [];
            foreach ($rules as $r) {
                $id = (string) ($r['id'] ?? '');
                $row = $input['rules'][$id] ?? null;
                if (!is_array($row)) {
                    $next[] = $r;
                    continue;
                }
                $r['active'] = !empty($row['active']);
                $r['trigger'] = array_key_exists((string) ($row['trigger'] ?? ''), SetupDefaults::TRIGGERS) ? (string) $row['trigger'] : $r['trigger'];
                $r['action'] = array_key_exists((string) ($row['action'] ?? ''), SetupDefaults::ACTIONS) ? (string) $row['action'] : $r['action'];
                $r['title'] = trim((string) ($row['title'] ?? $r['title'])) ?: $r['title'];
                $r['due_days'] = max(0, min(90, (int) ($row['due_days'] ?? $r['due_days'])));
                $next[] = $r;
            }
            $rules = $next;
        }
        $draft['automations'] = $rules;
        return $draft;
    }

    /** @param array<string, mixed> $draft */
    private static function patchDashboard(array $draft, array $input): array
    {
        $dash = $draft['dashboard'] ?? [];
        foreach (SetupDefaults::DASH_CARDS as $role => $cards) {
            $picked = $input['dashboard'][$role] ?? [];
            if (!is_array($picked)) {
                $picked = [];
            }
            $dash[$role] = array_values(array_intersect(array_keys($cards), array_map('strval', $picked)));
        }
        $draft['dashboard'] = $dash;
        return $draft;
    }

    /** @param array<string, mixed> $draft */
    private static function patchPermissions(array $draft, array $input): array
    {
        $perms = $draft['permissions'] ?? [];
        foreach (SetupDefaults::ROLES as $role) {
            foreach (array_keys(SetupDefaults::MODULES) as $module) {
                $level = (string) ($input['permissions'][$role][$module] ?? $perms[$role][$module] ?? 'hide');
                if (!in_array($level, ['hide', 'view', 'edit'], true)) {
                    $level = 'hide';
                }
                if ($module === 'dashboard' && $level === 'hide') {
                    $level = 'view';
                }
                $perms[$role][$module] = $level;
            }
        }
        $draft['permissions'] = $perms;
        return $draft;
    }

    /** @param array<string, mixed> $draft */
    private static function assertPublishable(array $draft): void
    {
        $stages = $draft['stages'] ?? [];
        if ($stages === []) {
            throw new RuntimeException('Add at least one deal stage before you publish.');
        }
        $open = 0;
        $lost = 0;
        foreach ($stages as $s) {
            if ((int) ($s['is_lost'] ?? 0) === 1) {
                $lost++;
            } elseif ((int) ($s['is_won'] ?? 0) !== 1) {
                $open++;
            }
        }
        if ($open < 1) {
            throw new RuntimeException('Keep at least one open stage (not won or lost).');
        }
        if ($lost < 1) {
            throw new RuntimeException('Keep one Lost stage so a deal can be marked lost.');
        }
        foreach ($draft['labels'] ?? [] as $val) {
            if (trim((string) $val) === '') {
                throw new RuntimeException('Every name needs a word.');
            }
        }
    }

    /** @param list<array<string, mixed>> $draftStages */
    private static function applyStages(array $draftStages): void
    {
        $tid = self::tenantId();
        $db = Database::instance();
        $pdo = $db->pdo();
        $old = $db->fetchAll(
            'SELECT * FROM pipeline_stages WHERE tenant_id = :tid AND is_active = 1 ORDER BY sort_order, id',
            ['tid' => $tid]
        );
        $pdo->beginTransaction();
        try {
            $db->query('UPDATE pipeline_stages SET is_active = 0 WHERE tenant_id = :tid', ['tid' => $tid]);
            $newIds = [];
            $sort = 1;
            foreach ($draftStages as $s) {
                $db->query(
                    'INSERT INTO pipeline_stages (tenant_id, name, code, sort_order, probability, is_won, is_lost, is_active)
                     VALUES (:tid, :name, :code, :sort, :prob, :won, :lost, 1)',
                    [
                        'tid' => $tid,
                        'name' => mb_substr((string) ($s['name'] ?? 'Stage'), 0, 80),
                        'code' => mb_substr((string) ($s['code'] ?? 'stage'), 0, 40),
                        'sort' => $sort,
                        'prob' => (int) ($s['probability'] ?? 0),
                        'won' => (int) ($s['is_won'] ?? 0),
                        'lost' => (int) ($s['is_lost'] ?? 0),
                    ]
                );
                $newIds[] = [
                    'id' => (int) $db->lastId(),
                    'won' => (int) ($s['is_won'] ?? 0),
                    'lost' => (int) ($s['is_lost'] ?? 0),
                ];
                $sort++;
            }
            $map = self::stageMap($old, $newIds);
            foreach ($map as $oldId => $newId) {
                $db->query(
                    'UPDATE opportunities SET stage_id = :nid WHERE tenant_id = :tid AND stage_id = :oid',
                    ['nid' => $newId, 'tid' => $tid, 'oid' => $oldId]
                );
            }
            $pdo->commit();
        } catch (\Throwable $e) {
            if ($pdo->inTransaction()) {
                $pdo->rollBack();
            }
            throw $e;
        }
    }

    /**
     * @param list<array<string, mixed>> $old
     * @param list<array{id:int,won:int,lost:int}> $new
     * @return array<int, int>
     */
    private static function stageMap(array $old, array $new): array
    {
        return IndustryService::stageMap($old, $new);
    }

    private static function safeKey(string $raw): string
    {
        $key = strtolower(trim($raw));
        $key = preg_replace('/[^a-z0-9_]+/', '_', $key) ?? 'field';
        $key = trim($key, '_');
        if ($key === '' || !preg_match('/^[a-z]/', $key)) {
            $key = 'x_' . $key;
        }
        return mb_substr($key, 0, 40);
    }

    /** @return array<string, string> */
    private static function parseOptions(string $raw): array
    {
        $out = [];
        foreach (preg_split('/[\r\n,]+/', $raw) ?: [] as $line) {
            $line = trim($line);
            if ($line === '') {
                continue;
            }
            if (str_contains($line, '=')) {
                [$val, $label] = array_map('trim', explode('=', $line, 2));
            } else {
                $val = self::safeKey($line);
                $label = $line;
            }
            if ($val !== '') {
                $out[$val] = $label !== '' ? $label : $val;
            }
        }
        return $out;
    }
}
