{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- | A way to synchronise a single item with safe merge conflicts.
--
-- The setup is as follows:
--
-- * A central server is set up to synchronise with
-- * Each client synchronises with the central server, but never with eachother
--
--
--
-- = A client should operate as follows:
--
-- The client starts with an 'initialClientStore'.
--
-- * The client produces a 'SyncRequest' with 'makeSyncRequest'.
-- * The client sends that request to the central server and gets a 'SyncResponse'.
-- * The client then updates its local store with 'mergeSyncResponseIgnoreProblems'.
--
--
-- = The central server should operate as follows:
--
-- The server starts with an 'initialServerStore'.
--
-- * The server accepts a 'SyncRequest'.
-- * The server performs operations according to the functionality of 'processServerSync' or 'processServerSyncCustom'.
-- * The server respons with a 'SyncResponse'.
--
--
-- WARNING:
-- This whole approach can break down if a server resets its server times
-- or if a client syncs with two different servers using the same server times.
module Data.Mergeful.Collection
  ( -- * Client side
    ClientStore (..),
    Timed (..),
    ServerTime (..),
    initialClientStore,

    -- ** Querying the client store
    clientStoreSize,
    clientStoreClientIdSet,
    clientStoreUndeletedSyncIdSet,
    clientStoreSyncIdSet,
    clientStoreItems,

    -- ** Changing the client store
    addItemToClientStore,
    findFreeSpot,
    markItemDeletedInClientStore,
    changeItemInClientStore,
    deleteItemFromClientStore,

    -- ** Maxing a sync request
    SyncRequest (..),
    initialSyncRequest,
    makeSyncRequest,

    -- ** Merging the response
    SyncResponse (..),
    ClientAddition (..),
    ItemMergeStrategy (..),
    ChangeConflictResolution (..),
    ClientDeletedConflictResolution (..),
    ServerDeletedConflictResolution (..),
    mergeFromServerStrategy,
    mergeFromClientStrategy,
    mergeUsingCRDTStrategy,
    mergeSyncResponseFromServer,
    mergeSyncResponseFromClient,
    mergeSyncResponseUsingCRDT,
    mergeSyncResponseUsingStrategy,
    ClientSyncProcessor (..),
    mergeSyncResponseCustom,

    -- *** Utility functions for implementing pure client-side merging
    ClientId (..),
    mergeAddedItems,
    mergeSyncedButChangedItems,
    mergeDeletedItems,

    -- *** Utility functions for implementing custom client-side merging
    mergeSyncedButChangedConflicts,
    mergeClientDeletedConflicts,
    mergeServerDeletedConflicts,

    -- * Server side

    -- ** The store
    ServerStore (..),
    initialServerStore,

    -- ** Processing a sync request
    processServerSync,
    ServerSyncProcessor (..),
    processServerSyncCustom,
    emptySyncResponse,
    initialServerTime,
    incrementServerTime,
  )
where

import Control.Applicative
import Control.DeepSeq
import Control.Monad
import Control.Monad.State
import Data.Aeson
import Data.List
import Data.Map (Map)
import qualified Data.Map as M
import Data.Maybe
import Data.Mergeful.Item
import Data.Mergeful.Timed
import Data.Set (Set)
import qualified Data.Set as S
import Data.Text (Text)
import Data.Validity
import Data.Validity.Containers ()
import Data.Word
import GHC.Generics (Generic)

-- | A Client-side identifier for items.
--
-- These only need to be unique at the client.
newtype ClientId
  = ClientId
      { unClientId :: Word64
      }
  deriving (Show, Eq, Ord, Enum, Bounded, Generic, ToJSON, ToJSONKey, FromJSON, FromJSONKey)

instance Validity ClientId

instance NFData ClientId

data ClientStore ci si a
  = ClientStore
      { -- | These items are new locally but have not been synced to the server yet.
        clientStoreAddedItems :: Map ci a,
        -- | These items have been synced at their respective 'ServerTime's.
        clientStoreSyncedItems :: Map si (Timed a),
        -- | These items have been synced at their respective 'ServerTime's
        -- but modified locally since then.
        clientStoreSyncedButChangedItems :: Map si (Timed a),
        -- | These items have been deleted locally after they were synced
        -- but the server has not been notified of that yet.
        clientStoreDeletedItems :: Map si ServerTime
      }
  deriving (Show, Eq, Generic)

instance
  (Validity ci, Validity si, Show ci, Show si, Ord ci, Ord si, Validity a) =>
  Validity (ClientStore ci si a)
  where
  validate cs@ClientStore {..} =
    mconcat
      [ genericValidate cs,
        declare "There are no duplicate IDs"
          $ distinct
          $ concat
            [ M.keys clientStoreSyncedItems,
              M.keys clientStoreSyncedButChangedItems,
              M.keys clientStoreDeletedItems
            ]
      ]

instance (NFData ci, NFData si, NFData a) => NFData (ClientStore ci si a)

instance
  (Ord ci, Ord si, FromJSONKey ci, FromJSONKey si, FromJSON a) =>
  FromJSON (ClientStore ci si a)
  where
  parseJSON =
    withObject "ClientStore" $ \o ->
      ClientStore <$> o .:? "added" .!= M.empty <*> o .:? "synced" .!= M.empty
        <*> o .:? "changed" .!= M.empty
        <*> o .:? "deleted" .!= M.empty

instance (ToJSONKey ci, ToJSONKey si, ToJSON a) => ToJSON (ClientStore ci si a) where
  toJSON ClientStore {..} =
    object $
      catMaybes
        [ jNull "added" clientStoreAddedItems,
          jNull "synced" clientStoreSyncedItems,
          jNull "changed" clientStoreSyncedButChangedItems,
          jNull "deleted" clientStoreDeletedItems
        ]

-- | A client store to start with.
--
-- This store contains no items.
initialClientStore :: ClientStore ci si a
initialClientStore =
  ClientStore
    { clientStoreAddedItems = M.empty,
      clientStoreSyncedItems = M.empty,
      clientStoreSyncedButChangedItems = M.empty,
      clientStoreDeletedItems = M.empty
    }

-- | The number of items in a client store
--
-- This does not count the deleted items, so that they really look deleted..
clientStoreSize :: ClientStore ci si a -> Word
clientStoreSize ClientStore {..} =
  fromIntegral $
    sum
      [ M.size clientStoreAddedItems,
        M.size clientStoreSyncedItems,
        M.size clientStoreSyncedButChangedItems
      ]

-- | The set of client ids.
--
-- These are only the client ids of the added items that have not been synced yet.
clientStoreClientIdSet :: ClientStore ci si a -> Set ci
clientStoreClientIdSet ClientStore {..} = M.keysSet clientStoreAddedItems

-- | The set of server ids.
--
-- This does not include the ids of items that have been marked as deleted.
clientStoreUndeletedSyncIdSet :: Ord si => ClientStore ci si a -> Set si
clientStoreUndeletedSyncIdSet ClientStore {..} =
  S.unions [M.keysSet clientStoreSyncedItems, M.keysSet clientStoreSyncedButChangedItems]

-- | The set of server ids.
--
-- This includes the ids of items that have been marked as deleted.
clientStoreSyncIdSet :: Ord si => ClientStore ci si a -> Set si
clientStoreSyncIdSet ClientStore {..} =
  S.unions
    [ M.keysSet clientStoreSyncedItems,
      M.keysSet clientStoreSyncedButChangedItems,
      M.keysSet clientStoreDeletedItems
    ]

-- | The set of items in the client store
--
-- This map does not include items that have been marked as deleted.
clientStoreItems :: (Ord ci, Ord si) => ClientStore ci si a -> Map (Either ci si) a
clientStoreItems ClientStore {..} =
  M.unions
    [ M.mapKeys Left clientStoreAddedItems,
      M.mapKeys Right $ M.map timedValue clientStoreSyncedItems,
      M.mapKeys Right $ M.map timedValue clientStoreSyncedButChangedItems
    ]

-- | Add an item to a client store as an added item.
--
-- This will take care of the uniqueness constraint of the 'ci's in the map.
addItemToClientStore ::
  (Ord ci, Enum ci, Bounded ci) => a -> ClientStore ci si a -> ClientStore ci si a
addItemToClientStore a cs =
  let oldAddedItems = clientStoreAddedItems cs
      newAddedItems =
        let newKey = findFreeSpot oldAddedItems
         in M.insert newKey a oldAddedItems
   in cs {clientStoreAddedItems = newAddedItems}

-- | Find a free client id to use
--
-- You shouldn't need this function, 'addItemToClientStore' takes care of this.
--
-- The values wrap around when reaching 'maxBound'.
findFreeSpot :: (Ord ci, Enum ci, Bounded ci) => Map ci a -> ci
findFreeSpot m =
  if M.null m
    then minBound
    else
      let (i, _) = M.findMax m
       in go (next i)
  where
    go i =
      if M.member i m
        then go (next i)
        else i
    next ci
      | ci == maxBound = minBound
      | otherwise = succ ci

-- | Mark an item deleted in a client store.
--
-- This function will not delete the item, but mark it as deleted instead.
markItemDeletedInClientStore :: Ord si => si -> ClientStore ci si a -> ClientStore ci si a
markItemDeletedInClientStore u cs =
  let oldSyncedItems = clientStoreSyncedItems cs
      oldChangedItems = clientStoreSyncedButChangedItems cs
      oldDeletedItems = clientStoreDeletedItems cs
      mItem = M.lookup u oldSyncedItems <|> M.lookup u oldChangedItems
   in case mItem of
        Nothing -> cs
        Just t ->
          let newSyncedItems = M.delete u oldSyncedItems
              newChangedItems = M.delete u oldChangedItems
              newDeletedItems = M.insert u (timedTime t) oldDeletedItems
           in cs
                { clientStoreSyncedItems = newSyncedItems,
                  clientStoreSyncedButChangedItems = newChangedItems,
                  clientStoreDeletedItems = newDeletedItems
                }

-- | Replace the given item with a new value.
--
-- This function will correctly mark the item as changed, if it exist.
--
-- It will not add an item to the store with the given id, because the
-- server may not have been the origin of that id.
changeItemInClientStore :: Ord si => si -> a -> ClientStore ci si a -> ClientStore ci si a
changeItemInClientStore i a cs =
  case M.lookup i (clientStoreSyncedItems cs) of
    Just t ->
      cs
        { clientStoreSyncedItems = M.delete i (clientStoreSyncedItems cs),
          clientStoreSyncedButChangedItems =
            M.insert i (t {timedValue = a}) (clientStoreSyncedButChangedItems cs)
        }
    Nothing ->
      case M.lookup i (clientStoreSyncedButChangedItems cs) of
        Nothing -> cs
        Just _ ->
          cs
            { clientStoreSyncedButChangedItems =
                M.adjust (\t -> t {timedValue = a}) i (clientStoreSyncedButChangedItems cs)
            }

-- | Delete an unsynced item from a client store.
--
-- This function will immediately delete the item, because it has never been synced.
deleteItemFromClientStore :: Ord ci => ci -> ClientStore ci si a -> ClientStore ci si a
deleteItemFromClientStore i cs = cs {clientStoreAddedItems = M.delete i (clientStoreAddedItems cs)}

newtype ServerStore si a
  = ServerStore
      { -- | A map of items, named using an 'si', together with the 'ServerTime' at which
        -- they were last synced.
        serverStoreItems :: Map si (Timed a)
      }
  deriving (Show, Eq, Generic, FromJSON, ToJSON)

instance (Validity si, Show si, Ord si, Validity a) => Validity (ServerStore si a)

instance (NFData si, NFData a) => NFData (ServerStore si a)

-- | A server store to start with
--
-- This store contains no items.
initialServerStore :: ServerStore si a
initialServerStore = ServerStore {serverStoreItems = M.empty}

data SyncRequest ci si a
  = SyncRequest
      { -- | These items are new locally but have not been synced to the server yet.
        syncRequestNewItems :: !(Map ci a),
        -- | These items have been synced at their respective 'ServerTime's.
        syncRequestKnownItems :: !(Map si ServerTime),
        -- | These items have been synced at their respective 'ServerTime's
        -- but modified locally since then.
        syncRequestKnownButChangedItems :: !(Map si (Timed a)),
        -- | These items have been deleted locally after they were synced
        -- but the server has not been notified of that yet.
        syncRequestDeletedItems :: !(Map si ServerTime)
      }
  deriving (Show, Eq, Generic)

instance
  (Validity ci, Validity si, Show ci, Show si, Ord ci, Ord si, Validity a) =>
  Validity (SyncRequest ci si a)
  where
  validate sr@SyncRequest {..} =
    mconcat
      [ genericValidate sr,
        declare "There are no duplicate IDs"
          $ distinct
          $ concat
            [ M.keys syncRequestKnownItems,
              M.keys syncRequestKnownButChangedItems,
              M.keys syncRequestDeletedItems
            ]
      ]

instance (NFData ci, NFData si, NFData a) => NFData (SyncRequest ci si a)

instance
  (Ord ci, Ord si, FromJSONKey ci, FromJSONKey si, FromJSON a) =>
  FromJSON (SyncRequest ci si a)
  where
  parseJSON =
    withObject "SyncRequest" $ \o ->
      SyncRequest <$> o .:? "added" .!= M.empty <*> o .:? "synced" .!= M.empty
        <*> o .:? "changed" .!= M.empty
        <*> o .:? "deleted" .!= M.empty

instance (ToJSONKey ci, ToJSONKey si, ToJSON a) => ToJSON (SyncRequest ci si a) where
  toJSON SyncRequest {..} =
    object $
      catMaybes
        [ jNull "added" syncRequestNewItems,
          jNull "synced" syncRequestKnownItems,
          jNull "changed" syncRequestKnownButChangedItems,
          jNull "deleted" syncRequestDeletedItems
        ]

-- | An intial 'SyncRequest' to start with.
--
-- It just asks the server to send over whatever it knows.
initialSyncRequest :: SyncRequest ci si a
initialSyncRequest =
  SyncRequest
    { syncRequestNewItems = M.empty,
      syncRequestKnownItems = M.empty,
      syncRequestKnownButChangedItems = M.empty,
      syncRequestDeletedItems = M.empty
    }

data ClientAddition i
  = ClientAddition
      { clientAdditionId :: i,
        clientAdditionServerTime :: ServerTime
      }
  deriving (Show, Eq, Generic)

instance Validity i => Validity (ClientAddition i)

instance NFData i => NFData (ClientAddition i)

instance FromJSON i => FromJSON (ClientAddition i) where
  parseJSON = withObject "ClientAddition" $ \o -> ClientAddition <$> o .: "id" <*> o .: "time"

instance ToJSON i => ToJSON (ClientAddition i) where
  toJSON ClientAddition {..} = object ["id" .= clientAdditionId, "time" .= clientAdditionServerTime]

data SyncResponse ci si a
  = SyncResponse
      { -- | The client added these items and server has succesfully been made aware of that.
        --
        -- The client needs to update their server times
        syncResponseClientAdded :: !(Map ci (ClientAddition si)),
        -- | The client changed these items and server has succesfully been made aware of that.
        --
        -- The client needs to update their server times
        syncResponseClientChanged :: !(Map si ServerTime),
        -- | The client deleted these items and server has succesfully been made aware of that.
        --
        -- The client can delete them from its deleted items
        syncResponseClientDeleted :: !(Set si),
        -- | These items have been added on the server side
        --
        -- The client should add them too.
        syncResponseServerAdded :: !(Map si (Timed a)),
        -- | These items have been modified on the server side.
        --
        -- The client should modify them too.
        syncResponseServerChanged :: !(Map si (Timed a)),
        -- | These items were deleted on the server side
        --
        -- The client should delete them too
        syncResponseServerDeleted :: !(Set si),
        -- | These are conflicts where the server and the client both have an item, but it is different.
        --
        -- The server kept its part of each, the client can either take whatever the server gave them
        -- or deal with the conflicts somehow, and then try to re-sync.
        syncResponseConflicts :: !(Map si (Timed a)),
        -- | These are conflicts where the server has an item but the client does not.
        --
        -- The server kept its item, the client can either take whatever the server gave them
        -- or deal with the conflicts somehow, and then try to re-sync.
        syncResponseConflictsClientDeleted :: !(Map si (Timed a)),
        -- | These are conflicts where the server has no item but the client has a modified item.
        --
        -- The server left its item deleted, the client can either delete its items too
        -- or deal with the conflicts somehow, and then try to re-sync.
        syncResponseConflictsServerDeleted :: !(Set si)
      }
  deriving (Show, Eq, Generic)

instance
  (Validity ci, Validity si, Show ci, Show si, Ord ci, Ord si, Validity a) =>
  Validity (SyncResponse ci si a)
  where
  validate sr@SyncResponse {..} =
    mconcat
      [ genericValidate sr,
        declare "There are no duplicate IDs"
          $ distinct
          $ concat
            [ map (\(_, ClientAddition {..}) -> clientAdditionId) $ M.toList syncResponseClientAdded,
              M.keys syncResponseClientChanged,
              S.toList syncResponseClientDeleted,
              M.keys syncResponseServerAdded,
              M.keys syncResponseServerChanged,
              S.toList syncResponseServerDeleted,
              M.keys syncResponseConflicts,
              M.keys syncResponseConflictsClientDeleted,
              S.toList syncResponseConflictsServerDeleted
            ]
      ]

instance (NFData ci, NFData si, NFData a) => NFData (SyncResponse ci si a)

instance
  (Ord ci, Ord si, FromJSON ci, FromJSON si, FromJSONKey ci, FromJSONKey si, FromJSON a) =>
  FromJSON (SyncResponse ci si a)
  where
  parseJSON =
    withObject "SyncResponse" $ \o ->
      SyncResponse <$> o .:? "client-added" .!= M.empty <*> o .:? "client-changed" .!= M.empty
        <*> o .:? "client-deleted" .!= S.empty
        <*> o .:? "server-added" .!= M.empty
        <*> o .:? "server-changed" .!= M.empty
        <*> o .:? "server-deleted" .!= S.empty
        <*> o .:? "conflict" .!= M.empty
        <*> o .:? "conflict-client-deleted" .!= M.empty
        <*> o .:? "conflict-server-deleted" .!= S.empty

instance
  (ToJSON ci, ToJSON si, ToJSONKey ci, ToJSONKey si, ToJSON a) =>
  ToJSON (SyncResponse ci si a)
  where
  toJSON SyncResponse {..} =
    object $
      catMaybes
        [ jNull "client-added" syncResponseClientAdded,
          jNull "client-changed" syncResponseClientChanged,
          jNull "client-deleted" syncResponseClientDeleted,
          jNull "server-added" syncResponseServerAdded,
          jNull "server-changed" syncResponseServerChanged,
          jNull "server-deleted" syncResponseServerDeleted,
          jNull "conflict" syncResponseConflicts,
          jNull "conflict-client-deleted" syncResponseConflictsClientDeleted,
          jNull "conflict-server-deleted" syncResponseConflictsServerDeleted
        ]

-- | A sync response to start with.
--
-- It is entirely empty.
emptySyncResponse :: SyncResponse ci si a
emptySyncResponse =
  SyncResponse
    { syncResponseClientAdded = M.empty,
      syncResponseClientChanged = M.empty,
      syncResponseClientDeleted = S.empty,
      syncResponseServerAdded = M.empty,
      syncResponseServerChanged = M.empty,
      syncResponseServerDeleted = S.empty,
      syncResponseConflicts = M.empty,
      syncResponseConflictsClientDeleted = M.empty,
      syncResponseConflictsServerDeleted = S.empty
    }

jNull :: (Foldable f, ToJSON (f b)) => Text -> f b -> Maybe (Text, Value)
jNull n s =
  if null s
    then Nothing
    else Just $ n .= s

-- | Produce an 'SyncRequest' from a 'ClientStore'.
--
-- Send this to the server for synchronisation.
makeSyncRequest :: ClientStore ci si a -> SyncRequest ci si a
makeSyncRequest ClientStore {..} =
  SyncRequest
    { syncRequestNewItems = clientStoreAddedItems,
      syncRequestKnownItems = M.map timedTime clientStoreSyncedItems,
      syncRequestKnownButChangedItems = clientStoreSyncedButChangedItems,
      syncRequestDeletedItems = clientStoreDeletedItems
    }

-- | Merge a 'SyncResponse' into the current 'ClientStore' by taking whatever the server gave the client in case of conflict.
--
-- Pro: Clients will converge on the same value.
--
-- __Con: Conflicting updates will be lost.__
mergeSyncResponseFromServer ::
  (Ord ci, Ord si) => ClientStore ci si a -> SyncResponse ci si a -> ClientStore ci si a
mergeSyncResponseFromServer =
  mergeSyncResponseUsingStrategy mergeFromServerStrategy

-- | Merge a 'SyncResponse' into the current 'ClientStore' by keeping whatever the client had in case of conflict.
--
-- Pro: No data will be lost
--
-- __Con: Clients will diverge when conflicts occur.__
mergeSyncResponseFromClient ::
  (Ord ci, Ord si) => ClientStore ci si a -> SyncResponse ci si a -> ClientStore ci si a
mergeSyncResponseFromClient = mergeSyncResponseUsingStrategy mergeFromClientStrategy

-- | Merge a 'SyncResponse' into the current 'ClientStore' by using the given GADT merging function in case of conflict
mergeSyncResponseUsingCRDT :: (Ord ci, Ord si) => (a -> a -> a) -> ClientStore ci si a -> SyncResponse ci si a -> ClientStore ci si a
mergeSyncResponseUsingCRDT = mergeSyncResponseUsingStrategy . mergeUsingCRDTStrategy

-- | Merge an 'SyncResponse' into the current 'ClientStore' with the given merge strategy.
--
-- In order for clients to converge on the same collection correctly, this function must be:
--
-- * Associative
-- * Idempotent
-- * The same on all clients
--
-- This function ignores mismatches.
mergeSyncResponseUsingStrategy ::
  (Ord ci, Ord si) =>
  ItemMergeStrategy a ->
  ClientStore ci si a ->
  SyncResponse ci si a ->
  ClientStore ci si a
mergeSyncResponseUsingStrategy strat cs sr =
  flip execState cs $ mergeSyncResponseCustom strat pureClientSyncProcessor sr

pureClientSyncProcessor :: forall ci si a. (Ord ci, Ord si) => ClientSyncProcessor ci si a (State (ClientStore ci si a))
pureClientSyncProcessor =
  ClientSyncProcessor
    { clientSyncProcessorQuerySyncedButChangedValues = \s ->
        gets
          ( \cs -> M.intersection (clientStoreSyncedButChangedItems cs) (M.fromSet (const ()) s)
          ),
      clientSyncProcessorSyncClientAdded = \m ->
        modify
          ( \cs ->
              let (leftovers, added) = mergeAddedItems (clientStoreAddedItems cs) m
               in cs {clientStoreAddedItems = leftovers, clientStoreSyncedItems = added `M.union` clientStoreSyncedItems cs}
          ),
      clientSyncProcessorSyncClientChanged = \m ->
        modify
          ( \cs ->
              let (leftovers, changed) = mergeSyncedButChangedItems (clientStoreSyncedButChangedItems cs) m
               in cs {clientStoreSyncedButChangedItems = leftovers, clientStoreSyncedItems = changed `M.union` clientStoreSyncedItems cs}
          ),
      clientSyncProcessorSyncClientDeleted = \s ->
        modify
          ( \cs ->
              let leftovers = mergeDeletedItems (clientStoreDeletedItems cs) s
               in cs {clientStoreDeletedItems = leftovers}
          ),
      clientSyncProcessorSyncMergedConflict = \resolved ->
        modify
          ( \cs ->
              let newSyncedButChanged = M.union resolved (clientStoreSyncedButChangedItems cs)
               in cs {clientStoreSyncedButChangedItems = newSyncedButChanged, clientStoreSyncedItems = clientStoreSyncedItems cs `M.difference` newSyncedButChanged}
          ),
      clientSyncProcessorSyncServerAdded = \m ->
        modify (\cs -> cs {clientStoreSyncedItems = m `M.union` clientStoreSyncedItems cs}),
      clientSyncProcessorSyncServerChanged = \m ->
        modify
          ( \cs ->
              let newSynced = m `M.union` clientStoreSyncedItems cs
               in cs {clientStoreSyncedItems = newSynced, clientStoreSyncedButChangedItems = clientStoreSyncedButChangedItems cs `M.difference` newSynced}
          ),
      clientSyncProcessorSyncServerDeleted = \s ->
        modify
          ( \cs ->
              let m = M.fromSet (const ()) s
               in cs
                    { clientStoreSyncedItems = clientStoreSyncedItems cs `M.difference` m,
                      clientStoreSyncedButChangedItems = clientStoreSyncedButChangedItems cs `M.difference` m
                    }
          )
    }

-- | Merge the local added items with the ones that the server has acknowledged as added.
mergeAddedItems ::
  forall ci si a.
  (Ord ci, Ord si) =>
  Map ci a ->
  Map ci (ClientAddition si) ->
  (Map ci a, Map si (Timed a))
mergeAddedItems local added = M.foldlWithKey go (M.empty, M.empty) local
  where
    go :: (Map ci a, Map si (Timed a)) -> ci -> a -> (Map ci a, Map si (Timed a))
    go (as, m) ci a =
      case M.lookup ci added of
        Nothing -> (M.insert ci a as, m)
        Just ClientAddition {..} ->
          ( as,
            M.insert
              clientAdditionId
              (Timed {timedValue = a, timedTime = clientAdditionServerTime})
              m
          )

-- | Merge the local synced but changed items with the ones that the server has acknowledged as changed.
mergeSyncedButChangedItems ::
  forall i a.
  Ord i =>
  Map i (Timed a) ->
  Map i ServerTime ->
  (Map i (Timed a), Map i (Timed a))
mergeSyncedButChangedItems local changed = M.foldlWithKey go (M.empty, M.empty) local
  where
    go :: (Map i (Timed a), Map i (Timed a)) -> i -> Timed a -> (Map i (Timed a), Map i (Timed a))
    go (m1, m2) k t =
      case M.lookup k changed of
        Nothing -> (M.insert k t m1, m2)
        Just st' -> (m1, M.insert k (t {timedTime = st'}) m2)

