<?php

declare(strict_types=1);

namespace App\OpenApi;

use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface;
use ApiPlatform\OpenApi\Model;
use ApiPlatform\OpenApi\OpenApi;
use ArrayObject;

final class OpenApiDecorator implements OpenApiFactoryInterface
{
    public function __construct(
        private readonly OpenApiFactoryInterface $decorated,
    ) {}

    public function __invoke(array $context = []): OpenApi
    {
        $openApi = ($this->decorated)($context);

        $this->addHealthCheck($openApi);
        $this->addSessionRoutes($openApi);
        $this->addAdminProfileRoutes($openApi);
        $this->addActivityLogs($openApi);
        $this->addCommentRoutes($openApi);
        $this->addCustomFieldValueRoutes($openApi);
        $this->addDocumentQueryRoutes($openApi);
        $this->addDocumentServeRoutes($openApi);
        $this->addEntityHistoryRoutes($openApi);
        $this->addMachineStructureRoutes($openApi);
        $this->addMachineCustomFieldsRoutes($openApi);
        $this->addModelTypeConversionRoutes($openApi);

        return $this->addTagDescriptions($openApi);
    }

    private function addHealthCheck(OpenApi $openApi): void
    {
        $openApi->getPaths()->addPath('/api/health', new Model\PathItem(
            get: new Model\Operation(
                operationId: 'getHealthCheck',
                tags: ['Monitoring'],
                summary: 'Vérification de santé du système',
                description: 'Retourne le statut du système, la version, la latence BDD et la mémoire utilisée.',
                responses: [
                    '200' => $this->jsonResponse('Système opérationnel.'),
                    '503' => $this->jsonResponse('Système dégradé.'),
                ],
            ),
        ));
    }

    private function addSessionRoutes(OpenApi $openApi): void
    {
        $openApi->getPaths()->addPath('/api/session/profiles', new Model\PathItem(
            get: new Model\Operation(
                operationId: 'getSessionProfiles',
                tags: ['Session'],
                summary: 'Lister les profils disponibles',
                description: 'Retourne les profils actifs (id, prénom, nom, hasPassword). Aucune authentification requise.',
                responses: ['200' => $this->jsonResponse('Liste des profils.')],
            ),
        ));

        $openApi->getPaths()->addPath('/api/session/profile', new Model\PathItem(
            get: new Model\Operation(
                operationId: 'getSessionProfile',
                tags: ['Session'],
                summary: 'Profil actif de la session',
                description: 'Retourne le profil actuellement connecté via la session.',
                responses: [
                    '200' => $this->jsonResponse('Profil actif.'),
                    '401' => $this->jsonResponse('Aucun profil actif.'),
                ],
            ),
            post: new Model\Operation(
                operationId: 'loginSessionProfile',
                tags: ['Session'],
                summary: 'Connexion — activer un profil',
                description: 'Active un profil dans la session. Requiert profileId et password.',
                requestBody: $this->jsonRequestBody('Identifiants de connexion.'),
                responses: [
                    '200' => $this->jsonResponse('Connexion réussie.'),
                    '401' => $this->jsonResponse('Mot de passe incorrect.'),
                    '404' => $this->jsonResponse('Profil introuvable.'),
                ],
            ),
            delete: new Model\Operation(
                operationId: 'logoutSessionProfile',
                tags: ['Session'],
                summary: 'Déconnexion — invalider la session',
                responses: ['200' => $this->jsonResponse('Session invalidée.')],
            ),
        ));
    }

