diff --git a/config/packages/nelmio_api_doc.yaml b/config/packages/nelmio_api_doc.yaml
index 70280858833d07d19b32a9ada23812dc09b650aa..9ec5a78abe3817d82ac01e5bbf2e00418e1ff713 100644
--- a/config/packages/nelmio_api_doc.yaml
+++ b/config/packages/nelmio_api_doc.yaml
@@ -12,6 +12,9 @@ nelmio_api_doc:
                     bearerFormat: JWT
         security:
             - OAuth2: []  # On répète le Nom arbitraire, et il faut l'utiliser dans les controllers @Security(name="OAuth2")
+#        definitions:
+#            entry:
+#                $ref: ../JsonSchema/entrySchema.json
     areas: # to filter documented areas
         path_patterns:
             - ^/api(?!/doc$) # Accepts routes under /api except /api/doc
diff --git a/src/Controller/ApiBaseController.php b/src/Controller/ApiBaseController.php
index 123bd74b9e7eeb7b72a11091067a43e70a310543..d5afdd9e73c7da99f5f8723415b03864000d6133 100644
--- a/src/Controller/ApiBaseController.php
+++ b/src/Controller/ApiBaseController.php
@@ -293,6 +293,33 @@ class ApiBaseController extends AbstractController
         return $entry;
     }
 