-- | Merge the local deleted items with the ones that the server has acknowledged as deleted.
mergeDeletedItems :: Ord i => Map i b -> Set i -> Map i b
mergeDeletedItems m s = m `M.difference` M.fromSet (const ()) s

data ClientSyncProcessor ci si a (m :: * -> *)
  = ClientSyncProcessor
      { -- | Get the synced values with keys in the given set
        clientSyncProcessorQuerySyncedButChangedValues :: !(Set si -> m (Map si (Timed a))),
        -- | Complete additions that were acknowledged by the server.
        -- This involves saving the server id and the server time
        clientSyncProcessorSyncClientAdded :: !(Map ci (ClientAddition si) -> m ()),
        -- | Complete changes that were acknowledged by the server
        -- This involves updating the server time
        clientSyncProcessorSyncClientChanged :: !(Map si ServerTime -> m ()),
        -- | Complete deletions that were acknowledged by the server
        -- This means deleting these tombstoned items entirely
        clientSyncProcessorSyncClientDeleted :: !(Set si -> m ()),
        -- | Store the items that were in a conflict but the conflict was resolved correctly.
        -- These items should be marked as changed.
        clientSyncProcessorSyncMergedConflict :: !(Map si (Timed a) -> m ()),
        clientSyncProcessorSyncServerAdded :: !(Map si (Timed a) -> m ()),
        clientSyncProcessorSyncServerChanged :: !(Map si (Timed a) -> m ()),
        clientSyncProcessorSyncServerDeleted :: !(Set si -> m ())
      }
  deriving (Generic)

