module Gamgee.Operation
  ( addToken
  , deleteToken
  , listTokens
  , getOTP
  , getInfo
  , changePassword
  ) where


import           Data.Aeson            ((.=))
import qualified Data.Aeson            as Aeson
import qualified Data.Time.Clock.POSIX as Clock
import qualified Data.Version          as Version
import qualified Gamgee.Effects        as Eff
import qualified Gamgee.Token          as Token
import           Paths_gamgee          (version)
import           Polysemy              (Members, Sem)
import qualified Polysemy.Error        as P
import qualified Polysemy.Input        as P
import qualified Polysemy.Output       as P
import qualified Polysemy.State        as P
import           Relude
import qualified Relude.Extra.Map      as Map


addToken :: Members [ P.State Token.Tokens
                    , Eff.Crypto
                    , Eff.SecretInput Text
                    , P.Error Eff.EffError ] r
         => Token.TokenSpec
         -> Sem r ()
addToken spec = do
  let ident = Token.getIdentifier spec
  tokens <- P.get @Token.Tokens
  if ident `Map.member` tokens
  then P.throw $ Eff.AlreadyExists ident
  else do
    spec' <- Eff.encryptSecret spec
    P.put $ Map.insert ident spec' tokens

deleteToken :: Members [ P.State Token.Tokens
                       , P.Error Eff.EffError ] r
            => Token.TokenIdentifier
            -> Sem r ()
deleteToken ident = do
  tokens <- P.get @Token.Tokens
  case Map.lookup ident tokens of
    Nothing -> P.throw $ Eff.NoSuchToken ident
    Just _  -> P.put $ Map.delete ident tokens

listTokens :: Members [ P.State Token.Tokens
                      , P.Output Text ] r
           => Sem r ()
listTokens = do
  tokens <- P.get @Token.Tokens
  mapM_ (P.output . Token.unTokenIdentifier . Token.getIdentifier) tokens

getOTP :: Members [ P.State Token.Tokens
                  , P.Error Eff.EffError
                  , P.Output Text
                  , Eff.TOTP ] r
       => Token.TokenIdentifier
       -> Clock.POSIXTime
       -> Sem r ()
getOTP ident time = do
  tokens <- P.get @Token.Tokens
  case Map.lookup ident tokens of
    Nothing   -> P.throw $ Eff.NoSuchToken ident
    Just spec -> Eff.getTOTP spec time >>= P.output

getInfo :: Members [ P.Input FilePath
                   , P.Output Aeson.Value ] r
        => Sem r (Maybe Token.Config)
        -> Sem r ()
getInfo cfg = do
  path <- P.input @FilePath

  -- Info command should work even if the config file can't be read. So we handle the
  -- potential "missing" config here with a `Maybe Config`.
  cfgVersion <- maybe Aeson.Null (Aeson.toJSON . Token.configVersion) <$> cfg

  let info = Aeson.object [ "version" .= Version.showVersion version
                          , "config"  .= Aeson.object [ "filepath" .= path
                                                      , "version"  .= cfgVersion ]
                          ]
  P.output info

changePassword :: Members [ P.State Token.Tokens
                          , Eff.SecretInput Text
                          , Eff.Crypto
                          , Eff.TOTP
                          , P.Error Eff.EffError ] r
               => Token.TokenIdentifier
               -> Sem r ()
changePassword ident = do
  tokens <- P.get @Token.Tokens
  case Map.lookup ident tokens of
    Nothing   -> P.throw $ Eff.NoSuchToken ident
    Just spec -> do
      secret <- Eff.getSecret spec
      spec' <- Eff.encryptSecret spec{ Token.tokenSecret = Token.TokenSecretPlainText secret }
      P.put $ Map.insert ident spec' tokens