{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeFamilies #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}

module Data.Mergeful.Persistent
  ( -- * Client side
    clientMakeSyncRequestQuery,
    clientMergeSyncResponseQuery,

    -- ** Raw processors
    clientSyncProcessor,

    -- ** Merging
    ItemMergeStrategy (..),
    mergeFromServerStrategy,
    mergeFromClientStrategy,
    mergeUsingCRDTStrategy,

    -- * Server side
    serverProcessSyncQuery,
    serverProcessSyncWithCustomIdQuery,

    -- ** Raw processors
    serverSyncProcessor,
    serverSyncWithCustomIdProcessor,

    -- * Utils

    -- ** Client side
    setupClientQuery,
    clientGetStoreQuery,

    -- ** Server side
    setupServerQuery,
    serverGetStoreQuery,
  )
where

import Control.Monad
import Control.Monad.IO.Class
import qualified Data.Map as M
import Data.Map (Map)
import Data.Maybe
import Data.Mergeful
import Data.Set (Set)
import qualified Data.Set as S
import Database.Persist
import Database.Persist.Sql

deriving instance PersistField ServerTime

deriving instance PersistFieldSql ServerTime

-- | Make a sync request
clientMakeSyncRequestQuery ::
  forall record sid a m.
  ( Ord sid,
    PersistEntity record,
    PersistField sid,
    PersistEntityBackend record ~ SqlBackend,
    ToBackendKey SqlBackend record,
    MonadIO m
  ) =>
  -- | The server id field
  EntityField record (Maybe sid) ->
  -- | The server time field
  EntityField record (Maybe ServerTime) ->
  -- | The changed flag
  EntityField record Bool ->
  -- | The deleted flag
  EntityField record Bool ->
  -- | How to read an unsynced client item
  (record -> a) ->
  -- | How to read a synced client item that's been changed
  (record -> (sid, Timed a)) ->
  -- | How to read a synced or deleted client item
  (record -> (sid, ServerTime)) ->
  SqlPersistT m (SyncRequest (Key record) sid a)
clientMakeSyncRequestQuery
  serverIdField
  serverTimeField
  changedField
  deletedField
  unmakeUnsyncedClientThing
  unmakeSyncedClientThing
  unmakeDeletedClientThing = do
    syncRequestNewItems <-
      M.fromList . map (\(Entity cid r) -> (cid, unmakeUnsyncedClientThing r))
        <$> selectList
          [ serverIdField ==. Nothing,
            serverTimeField ==. Nothing
          ]
          []
    syncRequestKnownItems <-
      M.fromList . map (unmakeDeletedClientThing . entityVal)
        <$> selectList
          [ serverIdField !=. Nothing,
            serverTimeField !=. Nothing,
            changedField ==. False,
            deletedField ==. False
          ]
          []
    syncRequestKnownButChangedItems <-
      M.fromList . map (unmakeSyncedClientThing . entityVal)
        <$> selectList
          [ serverIdField !=. Nothing,
            serverTimeField !=. Nothing,
            changedField ==. True,
            deletedField ==. False
          ]
          []
    syncRequestDeletedItems <-
      M.fromList . map (unmakeDeletedClientThing . entityVal)
        <$> selectList
          [ deletedField ==. True
          ]
          []
    pure SyncRequest {..}

-- Merge a sync response
clientMergeSyncResponseQuery ::
  forall record sid a m.
  ( Ord sid,
    PersistField sid,
    PersistEntityBackend record ~ SqlBackend,
    ToBackendKey SqlBackend record,
    MonadIO m
  ) =>
  -- | The server id field
  EntityField record (Maybe sid) ->
  -- | The server time field
  EntityField record (Maybe ServerTime) ->
  -- | The changed flag
  EntityField record Bool ->
  -- | The deleted flag
  EntityField record Bool ->
  -- | How to build a synced record from a server id and a timed item
  (sid -> Timed a -> record) ->
  -- | How to read a synced record back into a server id and a timed item
  (record -> (sid, Timed a)) ->
  -- | How to update a row with new data
  --
  -- You only need to perform the updates that have anything to do with the data to sync.
  -- The housekeeping updates are already taken care of.
  (a -> [Update record]) ->
  -- | The merge strategy.
  ItemMergeStrategy a ->
  -- | The sync response to merge
  SyncResponse (Key record) sid a ->
  SqlPersistT m ()
clientMergeSyncResponseQuery
  serverIdField
  serverTimeField
  changedField
  deletedField
  makeSyncedClientThing
  unmakeSyncedClientThing
  recordUpdates
  strat =
    mergeSyncResponseCustom strat $
      clientSyncProcessor
        serverIdField
        serverTimeField
        changedField
        deletedField
        makeSyncedClientThing
        unmakeSyncedClientThing
        recordUpdates

clientSyncProcessor ::
  forall record sid a m.
  ( Ord sid,
    PersistField sid,
    PersistEntityBackend record ~ SqlBackend,
    ToBackendKey SqlBackend record,
    MonadIO m
  ) =>
  -- | The server id field
  EntityField record (Maybe sid) ->
  -- | The server time field
  EntityField record (Maybe ServerTime) ->
  -- | The changed flag
  EntityField record Bool ->
  -- | The deleted flag
  EntityField record Bool ->
  -- | How to build a synced record from a server id and a timed item
  (sid -> Timed a -> record) ->
  -- | How to read a synced record back into a server id and a timed item
  (record -> (sid, Timed a)) ->
  -- | How to update a row with new data
  --
  -- You only need to perform the updates that have anything to do with the data to sync.
  -- The housekeeping updates are already taken care of.
  (a -> [Update record]) ->
  ClientSyncProcessor (Key record) sid a (SqlPersistT m)
clientSyncProcessor
  serverIdField
  serverTimeField
  changedField
  deletedField
  makeSyncedClientThing
  unmakeSyncedClientThing
  recordUpdates = ClientSyncProcessor {..}
    where
      clientSyncProcessorQuerySyncedButChangedValues :: Set sid -> SqlPersistT m (Map sid (Timed a))
      clientSyncProcessorQuerySyncedButChangedValues si = fmap (M.fromList . map (\(Entity _ r) -> unmakeSyncedClientThing r) . catMaybes) $ forM (S.toList si) $ \sid ->
        selectFirst
          [ serverIdField ==. Just sid,
            serverTimeField !=. Nothing,
            changedField ==. True,
            deletedField ==. False
          ]
          []
      clientSyncProcessorSyncClientAdded :: Map (Key record) (ClientAddition sid) -> SqlPersistT m ()
      clientSyncProcessorSyncClientAdded m = forM_ (M.toList m) $ \(cid, ClientAddition {..}) ->
        update
          cid
          [ serverIdField =. Just clientAdditionId,
            serverTimeField =. Just clientAdditionServerTime
          ]
      clientSyncProcessorSyncClientChanged :: Map sid ServerTime -> SqlPersistT m ()
      clientSyncProcessorSyncClientChanged m = forM_ (M.toList m) $ \(sid, st) ->
        updateWhere
          [serverIdField ==. Just sid]
          [ serverTimeField =. Just st,
            changedField =. False
          ]
      clientSyncProcessorSyncClientDeleted :: Set sid -> SqlPersistT m ()
      clientSyncProcessorSyncClientDeleted s = forM_ (S.toList s) $ \sid ->
        deleteWhere [serverIdField ==. Just sid]
      clientSyncProcessorSyncMergedConflict :: Map sid (Timed a) -> SqlPersistT m ()
      clientSyncProcessorSyncMergedConflict m = forM_ (M.toList m) $ \(sid, Timed a st) ->
        updateWhere
          [serverIdField ==. Just sid]
          $ [ serverTimeField =. Just st,
              changedField =. True
            ]
            ++ recordUpdates a
      clientSyncProcessorSyncServerAdded :: Map sid (Timed a) -> SqlPersistT m ()
      clientSyncProcessorSyncServerAdded m =
        insertMany_ $ map (uncurry makeSyncedClientThing) (M.toList m)
      clientSyncProcessorSyncServerChanged :: Map sid (Timed a) -> SqlPersistT m ()
      clientSyncProcessorSyncServerChanged m = forM_ (M.toList m) $ \(sid, Timed a st) -> do
        updateWhere
          [serverIdField ==. Just sid]
          $ [ serverTimeField =. Just st,
              changedField =. False
            ]
            ++ recordUpdates a
      clientSyncProcessorSyncServerDeleted :: Set sid -> SqlPersistT m ()
      clientSyncProcessorSyncServerDeleted s = forM_ (S.toList s) $ \sid ->
        deleteWhere [serverIdField ==. Just sid]

-- | Set up a client store.
--
-- You shouldn't need this.
setupClientQuery ::
  forall record sid a m.
  ( PersistEntityBackend record ~ SqlBackend,
    ToBackendKey SqlBackend record,
    MonadIO m
  ) =>
  (a -> record) ->
  (sid -> Timed a -> record) ->
  (sid -> Timed a -> record) ->
  (sid -> ServerTime -> record) ->
  ClientStore (Key record) sid a ->
  SqlPersistT m ()
setupClientQuery makeUnsyncedClientThing makeSyncedClientThing makeSyncedButChangedClientThing makeDeletedClientThing ClientStore {..} = do
  forM_ (M.toList clientStoreAddedItems) $ \(cid, t) ->
    insertKey cid $ makeUnsyncedClientThing t
  forM_ (M.toList clientStoreSyncedItems) $ \(sid, tt) ->
    insert_ $ makeSyncedClientThing sid tt
  forM_ (M.toList clientStoreSyncedButChangedItems) $ \(sid, tt) ->
    insert_ $ makeSyncedButChangedClientThing sid tt
  forM_ (M.toList clientStoreDeletedItems) $ \(sid, st) -> insert_ $ makeDeletedClientThing sid st

-- | Get the client store.
--
-- You shouldn't need this.
clientGetStoreQuery ::
  forall record sid a m.
  ( Ord sid,
    PersistEntity record,
    PersistField sid,
    PersistEntityBackend record ~ SqlBackend,
    ToBackendKey SqlBackend record,
    MonadIO m
  ) =>
  EntityField record (Maybe sid) ->
  EntityField record (Maybe ServerTime) ->
  EntityField record Bool ->
  EntityField record Bool ->
  (record -> a) ->
  (record -> (sid, Timed a)) ->
  (record -> (sid, ServerTime)) ->
  SqlPersistT m (ClientStore (Key record) sid a)
clientGetStoreQuery
  serverIdField
  serverTimeField
  changedField
  deletedField
  unmakeUnsyncedClientThing
  unmakeSyncedClientThing
  unmakeDeletedClientThing = do
    clientStoreAddedItems <-
      M.fromList . map (\(Entity cid cr) -> (cid, unmakeUnsyncedClientThing cr))
        <$> selectList
          [ serverIdField ==. Nothing,
            serverTimeField ==. Nothing
          ]
          []
    clientStoreSyncedItems <-
      M.fromList . map (unmakeSyncedClientThing . entityVal)
        <$> selectList
          [ serverIdField !=. Nothing,
            serverTimeField !=. Nothing,
            changedField ==. False,
            deletedField ==. False
          ]
          []
    clientStoreSyncedButChangedItems <-
      M.fromList . map (unmakeSyncedClientThing . entityVal)
        <$> selectList
          [ serverIdField !=. Nothing,
            serverTimeField !=. Nothing,
            changedField ==. True,
            deletedField ==. False
          ]
          []
    clientStoreDeletedItems <-
      M.fromList . map (unmakeDeletedClientThing . entityVal)
        <$> selectList
          [ deletedField ==. True
          ]
          []
    pure ClientStore {..}

-- | Process a sync request on the server side
serverProcessSyncQuery ::
  forall cid record a m.
  ( PersistEntity record,
    PersistEntityBackend record ~ SqlBackend,
    ToBackendKey SqlBackend record,
    MonadIO m
  ) =>
  -- | The server time field
  EntityField record ServerTime ->
  -- | The filters to select the relevant records
  --
  -- Use this if you want per-user syncing
  [Filter record] ->
  -- | How to load an item from the database
  (record -> Timed a) ->
  -- | How to add an item in the database with initial server time
  (a -> record) ->
  -- | How to update a record given new data
  (a -> [Update record]) ->
  -- | A sync request
  SyncRequest cid (Key record) a ->
  SqlPersistT m (SyncResponse cid (Key record) a)
serverProcessSyncQuery
  serverTimeField
  filters
  unmakeFunc
  makeFunc
  recordUpdates =
    processServerSyncCustom $
      serverSyncProcessor
        serverTimeField
        filters
        unmakeFunc
        makeFunc
        recordUpdates

serverSyncProcessor ::
  forall cid record a m.
  ( PersistEntity record,
    PersistEntityBackend record ~ SqlBackend,
    ToBackendKey SqlBackend record,
    MonadIO m
  ) =>
  -- | The server time field
  EntityField record ServerTime ->
  -- | The filters to select the relevant records
  --
  -- Use this if you want per-user syncing
  [Filter record] ->
  -- | How to load an item from the database
  (record -> Timed a) ->
  -- | How to add an item in the database with initial server time
  (a -> record) ->
  -- | How to update a record given new data
  (a -> [Update record]) ->
  ServerSyncProcessor cid (Key record) a (SqlPersistT m)
serverSyncProcessor
  serverTimeField
  filters
  unmakeFunc
  makeFunc
  recordUpdates =
    ServerSyncProcessor {..} :: ServerSyncProcessor cid (Key record) a (SqlPersistT m)
    where
      serverSyncProcessorRead = M.fromList . map (\(Entity i r) -> (i, unmakeFunc r)) <$> selectList filters []
      serverSyncProcessorAddItem = insert . makeFunc
      serverSyncProcessorChangeItem si st a = update si $ (serverTimeField =. st) : recordUpdates a
      serverSyncProcessorDeleteItem = delete

-- | Process a sync request on the server side with a custom id field
--
-- You can use this function if you want to use a UUID as your id instead of the sqlkey of the item.
serverProcessSyncWithCustomIdQuery ::
  forall cid sid record a m.
  ( Ord sid,
    PersistField sid,
    PersistEntityBackend record ~ SqlBackend,
    ToBackendKey SqlBackend record,
    MonadIO m
  ) =>
  -- | The custom id field
  EntityField record sid ->
  -- | The generator to generate the custom id field
  SqlPersistT m sid ->
  -- | The server time field
  EntityField record ServerTime ->
  -- | The filters to select the relevant records
  --
  -- Use this if you want per-user syncing
  [Filter record] ->
  -- | How to load an item from the database
  (record -> (sid, Timed a)) ->
  -- | How to add an item in the database with initial server time
  (sid -> a -> record) ->
  -- | How to update a record given new data
  (a -> [Update record]) ->
  -- | A sync request
  SyncRequest cid sid a ->
  SqlPersistT m (SyncResponse cid sid a)
serverProcessSyncWithCustomIdQuery
  idField
  uuidGen
  serverTimeField
  filters
  unmakeFunc
  makeFunc
  recordUpdates =
    processServerSyncCustom $
      serverSyncWithCustomIdProcessor
        idField
        uuidGen
        serverTimeField
        filters
        unmakeFunc
        makeFunc
        recordUpdates

serverSyncWithCustomIdProcessor ::
  forall cid sid record a m.
  ( Ord sid,
    PersistField sid,
    PersistEntityBackend record ~ SqlBackend,
    ToBackendKey SqlBackend record,
    MonadIO m
  ) =>
  -- | The custom id field
  EntityField record sid ->
  -- | The generator to generate the custom id field
  SqlPersistT m sid ->
  -- | The server time field
  EntityField record ServerTime ->
  -- | The filters to select the relevant records
  --
  -- Use this if you want per-user syncing
  [Filter record] ->
  -- | How to load an item from the database
  (record -> (sid, Timed a)) ->
  -- | How to add an item in the database with 'initialServerTime'
  (sid -> a -> record) ->
  -- | How to update a record given new data
  (a -> [Update record]) ->
  ServerSyncProcessor cid sid a (SqlPersistT m)
serverSyncWithCustomIdProcessor
  idField
  uuidGen
  serverTimeField
  filters
  unmakeFunc
  makeFunc
  recordUpdates = ServerSyncProcessor {..} :: ServerSyncProcessor cid sid a (SqlPersistT m)
    where
      serverSyncProcessorRead = M.fromList . map (\(Entity _ record) -> unmakeFunc record) <$> selectList filters []
      serverSyncProcessorAddItem a = do
        uuid <- uuidGen
        insert_ $ makeFunc uuid a
        pure uuid
      serverSyncProcessorChangeItem si st a = updateWhere [idField ==. si] $ (serverTimeField =. st) : recordUpdates a
      serverSyncProcessorDeleteItem si = deleteWhere [idField ==. si]

-- | Set up the server store
--
-- You shouldn't need this.
setupServerQuery ::
  forall sid record a.
  ( PersistEntity record,
    PersistEntityBackend record ~ SqlBackend
  ) =>
  (sid -> Timed a -> Entity record) ->
  ServerStore sid a ->
  SqlPersistT IO ()
setupServerQuery func ServerStore {..} =
  forM_ (M.toList serverStoreItems) $ \(sid, tt) ->
    let (Entity k r) = func sid tt
     in insertKey k r

-- | Get the server store
--
-- You shouldn't need this.
serverGetStoreQuery ::
  ( Ord sid,
    PersistEntity record,
    PersistEntityBackend record ~ SqlBackend
  ) =>
  (Entity record -> (sid, Timed a)) ->
  SqlPersistT IO (ServerStore sid a)
serverGetStoreQuery func = ServerStore . M.fromList . map func <$> selectList [] []