mergeSyncResponseCustom :: (Ord si, Monad m) => ItemMergeStrategy a -> ClientSyncProcessor ci si a m -> SyncResponse ci si a -> m ()
mergeSyncResponseCustom ItemMergeStrategy {..} ClientSyncProcessor {..} SyncResponse {..} = do
  -- Every client deleted conflict needs to be added, if the sync processor says so
  let resolvedClientDeletedConflicts = mergeClientDeletedConflicts itemMergeStrategyMergeClientDeletedConflict syncResponseConflictsClientDeleted
  -- Every change conflict, unless the client item is kept, needs to be updated
  -- The unresolved conflicts don't need to be updated.
  clientChangeConflicts <- clientSyncProcessorQuerySyncedButChangedValues $ M.keysSet syncResponseConflicts
  let (_, mergedChangeConflicts, resolvedChangeConflicts) = mergeSyncedButChangedConflicts itemMergeStrategyMergeChangeConflict clientChangeConflicts syncResponseConflicts
  -- Every served deleted conflict needs to be deleted, if the sync processor says so
  clientServerDeletedConflicts <- clientSyncProcessorQuerySyncedButChangedValues syncResponseConflictsServerDeleted
  let resolvedServerDeletedConflicts = mergeServerDeletedConflicts itemMergeStrategyMergeServerDeletedConflict clientServerDeletedConflicts
  -- The order here matters.
  clientSyncProcessorSyncServerAdded $ M.union syncResponseServerAdded resolvedClientDeletedConflicts
  clientSyncProcessorSyncServerChanged $ M.union syncResponseServerChanged resolvedChangeConflicts
  clientSyncProcessorSyncServerDeleted $ S.union syncResponseServerDeleted resolvedServerDeletedConflicts
  clientSyncProcessorSyncMergedConflict mergedChangeConflicts
  clientSyncProcessorSyncClientDeleted syncResponseClientDeleted
  clientSyncProcessorSyncClientChanged syncResponseClientChanged
  clientSyncProcessorSyncClientAdded syncResponseClientAdded