    private function addAdminProfileRoutes(OpenApi $openApi): void
    {
        $openApi->getPaths()->addPath('/api/admin/profiles', new Model\PathItem(
            get: new Model\Operation(
                operationId: 'adminListProfiles',
                tags: ['Admin — Profils'],
                summary: 'Lister tous les profils',
                description: 'Liste complète des profils triés par prénom. Requiert ROLE_ADMIN.',
                responses: ['200' => $this->jsonResponse('Liste des profils.')],
            ),
            post: new Model\Operation(
                operationId: 'adminCreateProfile',
                tags: ['Admin — Profils'],
                summary: 'Créer un profil',
                description: 'Crée un nouveau profil avec rôle. Requiert ROLE_ADMIN.',
                requestBody: $this->jsonRequestBody('Données du profil (firstName, lastName, email, password, role).'),
                responses: [
                    '201' => $this->jsonResponse('Profil créé.'),
                    '400' => $this->jsonResponse('Données invalides.'),
                    '409' => $this->jsonResponse('Email déjà utilisé.'),
                ],
            ),
        ));

        $idParam = $this->pathParam('id', 'Identifiant du profil');

        $openApi->getPaths()->addPath('/api/admin/profiles/{id}/role', new Model\PathItem(
            put: new Model\Operation(
                operationId: 'adminUpdateProfileRole',
                tags: ['Admin — Profils'],
                summary: 'Modifier le rôle d\'un profil',
                description: 'Change le rôle d\'un profil. Empêche la suppression du dernier admin. Requiert ROLE_ADMIN.',
                parameters: [$idParam],
                requestBody: $this->jsonRequestBody('Nouveau rôle.'),
                responses: [
                    '200' => $this->jsonResponse('Rôle mis à jour.'),
                    '400' => $this->jsonResponse('Rôle invalide ou dernier admin.'),
                ],
            ),
        ));

        $openApi->getPaths()->addPath('/api/admin/profiles/{id}/password', new Model\PathItem(
            put: new Model\Operation(
                operationId: 'adminUpdateProfilePassword',
                tags: ['Admin — Profils'],
                summary: 'Modifier le mot de passe d\'un profil',
                description: 'Requiert ROLE_ADMIN.',
                parameters: [$idParam],
                requestBody: $this->jsonRequestBody('Nouveau mot de passe.'),
                responses: ['200' => $this->jsonResponse('Mot de passe mis à jour.')],
            ),
        ));

        $openApi->getPaths()->addPath('/api/admin/profiles/{id}/deactivate', new Model\PathItem(
            put: new Model\Operation(
                operationId: 'adminDeactivateProfile',
                tags: ['Admin — Profils'],
                summary: 'Désactiver un profil',
                description: 'Désactive un profil. Empêche la désactivation du dernier admin. Requiert ROLE_ADMIN.',
                parameters: [$idParam],
                responses: [
                    '200' => $this->jsonResponse('Profil désactivé.'),
                    '400' => $this->jsonResponse('Dernier admin, impossible de désactiver.'),
                ],
            ),
        ));
    }

    private function addActivityLogs(OpenApi $openApi): void
    {
        $openApi->getPaths()->addPath('/api/activity-logs', new Model\PathItem(
            get: new Model\Operation(
                operationId: 'getActivityLogs',
                tags: ['Audit'],
                summary: 'Journal d\'activité paginé',
                description: 'Retourne les logs d\'audit avec filtrage optionnel par type d\'entité et action. Requiert ROLE_VIEWER.',
                parameters: [
                    $this->queryParam('page', 'Numéro de page'),
                    $this->queryParam('itemsPerPage', 'Éléments par page'),
                    $this->queryParam('entityType', 'Filtrer par type d\'entité'),
                    $this->queryParam('action', 'Filtrer par action'),
                ],
                responses: ['200' => $this->jsonResponse('Logs paginés.')],
            ),
        ));
    }

    private function addCommentRoutes(OpenApi $openApi): void
    {
        $openApi->getPaths()->addPath('/api/comments', new Model\PathItem(
            post: new Model\Operation(
                operationId: 'createComment',
                tags: ['Commentaires'],
                summary: 'Créer un commentaire',
                description: 'Ajoute un commentaire à une entité (machine, pièce, composant, produit, catégorie, squelette). Requiert ROLE_VIEWER.',
                requestBody: $this->jsonRequestBody('Données du commentaire (content, entityType, entityId, entityName).'),
                responses: [
                    '201' => $this->jsonResponse('Commentaire créé.'),
                    '400' => $this->jsonResponse('Données invalides.'),
                ],
            ),
        ));

        $openApi->getPaths()->addPath('/api/comments/{id}/resolve', new Model\PathItem(
            patch: new Model\Operation(
                operationId: 'resolveComment',
                tags: ['Commentaires'],
                summary: 'Résoudre un commentaire',
                description: 'Marque un commentaire comme résolu. Requiert ROLE_GESTIONNAIRE.',
                parameters: [$this->pathParam('id', 'Identifiant du commentaire')],
                responses: [
                    '200' => $this->jsonResponse('Commentaire résolu.'),
                    '404' => $this->jsonResponse('Commentaire introuvable.'),
                ],
            ),
        ));

        $openApi->getPaths()->addPath('/api/comments/stats/unresolved-count', new Model\PathItem(
            get: new Model\Operation(
                operationId: 'getUnresolvedCommentCount',
                tags: ['Commentaires'],
                summary: 'Nombre de commentaires non résolus',
                description: 'Requiert ROLE_VIEWER.',
                responses: ['200' => $this->jsonResponse('Compteur.')],
            ),
        ));
    }

