<?php

namespace App\NotificationMaps;

use App\Models\NotificationMap;

/**
 * Builds an OpenAPI 3.0 document describing a single notification map's inbound
 * endpoint, derived from its field_map (request body shape) and validation rules
 * (required fields). The body schema is keyed by source path, since that is what
 * external callers actually send.
 */
class NotificationOpenApiGenerator
{
    public function generate(NotificationMap $map): array
    {
        $path = '/api/notifications/{tenant}/' . $map->slug;

        return [
            'openapi' => '3.0.0',
            'info' => [
                'title' => $map->name . ' notification',
                'description' => 'Inbound stock-update notification endpoint for "' . $map->name . '".',
                'version' => '1.0.0',
            ],
            'paths' => [
                $path => [
                    'post' => [
                        'summary' => 'Submit a ' . $map->name . ' notification',
                        'security' => [['bearerAuth' => []]],
                        'parameters' => [
                            [
                                'name' => 'tenant',
                                'in' => 'path',
                                'required' => true,
                                'description' => 'Tenant identifier.',
                                'schema' => ['type' => 'string'],
                            ],
                        ],
                        'requestBody' => [
                            'required' => true,
                            'content' => [
                                'application/json' => [
                                    'schema' => $this->requestSchema($map),
                                ],
                            ],
                        ],
                        'responses' => [
                            '200' => ['description' => 'Notification received.'],
                            '401' => ['description' => 'Missing or invalid API token.'],
                            '404' => ['description' => 'Unknown or inactive notification.'],
                            '422' => ['description' => 'Payload failed validation.'],
                        ],
                    ],
                ],
            ],
            'components' => [
                'securitySchemes' => [
                    'bearerAuth' => [
                        'type' => 'http',
                        'scheme' => 'bearer',
                    ],
                ],
            ],
        ];
    }

    /**
     * Build the request body schema from the field map, nesting dot-notation source
     * paths into object properties and marking required fields.
     */
    private function requestSchema(NotificationMap $map): array
    {
        $schema = ['type' => 'object', 'properties' => []];
        $config = $map->config ?? [];
        $required = $this->requiredSources($map);

        foreach (($map->field_map ?? []) as $field) {
            $source = $field['source'] ?? null;
            if (empty($source)) {
                continue;
            }

            $leaf = $this->leafSchema($field, $config);
            $this->setNested($schema, explode('.', $source), $leaf);
        }

        $rootRequired = array_values(array_filter(
            $required,
            fn ($source) => !str_contains($source, '.')
        ));

        if (!empty($rootRequired)) {
            $schema['required'] = $rootRequired;
        }

        return $schema;
    }

    /**
     * Source paths whose mapped destination carries a "required" validation rule.
     *
     * @return array<int, string>
     */
    private function requiredSources(NotificationMap $map): array
    {
        $validation = $map->validation ?? [];

        $requiredDestinations = [];
        foreach ($validation as $destination => $rules) {
            if (is_array($rules) && in_array('required', $rules, true)) {
                $requiredDestinations[] = $destination;
            }
        }

        return collect($map->field_map ?? [])
            ->filter(fn ($field) => in_array($field['destination'] ?? null, $requiredDestinations, true))
            ->pluck('source')
            ->filter()
            ->values()
            ->all();
    }

    /**
     * OpenAPI schema for a single field, by its configured type.
     *
     * @param array<string, mixed> $field
     * @param array<string, mixed> $config
     * @return array<string, mixed>
     */
    private function leafSchema(array $field, array $config): array
    {
        $type = $field['type'] ?? NotificationPayloadMapper::TYPE_STRING;
        $destination = $field['destination'] ?? null;

        return match ($type) {
            NotificationPayloadMapper::TYPE_INTEGER => ['type' => 'integer'],
            NotificationPayloadMapper::TYPE_FLOAT => ['type' => 'number'],
            NotificationPayloadMapper::TYPE_BOOLEAN => ['type' => 'boolean'],
            NotificationPayloadMapper::TYPE_DATE => ['type' => 'string', 'format' => 'date-time'],
            NotificationPayloadMapper::TYPE_IMAGE_LIST => $this->imageListSchema($destination, $config),
            NotificationPayloadMapper::TYPE_EQUIPMENT => $this->specListSchema(
                ['type', 'description', 'category', 'code', 'price']
            ),
            NotificationPayloadMapper::TYPE_TECHNICAL_DATA => $this->specListSchema(
                ['description', 'value', 'category', 'code']
            ),
            // taxonomy and dealership are resolved from a name string.
            default => ['type' => 'string'],
        };
    }

    /**
     * Schema for a spec-style list (equipment / technical data): an array of objects
     * limited to the given string properties. "description" is always required.
     *
     * @param array<int, string> $keys
     * @return array<string, mixed>
     */
    private function specListSchema(array $keys): array
    {
        $properties = [];
        foreach ($keys as $key) {
            $properties[$key] = ['type' => 'string'];
        }

        return [
            'type' => 'array',
            'items' => [
                'type' => 'object',
                'required' => ['description'],
                'properties' => $properties,
            ],
        ];
    }

    /**
     * Images may be plain URL strings or objects exposing the URL under the
     * configured url_key (default "url").
     *
     * @param array<string, mixed> $config
     * @return array<string, mixed>
     */
    private function imageListSchema(?string $destination, array $config): array
    {
        $urlKey = $config[$destination]['url_key'] ?? 'url';

        return [
            'type' => 'array',
            'items' => [
                'oneOf' => [
                    ['type' => 'string', 'format' => 'uri'],
                    [
                        'type' => 'object',
                        'properties' => [
                            $urlKey => ['type' => 'string', 'format' => 'uri'],
                        ],
                    ],
                ],
            ],
        ];
    }

    /**
     * Place a leaf schema at a dot-notation path within an object schema, creating
     * intermediate object nodes as needed.
     *
     * @param array<string, mixed> $schema
     * @param array<int, string> $segments
     * @param array<string, mixed> $leaf
     */
    private function setNested(array &$schema, array $segments, array $leaf): void
    {
        $key = array_shift($segments);

        if (empty($segments)) {
            $schema['properties'][$key] = $leaf;
            return;
        }

        if (!isset($schema['properties'][$key]) || ($schema['properties'][$key]['type'] ?? null) !== 'object') {
            $schema['properties'][$key] = ['type' => 'object', 'properties' => []];
        }

        $this->setNested($schema['properties'][$key], $segments, $leaf);
    }
}