-- | Resolve change conflicts
mergeSyncedButChangedConflicts ::
  forall si a.
  Ord si =>
  (a -> a -> ChangeConflictResolution a) ->
  -- | The conflicting items on the client side
  Map si (Timed a) ->
  -- | The conflicting items on the server side
  Map si (Timed a) ->
  -- | Unresolved conflicts on the left, merged conflicts in the middle, resolved conflicts on the right
  --
  -- * The unresolved conflicts should remain as-is
  -- * The merged conflicts should be updated and marked as changed
  -- * The resolved conflicts should be updated and marked as unchanged
  (Map si (Timed a), Map si (Timed a), Map si (Timed a))
mergeSyncedButChangedConflicts func clientItems =
  M.foldlWithKey go (M.empty, M.empty, M.empty)
  where
    go ::
      (Map si (Timed a), Map si (Timed a), Map si (Timed a)) ->
      si ->
      Timed a ->
      (Map si (Timed a), Map si (Timed a), Map si (Timed a))
    go tup@(unresolved, merged, resolved) key s@(Timed si st) = case M.lookup key clientItems of
      Nothing -> tup -- TODO not even sure what this would mean. Should not happen I guess. Just throw it away
      Just c@(Timed ci _) -> case func ci si of
        KeepLocal ->
          (M.insert key c unresolved, merged, resolved)
        Merged mi ->
          (unresolved, M.insert key (Timed mi st) merged, M.insert key s resolved)
        TakeRemote ->
          (unresolved, merged, M.insert key s resolved)