    private function addCustomFieldValueRoutes(OpenApi $openApi): void
    {
        $openApi->getPaths()->addPath('/api/custom-fields/values', new Model\PathItem(
            post: new Model\Operation(
                operationId: 'createCustomFieldValue',
                tags: ['Champs personnalisés'],
                summary: 'Créer une valeur de champ personnalisé',
                description: 'Crée une valeur pour un champ personnalisé sur une entité. Auto-crée le champ si nécessaire. Requiert ROLE_GESTIONNAIRE.',
                requestBody: $this->jsonRequestBody('Données (customFieldId/customFieldName, value, entité cible).'),
                responses: [
                    '201' => $this->jsonResponse('Valeur créée.'),
                    '400' => $this->jsonResponse('Données invalides.'),
                ],
            ),
        ));

        $openApi->getPaths()->addPath('/api/custom-fields/values/upsert', new Model\PathItem(
            post: new Model\Operation(
                operationId: 'upsertCustomFieldValue',
                tags: ['Champs personnalisés'],
                summary: 'Créer ou mettre à jour une valeur de champ personnalisé',
                description: 'Requiert ROLE_GESTIONNAIRE.',
                requestBody: $this->jsonRequestBody('Données du champ.'),
                responses: ['200' => $this->jsonResponse('Valeur créée ou mise à jour.')],
            ),
        ));

        $openApi->getPaths()->addPath('/api/custom-fields/values/{entityType}/{entityId}', new Model\PathItem(
            get: new Model\Operation(
                operationId: 'listCustomFieldValues',
                tags: ['Champs personnalisés'],
                summary: 'Lister les valeurs de champs personnalisés d\'une entité',
                description: 'Requiert ROLE_VIEWER.',
                parameters: [
                    $this->pathParam('entityType', 'Type d\'entité (machine, composant, piece, product)'),
                    $this->pathParam('entityId', 'Identifiant de l\'entité'),
                ],
                responses: ['200' => $this->jsonResponse('Liste des valeurs.')],
            ),
        ));

        $idParam = $this->pathParam('id', 'Identifiant de la valeur');

        $openApi->getPaths()->addPath('/api/custom-fields/values/{id}', new Model\PathItem(
            patch: new Model\Operation(
                operationId: 'updateCustomFieldValue',
                tags: ['Champs personnalisés'],
                summary: 'Modifier une valeur de champ personnalisé',
                description: 'Requiert ROLE_GESTIONNAIRE.',
                parameters: [$idParam],
                requestBody: $this->jsonRequestBody('Nouvelle valeur.'),
                responses: ['200' => $this->jsonResponse('Valeur mise à jour.')],
            ),
            delete: new Model\Operation(
                operationId: 'deleteCustomFieldValue',
                tags: ['Champs personnalisés'],
                summary: 'Supprimer une valeur de champ personnalisé',
                description: 'Requiert ROLE_GESTIONNAIRE.',
                parameters: [$idParam],
                responses: ['204' => new Model\Response(description: 'Valeur supprimée.')],
            ),
        ));
    }

    private function addDocumentQueryRoutes(OpenApi $openApi): void
    {
        $entities = [
            'site'      => 'un site',
            'machine'   => 'une machine',
            'composant' => 'un composant',
            'piece'     => 'une pièce',
            'product'   => 'un produit',
        ];

        foreach ($entities as $entity => $label) {
            $openApi->getPaths()->addPath("/api/documents/{$entity}/{id}", new Model\PathItem(
                get: new Model\Operation(
                    operationId: 'getDocumentsBy'.ucfirst($entity),
                    tags: ['Documents'],
                    summary: "Documents rattachés à {$label}",
                    description: 'Requiert ROLE_VIEWER.',
                    parameters: [$this->pathParam('id', "Identifiant de l'entité")],
                    responses: ['200' => $this->jsonResponse('Liste des documents.')],
                ),
            ));
        }
    }

