{-# LANGUAGE RecordWildCards #-}

module Control.Flipper.Adapters.Postgres.Query
    ( getFeatures
    , getFeatureByName
    , addFeature
    , upsertFeature
    , actorCount
    , featureCount
    , M.mkFeature
    ) where

import           Control.Monad                              (forM_, void)
import           Data.Set                                   (Set)
import qualified Data.Set                                   as S
import           Data.Time.Clock                            (getCurrentTime)

import           Control.Flipper.Adapters.Postgres.DBAccess as DB
import           Control.Flipper.Adapters.Postgres.Models   as M
import qualified Control.Flipper.Types                      as T
import           Control.Monad.IO.Class                     (MonadIO, liftIO)

{- |
Selects all feature records
Returns domain model
-}
getFeatures :: (MonadIO app, Monad m)
            => DBAccess m -> app T.Features
getFeatures dbAccess = modelsToFeatures <$> getFeatures' dbAccess

{- |
Selects all feature records
Returns database entities
-}
getFeatures' :: (MonadIO app, Monad m)
            => DBAccess m -> app [Entity Feature]
getFeatures' DBAccess{..} = liftIO $ runDb selectFeatures

{- |
Selects a feature record by its unique name
Returns a domain model
-}
getFeatureByName :: (MonadIO app, Monad m)
                 => T.FeatureName -> DBAccess m -> app (Maybe T.Feature)
getFeatureByName fName dbAccess@DBAccess{..} = do
    mFeatureEnt <- getFeatureByName' fName dbAccess
    case mFeatureEnt of
        Nothing                   -> return Nothing
        (Just (Entity fId feature)) -> do
            -- use Esqueleto to join this relation
            actors <- liftIO $ runDb (selectActorsByFeatureId fId)
            let f = (modelToFeature feature) { T.enabledActors = S.fromList (map (actorActorId . entityVal) actors) }
            return . Just $ f

{- |
Selects a feature record by its unique name
Returns a database entity
-}
getFeatureByName' :: (MonadIO app, Monad m)
                 => T.FeatureName -> DBAccess m -> app (Maybe (Entity Feature))
getFeatureByName' fName DBAccess{..} = liftIO $ runDb (findFeature fName)

{- |
Inserts a new feature record if one with a matching name does not already exist.
Updates an existing feature record if one with a matching name already exists.
-}
upsertFeature :: (MonadIO app, Monad m)
              => T.Feature -> DBAccess m -> app ()
upsertFeature feature dbAccess = do
    mFeature <- getFeatureByName' (T.featureName feature) dbAccess
    case mFeature of
        Nothing ->
            liftIO (featureToModel feature) >>= void . flip addFeature' dbAccess
        (Just (Entity fId _)) -> do
            updatedFeature <- liftIO $ featureToModel feature
            replaceFeature fId updatedFeature dbAccess

{- |
Inserts a new feature record and all associated actors.
-}
addFeature :: (MonadIO app, Monad m)
           => T.Feature -> DBAccess m -> app (Key Feature)
addFeature feature dbAccess = do
    model <- liftIO $ featureToModel feature
    addFeature' model dbAccess

{- |
Inserts a new feature record and all associated actors.
-}
addFeature' :: (MonadIO app, Monad m)
           => FeatureWithActorIds -> DBAccess m -> app (Key Feature)
addFeature' (feature, actorIds) dbAccess@DBAccess{..} = do
    key <- liftIO $ runDb (insertFeature feature)
    addActors actorIds key dbAccess
    return key

addActors :: (MonadIO app, Monad m)
          => Set T.ActorId -> FeatureId -> DBAccess m -> app ()
addActors actorIds fId DBAccess{..} =
    liftIO $ forM_ actorIds $ \aId ->
        actorIdToModel aId fId >>= runDb . insertActor

deleteActors :: (MonadIO app, Monad m)
             => Set T.ActorId -> FeatureId -> DBAccess m -> app ()
deleteActors actorIds fId DBAccess{..} =
    liftIO $ forM_ actorIds $ runDb . deleteActor fId

{- |
Updates an existing feature record.
-}
replaceFeature :: (MonadIO app, Monad m)
               => FeatureId -> FeatureWithActorIds -> DBAccess m -> app ()
replaceFeature fId (feature, newActorIds) dbAccess@DBAccess{..} = do
    now <- liftIO getCurrentTime
    oldActorIds <- (S.fromList . map (actorActorId . entityVal)) <$> liftIO (runDb (selectActorsByFeatureId fId))

    let (toAdd, toDelete) = actorDiff oldActorIds newActorIds
    addActors toAdd fId dbAccess
    deleteActors toDelete fId dbAccess

    liftIO $ runDb (updateFeature fId (feature { featureUpdated = now }))

actorDiff :: Set T.ActorId -> Set T.ActorId -> (Set T.ActorId, Set T.ActorId)
actorDiff oldActorIds newActorIds =
    let actorIdsToAdd = S.difference newActorIds oldActorIds
        actorIdsToDelete = S.difference oldActorIds newActorIds
    in (actorIdsToAdd, actorIdsToDelete)

{- |
Returns a count of all feature records
-}
featureCount :: (MonadIO app, Monad m)
             => DBAccess m -> app Int
featureCount DBAccess{..} = liftIO $ runDb countFeatures

{- |
Returns a count of all actor records
-}
actorCount :: (MonadIO app, Monad m)
             => DBAccess m -> app Int
actorCount DBAccess{..} = liftIO $ runDb countActors