-- | Resolve client deleted conflicts
mergeClientDeletedConflicts ::
  (a -> ClientDeletedConflictResolution) ->
  -- | The conflicting items on the server side
  Map si (Timed a) ->
  -- | A map of items that need to be updated on the client.
  Map si (Timed a)
mergeClientDeletedConflicts func = M.filter $ \(Timed si _) ->
  case func si of
    TakeRemoteChange -> True
    StayDeleted -> False

-- | Resolve server deleted conflicts
mergeServerDeletedConflicts ::
  (a -> ServerDeletedConflictResolution) ->
  -- | The conflicting items on the client side
  Map si (Timed a) ->
  -- | The result is a map of items that need to be deleted on the client.
  Set si
mergeServerDeletedConflicts func m = M.keysSet $ flip M.filter m $ \(Timed si _) -> case func si of
  KeepLocalChange -> False
  Delete -> True

data Identifier ci si
  = OnlyServer si
  | BothServerAndClient si ci
  deriving (Show, Eq, Ord, Generic)

instance (Validity ci, Validity si) => Validity (Identifier ci si)

instance (NFData ci, NFData si) => NFData (Identifier ci si)

data ServerSyncProcessor ci si a m
  = ServerSyncProcessor
      { -- | Read all items
        serverSyncProcessorRead :: !(m (Map si (Timed a))),
        -- | Add an item with 'initialServerTime'
        serverSyncProcessorAddItem :: !(a -> m si),
        -- | Update an item
        serverSyncProcessorChangeItem :: !(si -> ServerTime -> a -> m ()),
        -- | Delete an item
        serverSyncProcessorDeleteItem :: !(si -> m ())
      }
  deriving (Generic)