+    /**
+     * - On ajoute les commentaires de l'entrée source à la suite de ceux de l'entrée cible
+     * - On ajoute les items de l'entrée source à la suite de ceux de l'entrée cible
+     * - On ajoute le label 'merged' au headword commun aux deux entrées
+     *
+     * @param Entry $targetEntry
+     * @param Entry $sourceEntry
+     * @return Entry
+     */
+    public function mergeEntries(Entry $targetEntry, Entry $sourceEntry)
+    {
+        $targetEntry->setComments($targetEntry->getComments() . "\n\nCommentaires ajoutés suite à une fusion:\n" . $sourceEntry->getComments());
+
+        $sourceAttributes = $sourceEntry->getAttributes();
+        $targetAttributes = $targetEntry->getAttributes();
+        $targetAttributes['Items'] = array_merge_recursive($targetAttributes['Items'], $sourceAttributes['Items']);
+        $targetEntry->setAttributes($targetAttributes);
+
+        $mergedLabel = $this->doctrine->getRepository(Label::class)->findOneBy([
+            'category' => Label::LABEL_CATEGORY_SYSTEM,
+            'name' => Label::LABEL_MERGED,
+        ]);
+        $targetEntry->getHeadword()->addLabel($mergedLabel);
+
+        return $targetEntry;
+    }
+
     /**
      * @param Headword $headword
      * @return Entry|null
diff --git a/src/Controller/ApiEntryController.php b/src/Controller/ApiEntryController.php
index 7db43ba1bba0170e7924e77590f389f745cffe24..7c7ac61952947c6919044723bf0189ff18d636cc 100644
--- a/src/Controller/ApiEntryController.php
+++ b/src/Controller/ApiEntryController.php
@@ -9,6 +9,7 @@ use App\Entity\Lexicon;
 use App\Entity\User;
 use App\Manager\WiktionaryManager;
 use Doctrine\Persistence\ManagerRegistry;
+use JsonSchema\Validator;
 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
 use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\HttpFoundation\Request;
@@ -61,7 +62,7 @@ class ApiEntryController extends ApiBaseController
     /**
      * Recherche d'une entrée par graphie et par langue, dans un ensemble de lexiques
      *
-     * @Route("/search", name="search", methods={"POST"})
+     * @Route("/search", name="api_search", methods={"POST"})
      *
      * @OA\Response(response=200, description="success",
      *      @OA\JsonContent(type="array",
@@ -115,7 +116,7 @@ class ApiEntryController extends ApiBaseController
     /**
      * Crée une entrée dans les lexiques spécifiés à partir d'un mot. Si force=true, on crée  l'entrée même si le mot n'est pas trouvé dans le wiktionnaire
      *
-     * @Route("/create", name="create_entry", methods={"POST"})
+     * @Route("/create", name="api_create_entry", methods={"POST"})
      *
      * @OA\Response(response=200, description="success", @OA\JsonContent(type="string"))
      * @OA\Response(response=401, description="error", @OA\JsonContent(type="string"))
@@ -184,7 +185,7 @@ class ApiEntryController extends ApiBaseController
     }
 
     /**
-     * Copie une sélection d'entrée vers des lexiques cibles. Copie également les labels spécifiés à partir d'un mot. Si force=true, on crée  l'entrée même si le mot n'est pas trouvé dans le wiktionnaire
+     * Copie une sélection d'entrée vers des lexiques cibles. Si force=true, on crée  l'entrée même si le mot n'est pas trouvé dans le wiktionnaire
      *
      * @Route("/copy", name="api_copy_entries", methods={"POST"})
      *
@@ -202,7 +203,7 @@ class ApiEntryController extends ApiBaseController
      *         @OA\Items(
      *           type="integer"
      *         )
-     *       )
+     *       ),
      *       @OA\Property(property="entries", type="array",
      *         example={1, 2},
      *         @OA\Items(
@@ -233,7 +234,7 @@ class ApiEntryController extends ApiBaseController
         if (!in_array($data['merge'], Lexicon::MERGE_MODES)) {
             return $this->createJsonResponse(401, ['error' => sprintf("Le mode de fusion « merge » doit être overwrite OU ignore OU merge")]);
         }
-        $entries = $this->doctrine->getRepository(Entry::class)->find($data['entries']);
+        $entries = $this->doctrine->getRepository(Entry::class)->findById($data['entries']);
         if (!$entries) {
             return $this->createJsonResponse(401, ['error' => sprintf("Aucune entrée trouvée")]);
         }
@@ -241,32 +242,71 @@ class ApiEntryController extends ApiBaseController
         if (!$lexicons) {
             return $this->createJsonResponse(401);
         }
-
-        // On récupère ou on crée le mot-vedette
-        if (!$headword) {
-            if ($forceCreation || $wiktionaryManager->search($data['graphy'], $language)) {
-                $headword = $this->newHeadword($data['graphy'], $language);
-            } else {
-                return $this->createJsonResponse(401, ['warning' => sprintf("Le mot «%s» n'existe ni dans Balex ni dans le wiktionnaire.", $data['graphy'])]);
-            }
+        $language = $this->getLexiconsLanguage($lexicons);
+        if (!$language) {
+            return $this->createJsonResponse(401, ['error' => sprintf("Les lexiques cibles doivent tous appartenir à la même langue")]);
         }
 
-        // On crée l'entrée dans chaque lexique si elle n'y existe pas (et dans le lexique Zéro si possible)
+        $added = [];
+        $overwritten = [];
+        $ignored = [];
+        $merged = [];
+        // On crée l'entrée dans chaque lexique si elle n'y existe pas
         foreach ($lexicons as $lexicon) {
-            if (!$this->doctrine->getRepository(Entry::class)->findBy(['lexicon' => $lexicon, 'headword' => $headword])) {
-                $entry = $this->createEntryInLexicon($headword, $lexicon);
-                $this->success[] = sprintf("Entrée créée dans le lexique : %s.", $lexicon);
-            } else {
-                $this->warning[] = sprintf("Cette entrée est déjà présente dans le lexique %s.", $lexicon);
+
+            foreach ($entries as $entry) {
+
+                if ($language !== $entry->getLanguage()) {
+                    return $this->createJsonResponse(401, ['error' => sprintf("L'entrée %s n'est pas dans la même langue que les lexiques cibles", $entry->getId())]);
+                }
+
+                $targetEntry = $this->doctrine->getRepository(Entry::class)->findOneBy([
+                    'headword' => $entry->getHeadword(),
+                    'lexicon'  => $lexicon,
+                ]);
+
+                // Si l'entrée existe déjà
+                if ($targetEntry) {
+                    switch ($data['merge']) {
+                        case Lexicon::MERGE_OVERWRITE: // On remplace l'entrée cible si elle existe
+                            $targetEntry->setAttributes($entry->getAttributes());
+                            $targetEntry->setComments($entry->getComments());
+                            $overwritten[] = $targetEntry->getId();
+                            break;
+                        case Lexicon::MERGE_IGNORE: // On ne fait rien si l'entrée cible existe
+                            $ignored[] = $targetEntry->getId();
+                            break;
+                        case Lexicon::MERGE_MERGE:  // On fusionne les 2 entrées
+                            $targetEntry = $this->mergeEntries($targetEntry, $entry);
+                            $merged[] = $targetEntry->getId();
+                            break;
+                    }
+                } else {
+                    $newEntry = clone($entry);
+                    $newEntry->setLexicon($lexicon);
+                    $newEntry->setCreatedAt(new \DateTimeImmutable());
+                    $newEntry->setUpdatedAt(null);
+                    $this->doctrine->getManager()->persist($newEntry);
+                    $this->doctrine->getManager()->flush();
+                    $added[] = $newEntry->getId();
+                }
+
             }
         }
         $this->doctrine->getManager()->flush();
 
-        return $this->createJsonResponse(200);
+        $result = array_filter([
+            'added' => $added,
+            'overwritten' => $overwritten,
+            'ignored' => $ignored,
+            'merged' => $merged,
+        ]);
+
+        return $this->createJsonResponse(200, $result);
     }
 
     /**
-     * @Route("/get/{id}", name="get_entry", methods={"GET"})
+     * @Route("/get/{id}", name="api_get_entry", methods={"GET"})
      *
      * @OA\Response(
      *     response=200,
@@ -285,6 +325,7 @@ class ApiEntryController extends ApiBaseController
      *     description="id of the entry to be returned",
      *     @OA\Schema(type="string")
      * )
+     *
      * @OA\Tag(name="Entries")
      * @Security(name="OAuth2")
      */