    private function addDocumentServeRoutes(OpenApi $openApi): void
    {
        $idParam = $this->pathParam('id', 'Identifiant du document');

        $openApi->getPaths()->addPath('/api/documents/{id}/file', new Model\PathItem(
            get: new Model\Operation(
                operationId: 'serveDocumentFile',
                tags: ['Documents'],
                summary: 'Afficher un fichier document (inline)',
                description: 'Sert le fichier pour affichage dans le navigateur. Requiert ROLE_VIEWER.',
                parameters: [$idParam],
                responses: ['200' => new Model\Response(description: 'Contenu du fichier.')],
            ),
        ));

        $openApi->getPaths()->addPath('/api/documents/{id}/download', new Model\PathItem(
            get: new Model\Operation(
                operationId: 'downloadDocumentFile',
                tags: ['Documents'],
                summary: 'Télécharger un fichier document',
                description: 'Sert le fichier en téléchargement (attachment). Requiert ROLE_VIEWER.',
                parameters: [$idParam],
                responses: ['200' => new Model\Response(description: 'Fichier en téléchargement.')],
            ),
        ));
    }

    private function addEntityHistoryRoutes(OpenApi $openApi): void
    {
        $entities = [
            'machines'   => 'une machine',
            'pieces'     => 'une pièce',
            'composants' => 'un composant',
            'products'   => 'un produit',
        ];

        foreach ($entities as $plural => $label) {
            $openApi->getPaths()->addPath("/api/{$plural}/{id}/history", new Model\PathItem(
                get: new Model\Operation(
                    operationId: 'get'.ucfirst(rtrim($plural, 's')).'History',
                    tags: ['Audit'],
                    summary: "Historique d'audit de {$label}",
                    description: "Retourne les 200 derniers événements d'audit. Requiert ROLE_VIEWER.",
                    parameters: [$this->pathParam('id', "Identifiant de l'entité")],
                    responses: ['200' => $this->jsonResponse('Historique paginé.')],
                ),
            ));
        }
    }

    private function addMachineStructureRoutes(OpenApi $openApi): void
    {
        $idParam = $this->pathParam('id', 'Identifiant de la machine');

        $openApi->getPaths()->addPath('/api/machines/{id}/structure', new Model\PathItem(
            get: new Model\Operation(
                operationId: 'getMachineStructure',
                tags: ['Machines — Structure'],
                summary: 'Structure complète d\'une machine',
                description: 'Retourne les composants, pièces et produits avec hiérarchie. Requiert ROLE_VIEWER.',
                parameters: [$idParam],
                responses: ['200' => $this->jsonResponse('Structure de la machine.')],
            ),
            patch: new Model\Operation(
                operationId: 'updateMachineStructure',
                tags: ['Machines — Structure'],
                summary: 'Modifier la structure d\'une machine',
                description: 'Crée, met à jour ou supprime les liens composants/pièces/produits. Requiert ROLE_GESTIONNAIRE.',
                parameters: [$idParam],
                requestBody: $this->jsonRequestBody('Liens à créer/modifier/supprimer.'),
                responses: ['200' => $this->jsonResponse('Structure mise à jour.')],
            ),
        ));

        $openApi->getPaths()->addPath('/api/machines/{id}/clone', new Model\PathItem(
            post: new Model\Operation(
                operationId: 'cloneMachine',
                tags: ['Machines — Structure'],
                summary: 'Cloner une machine',
                description: 'Clone une machine avec tous ses composants, pièces, produits, champs personnalisés et constructeurs. Requiert ROLE_GESTIONNAIRE.',
                parameters: [$idParam],
                requestBody: $this->jsonRequestBody('Données de la copie (name, siteId, reference).'),
                responses: [
                    '201' => $this->jsonResponse('Machine clonée.'),
                    '404' => $this->jsonResponse('Machine source introuvable.'),
                ],
            ),
        ));
    }

    private function addMachineCustomFieldsRoutes(OpenApi $openApi): void
    {
        $openApi->getPaths()->addPath('/api/machines/{id}/add-custom-fields', new Model\PathItem(
            post: new Model\Operation(
                operationId: 'addMachineCustomFields',
                tags: ['Machines — Structure'],
                summary: 'Initialiser les champs personnalisés manquants',
                description: 'Crée les entrées de valeur manquantes pour les champs personnalisés définis. Requiert ROLE_GESTIONNAIRE.',
                parameters: [$this->pathParam('id', 'Identifiant de la machine')],
                responses: ['200' => $this->jsonResponse('Champs ajoutés.')],
            ),
        ));
    }