-- | Process a server sync
--
-- === __Implementation Details__
--
-- There are four cases for the items in the sync request
--
-- - Added (A)
-- - Synced (S)
-- - Changed (C)
-- - Deleted (D)
--
-- Each of them present options and may require action on the sever side:
--
-- * Added:
--
--     * Client Added (CA) (This is the only case where a new identifier needs to be generated.)
--
-- * Synced:
--
--     * Server Changed (SC) (Nothing)
--     * Server Deleted (SD) (Nothing)
--
-- * Changed:
--
--     * Client Changed (CC) (Update value and increment server time)
--     * Change Conflict (CConf) (Nothing)
--     * Server Deleted Conflict (SDC) (Nothing)
--
-- * Deleted:
--
--     * Client Deleted (CD) (Delete the item)
--     * Client Deleted Conflict (CDC) (Nothing)
--
-- * Extra:
--
--     * Server Added (SA) (Nothing)
--
-- For more detailed comments of the nine cases, see the source of 'processServerItemSync' in the "Data.Mergeful.Item".
processServerSyncCustom ::
  forall ci si a m.
  ( Ord si,
    Monad m
  ) =>
  -- | Your server sync processor
  ServerSyncProcessor ci si a m ->
  SyncRequest ci si a ->
  m (SyncResponse ci si a)