@@ -300,7 +341,72 @@ class ApiEntryController extends ApiBaseController
     }
 
     /**
-     * @Route("/remove", name="entry_delete", methods={"DELETE"})
+     * @Route("/edit/{id}", name="api_edit_entry", methods={"POST"})
+     *
+     * @OA\Response(
+     *     response=200,
+     *     description="Success",
+     *     @OA\JsonContent(
+     *        ref=@Model(type=Entry::class, groups={"entry:read"})
+     *     )
+     * )
+     * @OA\Response(response=401, description="error", @OA\JsonContent(type="string"))
+     * @OA\Response(response=403, description="error", @OA\JsonContent(type="string"))
+     * @OA\Response(response=500, description="error", @OA\JsonContent(type="string"))
+     *
+     * @OA\Parameter(
+     *     name="id",
+     *     in="path",
+     *     description="id of the entry to edit",
+     *     @OA\Schema(type="string")
+     * )
+     * @OA\RequestBody(
+     *     required=true,
+     *     @OA\JsonContent(
+     *       required={"attributes"},
+     *       @OA\Property(property="comment", type="string", example="commentaire optionnel ajouté à la suite des commentaires existants"),
+     *       @OA\Property(property="attributes", type="object")
+     *     )
+     * )
+     * @OA\Tag(name="Entries")
+     * @Security(name="OAuth2")
+     */
+    public function editEntry(Request $request, SerializerInterface $serializer, Entry $entry = null)
+    {
+        if ($entry === null) {
+            return $this->createJsonResponse(401, ['error' => sprintf("Pas d'entrée trouvée pour cette Id")]);
+        }
+        $data = json_decode($request->getContent(), true);
+        if (!$data) {
+            return $this->createJsonResponse(401, ['error' => sprintf("Json non valide")]);
+        }
+        if ($missingFields = $this->getMissingFields($data, ['attributes'])) {
+            return $this->createJsonResponse(401, ['error' => sprintf("Veuillez fournir une valeur pour: %s", implode(', ', $missingFields))]);
+        }
+        $attributes = json_decode(json_encode($data['attributes']));
+        $validator = new Validator();
+        $validator->validate($attributes, (object) ['$ref' => __DIR__ . "/../JsonSchema/entrySchema.json"]);
+        if (!$validator->isValid()) {
+            $message = "JSON does not validate. Violations:\n";
+            foreach ($validator->getErrors() as $error) {
+                $message .= sprintf("[%s] %s\n", $error['property'], $error['message']);
+            }
+            return $this->createJsonResponse(401, ['error' => $message]);
+        } else {
+            $entry->setAttributes($data['attributes']);
+            if ($data['comment'] ?? null) {
+                $entry->setComments($entry->getComments() . "\n" . $data['comment']);
+            }
+            $this->doctrine->getManager()->flush();
+        }
+
+        $updatedEntryData = $serializer->serialize($entry, 'json', ['groups' => ["entry:read", "headword:read", "lexicon:read"]]);
+
+        return new JsonResponse($updatedEntryData, 200, [], true);
+    }
+
+    /**
+     * @Route("/remove", name="api_entry_delete", methods={"DELETE"})
      *
      * @OA\Response(
      *     response=200,
diff --git a/src/Controller/LexiconController.php b/src/Controller/LexiconController.php
index 4e907e8ec0de1d14cafb28bc58e5e408f4a26bf8..e6e544f320e36c4804d437e6b777cc5da50d4651 100644
--- a/src/Controller/LexiconController.php
+++ b/src/Controller/LexiconController.php
@@ -2,6 +2,7 @@
 
 namespace App\Controller;
 
+use App\Entity\Entry;
 use App\Entity\Lexicon;
 use App\Repository\LexiconRepository;
 use Doctrine\Persistence\ManagerRegistry;
@@ -22,6 +23,12 @@ class LexiconController extends AbstractController
      */
     public function index(LexiconRepository $lexiconRepository): Response
     {
+//        $entry1 = $this->getDoctrine()->getRepository(Entry::class)->find(1);
+//        $entry2 = $this->getDoctrine()->getRepository(Entry::class)->find(9);
+//
+//
+//        dump($entry1->getAttributes(), $entry2->getAttributes(), array_merge_recursive($entry1->getAttributes()['Items'], $entry2->getAttributes()['Items']));die();
+
         return $this->render('lexicon/index.html.twig', [
             'lexicons' => $lexiconRepository->findAll(),
         ]);
diff --git a/src/Entity/Entry.php b/src/Entity/Entry.php
index 33e80d4b47c73d8dddcfd06ea99173d70adc315d..dcd073ae7a80b52be445343e6633d1adb7ea7934 100644
--- a/src/Entity/Entry.php
+++ b/src/Entity/Entry.php
@@ -34,6 +34,7 @@ class Entry
     private $attributes = [];
 
     /**
+     * @Groups({"entry:read"})
      * @ORM\Column(type="text", nullable=true)
      */
     private $comments;
@@ -68,12 +69,6 @@ class Entry
      */
     private $lexicon;
 
-    /**
-     * @Groups({"entry:write"})
-     * @var Lexicon[]
-     */
-    private $lexicons;
-
     public function __construct()
     {
     }
@@ -195,20 +190,4 @@ class Entry
 
         return $this;
     }