    private function addModelTypeConversionRoutes(OpenApi $openApi): void
    {
        $idParam = $this->pathParam('id', 'Identifiant du ModelType');

        $openApi->getPaths()->addPath('/api/model_types/{id}/conversion-check', new Model\PathItem(
            get: new Model\Operation(
                operationId: 'checkModelTypeConversion',
                tags: ['ModelType'],
                summary: 'Vérifier la convertibilité d\'un ModelType',
                description: 'Vérifie si la catégorie du ModelType peut être convertie. Requiert ROLE_VIEWER.',
                parameters: [$idParam],
                responses: ['200' => $this->jsonResponse('Résultat de la vérification.')],
            ),
        ));

        $openApi->getPaths()->addPath('/api/model_types/{id}/convert', new Model\PathItem(
            post: new Model\Operation(
                operationId: 'convertModelType',
                tags: ['ModelType'],
                summary: 'Convertir la catégorie d\'un ModelType',
                description: 'Convertit la catégorie. Retourne 409 en cas de conflit. Requiert ROLE_GESTIONNAIRE.',
                parameters: [$idParam],
                responses: [
                    '200' => $this->jsonResponse('Conversion effectuée.'),
                    '409' => $this->jsonResponse('Conflit — conversion impossible.'),
                ],
            ),
        ));
    }

    private function addTagDescriptions(OpenApi $openApi): OpenApi
    {
        $customTags = [
            'Monitoring'           => 'Supervision et vérification de l\'état de santé du système (statut, version, latence BDD, mémoire).',
            'Session'              => 'Authentification par session. Permet de lister les profils disponibles, se connecter (activer un profil) et se déconnecter.',
            'Admin — Profils'      => 'Administration des profils utilisateurs. Création, modification des rôles et mots de passe, désactivation. Réservé aux administrateurs.',
            'Audit'                => 'Journal d\'activité et historique d\'audit. Consultation des modifications apportées aux entités avec détail des changements (diff et snapshot).',
            'Commentaires'         => 'Système de commentaires et annotations sur les entités. Permet de créer des commentaires, les résoudre et suivre le nombre de commentaires ouverts.',
            'Champs personnalisés' => 'Gestion des valeurs de champs personnalisés. Permet d\'ajouter, modifier et supprimer des données dynamiques sur les machines, pièces, composants et produits.',
            'Documents'            => 'Gestion des fichiers joints. Consultation des documents par entité, affichage inline et téléchargement.',
            'Machines — Structure' => 'Structure hiérarchique des machines. Consultation et modification des liaisons composants/pièces/produits, clonage de machines et initialisation des champs personnalisés.',
            'ModelType'            => 'Conversion de catégories de types. Vérification de compatibilité et conversion effective des catégories de ModelType.',
        ];

        $existingTags  = $openApi->getTags();
        $existingNames = array_map(static fn (Model\Tag $tag) => $tag->getName(), $existingTags);

        foreach ($customTags as $name => $description) {
            if (!in_array($name, $existingNames, true)) {
                $existingTags[] = new Model\Tag(name: $name, description: $description);
            }
        }

        return $openApi->withTags($existingTags);
    }

    private function jsonResponse(string $description): Model\Response
    {
        return new Model\Response(
            description: $description,
            content: new ArrayObject([
                'application/json' => new Model\MediaType(
                    schema: new ArrayObject(['type' => 'object']),
                ),
            ]),
        );
    }

    private function jsonRequestBody(string $description): Model\RequestBody
    {
        return new Model\RequestBody(
            description: $description,
            content: new ArrayObject([
                'application/json' => new Model\MediaType(
                    schema: new ArrayObject(['type' => 'object']),
                ),
            ]),
            required: true,
        );
    }

    private function pathParam(string $name, string $description): Model\Parameter
    {
        return new Model\Parameter(
            name: $name,
            in: 'path',
            description: $description,
            required: true,
            schema: ['type' => 'string'],
        );
    }

    private function queryParam(string $name, string $description): Model\Parameter
    {
        return new Model\Parameter(
            name: $name,
            in: 'query',
            description: $description,
            required: false,
            schema: ['type' => 'string'],
        );
    }
}