processServerSyncCustom ServerSyncProcessor {..} SyncRequest {..} = do
  serverItems <- serverSyncProcessorRead
  -- A: CA (generate a new identifier)
  syncResponseClientAdded <- forM syncRequestNewItems $ \a -> do
    si <- serverSyncProcessorAddItem a
    pure $ ClientAddition {clientAdditionId = si, clientAdditionServerTime = initialServerTime}
  -- C:
  let decideOnSynced tup@(sc, sd) (si, ct) =
        case M.lookup si serverItems of
          -- SD: The server must have deleted it.
          Nothing -> (sc, S.insert si sd)
          Just t@(Timed _ st) ->
            if ct >= st
              then tup -- In sync
              else (M.insert si t sc, sd) -- SC: The server has changed it because its server time is newer
  let (syncResponseServerChanged, syncResponseServerDeleted) = foldl decideOnSynced (M.empty, S.empty) (M.toList syncRequestKnownItems)
  -- S:
  let decideOnChanged (cc, cConf, sdc) (si, Timed clientItem ct) = do
        case M.lookup si serverItems of
          -- SDC
          Nothing -> pure (cc, cConf, S.insert si sdc)
          Just serverTimed@(Timed _ st) ->
            if ct >= st
              then do
                -- CC
                let st' = incrementServerTime st
                -- Update the server item
                serverSyncProcessorChangeItem si st' clientItem
                pure (M.insert si st' cc, cConf, sdc)
              else do
                -- CConf
                pure (cc, M.insert si serverTimed cConf, sdc)
  (syncResponseClientChanged, syncResponseConflicts, syncResponseConflictsServerDeleted) <- foldM decideOnChanged (M.empty, M.empty, S.empty) (M.toList syncRequestKnownButChangedItems)
  --- D:
  let decideOnDeleted (cd, cdc) (si, ct) = do
        case M.lookup si serverItems of
          Nothing -> do
            -- CD: It was already deleted on the server side, Just pretend that the client made that happen.
            pure (S.insert si cd, cdc)
          Just serverTimed@(Timed _ st) ->
            if ct >= st
              then do
                -- CD
                -- Delete the item
                serverSyncProcessorDeleteItem si
                pure (S.insert si cd, cdc)
              else do
                -- CDC
                pure (cd, M.insert si serverTimed cdc)
  (syncResponseClientDeleted, syncResponseConflictsClientDeleted) <- foldM decideOnDeleted (S.empty, M.empty) (M.toList syncRequestDeletedItems)
  -- Extra: for all items that are in the server but not in the sync request, we need to say they are server added.
  let syncResponseServerAdded = serverItems `M.difference` M.unions [() <$ syncRequestKnownItems, () <$ syncRequestKnownButChangedItems, () <$ syncRequestDeletedItems]
  pure SyncResponse {..}

-- | Serve an 'SyncRequest' using the current 'ServerStore', producing an 'SyncResponse' and a new 'ServerStore'.
processServerSync ::
  forall ci si a m.
  ( Ord si,
    Monad m
  ) =>
  -- | The action that is guaranteed to generate unique identifiers
  m si ->
  ServerStore si a ->
  SyncRequest ci si a ->
  m (SyncResponse ci si a, ServerStore si a)
processServerSync genId ss sr = runStateT (processServerSyncCustom (pureServerSyncProcessor genId) sr) ss

-- | A potentially pure sync processor
pureServerSyncProcessor ::
  (Ord si, Monad m) =>
  -- | The action that is guaranteed to generate unique identifiers
  m si ->
  ServerSyncProcessor ci si a (StateT (ServerStore si a) m)
pureServerSyncProcessor genId = ServerSyncProcessor {..}
  where
    serverSyncProcessorRead = gets serverStoreItems
    serverSyncProcessorAddItem a = do
      i <- lift genId
      modify (\(ServerStore m) -> ServerStore (M.insert i (Timed a initialServerTime) m))
      pure i
    serverSyncProcessorChangeItem si st a =
      modify
        ( \(ServerStore m) ->
            let m' = M.adjust (const (Timed a st)) si m
             in ServerStore m'
        )
    serverSyncProcessorDeleteItem si =
      modify
        ( \(ServerStore m) ->
            let m' = M.delete si m
             in ServerStore m'
        )

distinct :: Eq a => [a] -> Bool
distinct ls = nub ls == ls