<?php

declare(strict_types=1);

final class MicElementRegistry
{
    /** @var array<string, MicElementResolverInterface> */
    private array $resolvers = [];

    /** @var array<string, ?string> */
    private array $identityCache = [];

    public function registerCore(string $slug, MicElementResolverInterface $resolver): void
    {
        $slug = $this->normalizeSlug($slug);
        if ($slug === '' || str_starts_with($slug, 'x.')) {
            throw new InvalidArgumentException('Core mic slug must be non-empty and must not use the x.* namespace.');
        }
        $this->register($slug, $resolver);
    }

    public function registerExtension(string $vendor, string $slug, MicElementResolverInterface $resolver): void
    {
        $vendor = $this->normalizeVendor($vendor);
        $slug = $this->normalizeSlug($slug);
        if ($vendor === '' || $slug === '') {
            throw new InvalidArgumentException('Extension mic vendor and slug are required.');
        }
        if (str_starts_with($slug, 'x.')) {
            throw new InvalidArgumentException('Extension mic slug must be the local slug, not the fully-qualified x.* slug.');
        }
        $this->register('x.' . $vendor . '.' . $slug, $resolver);
    }

    /**
     * @param array<string, mixed> $context
     */
    public function resolve(string $type, string $id, array $context): ?string
    {
        $type = $this->normalizeSlug($type);
        $id = trim($id);
        if ($type === '' || $id === '' || !isset($this->resolvers[$type])) {
            return null;
        }

        $key = $type . '|' . $id;
        if (array_key_exists($key, $this->identityCache)) {
            return $this->identityCache[$key];
        }

        $resolved = $this->resolvers[$type]->resolve($id, $context);
        $this->identityCache[$key] = is_string($resolved) && $resolved !== '' ? $resolved : null;
        return $this->identityCache[$key];
    }

    public function resetIdentityCache(): void
    {
        $this->identityCache = [];
    }

    public function resolverFor(string $type): ?MicElementResolverInterface
    {
        try {
            $normalized = $this->normalizeSlug($type);
        } catch (InvalidArgumentException) {
            return null;
        }
        return $this->resolvers[$normalized] ?? null;
    }

    /**
     * @return array<string, array<string, mixed>>
     */
    public function knownTypes(): array
    {
        $types = [];
        foreach ($this->resolvers as $slug => $resolver) {
            $descriptor = $this->normalizeDescriptor($resolver->describe(), $slug);
            $types[$slug] = $descriptor;
        }
        ksort($types);
        return $types;
    }

    private function register(string $slug, MicElementResolverInterface $resolver): void
    {
        if (isset($this->resolvers[$slug])) {
            throw new InvalidArgumentException('Mic element type is already registered: ' . $slug . '.');
        }

        $descriptor = $this->normalizeDescriptor($resolver->describe(), $slug);
        if ((string) $descriptor['slug'] !== $slug) {
            throw new InvalidArgumentException('Mic resolver descriptor slug must match the registered slug.');
        }
        if ($descriptor['bankSurface'] === 'picker' && trim((string) ($descriptor['parentSurfaceUrl'] ?? '')) === '') {
            throw new InvalidArgumentException('Picker mic types require parentSurfaceUrl.');
        }

        $this->resolvers[$slug] = $resolver;
    }

    /**
     * @param array<string, mixed> $descriptor
     * @return array<string, mixed>
     */
    private function normalizeDescriptor(array $descriptor, string $fallbackSlug): array
    {
        $surface = (string) ($descriptor['bankSurface'] ?? 'none');
        if (!in_array($surface, ['editor', 'picker', 'none'], true)) {
            throw new InvalidArgumentException('Mic bankSurface must be editor, picker, or none.');
        }

        return [
            'slug' => $this->normalizeSlug((string) ($descriptor['slug'] ?? $fallbackSlug)),
            'label' => trim((string) ($descriptor['label'] ?? $fallbackSlug)),
            'description' => trim((string) ($descriptor['description'] ?? '')),
            'icon' => trim((string) ($descriptor['icon'] ?? '')),
            'scopes' => array_values(array_map('strval', is_array($descriptor['scopes'] ?? null) ? $descriptor['scopes'] : [])),
            'bankSurface' => $surface,
            'parentSurfaceUrl' => trim((string) ($descriptor['parentSurfaceUrl'] ?? '')),
            'adminUrl' => trim((string) ($descriptor['adminUrl'] ?? '')),
        ];
    }

    private function normalizeSlug(string $slug): string
    {
        $slug = strtolower(trim($slug));
        if ($slug === '') {
            return '';
        }
        if (!preg_match('/^(?:x\.[a-z][a-z0-9_]*\.)?[a-z][a-z0-9_-]*$/', $slug)) {
            throw new InvalidArgumentException('Mic slug must be lowercase and namespaced extensions must use x.{vendor}.{slug}.');
        }
        return $slug;
    }

    private function normalizeVendor(string $vendor): string
    {
        $vendor = strtolower(trim($vendor));
        if ($vendor === '' || !preg_match('/^[a-z][a-z0-9_]*$/', $vendor)) {
            throw new InvalidArgumentException('Mic extension vendor must be snake_case.');
        }
        return $vendor;
    }
}

function myronMicElementRegistry(): MicElementRegistry
{
    static $registry = null;
    if ($registry instanceof MicElementRegistry) {
        return $registry;
    }
    $registry = new MicElementRegistry();
    return $registry;
}