-
-    /**
-     * @return Lexicon[]
-     */
-    public function getLexicons(): array
-    {
-        return $this->lexicons;
-    }
-
-    /**
-     * @param Lexicon[] $lexicons
-     */
-    public function setLexicons(array $lexicons): void
-    {
-        $this->lexicons = $lexicons;
-    }
 }
diff --git a/src/Security/Voter/LexiconVoter.php b/src/Security/Voter/LexiconVoter.php
index f60c47b594c0893da5d3a881f9fe6be790bed76b..520dd6bc121bce455e70975e9fac0abe3ca281b8 100644
--- a/src/Security/Voter/LexiconVoter.php
+++ b/src/Security/Voter/LexiconVoter.php
@@ -59,7 +59,7 @@ class LexiconVoter extends Voter
             return true;
         }
 
-        if (in_array($user, $lexicon->getGroup()->getMembers())) {
+        if ($lexicon->getGroup() && in_array($user, $lexicon->getGroup()->getMembers())) {
             return true;
         }
 
@@ -77,7 +77,7 @@ class LexiconVoter extends Voter
             return true;
         }
 
-        if (in_array($user, $lexicon->getGroup()->getMembers())) {
+        if ($lexicon->getGroup() && in_array($user, $lexicon->getGroup()->getMembers())) {
             return true;
         }