-- | Data structures to define and manipulate tokens
module Gamgee.Token
    ( TokenType (..)
    , TokenLabel (..)
    , TokenSecret (..)
    , TokenIssuer (..)
    , TokenAlgorithm (..)
    , TokenDigits (..)
    , TokenPeriod (..)
    , TokenSpec (..)
    , TokenIdentifier (..)
    , Tokens
    , Config(..)
    , getIdentifier
    , currentConfigVersion
    , initialConfig
    ) where

import qualified Data.Aeson as Aeson
import qualified Data.Text  as Text
import           Relude


-- | Type of token TOTP or HOTP (not supported yet)
data TokenType = TOTP
  deriving stock Show

instance Aeson.FromJSON TokenType where
  parseJSON (Aeson.String "totp") = return TOTP
  parseJSON invalid               = fail $ "Invalid token type: " ++ show invalid

instance Aeson.ToJSON TokenType where
  toJSON TOTP = Aeson.String "totp"

-- | Label of the token
newtype TokenLabel = TokenLabel {
  unTokenLabel :: Text
  }
  deriving newtype (Show, IsString, Aeson.FromJSON, Aeson.ToJSON)

-- | Secret used to generate OTPs
data TokenSecret = TokenSecretPlainText Text
                 | TokenSecretAES256 {
                     tokenSecretAES256IV     :: Text
                     , tokenSecretAES256Data :: Text
                     }
                 deriving stock    (Show, Generic)
                 deriving anyclass (Aeson.FromJSON, Aeson.ToJSON)

-- | Optional issuer of this token
newtype TokenIssuer = TokenIssuer {
  unTokenIssuer :: Text
  }
  deriving newtype (Show, IsString, Aeson.FromJSON, Aeson.ToJSON)

data TokenAlgorithm = AlgorithmSHA1
                    | AlgorithmSHA256
                    | AlgorithmSHA512
                    deriving stock    (Show, Generic)
                    deriving anyclass (Aeson.FromJSON, Aeson.ToJSON)

data TokenDigits = Digits6
                 | Digits8
                 deriving stock Show

instance Aeson.FromJSON TokenDigits where
  parseJSON (Aeson.Number 6) = return Digits6
  parseJSON (Aeson.Number 8) = return Digits8
  parseJSON invalid          = fail $ "Invalid number of digits: " ++ show invalid ++ ". Must be 6 or 8."

instance Aeson.ToJSON TokenDigits where
  toJSON Digits6 = Aeson.Number 6
  toJSON Digits8 = Aeson.Number 8

-- | Refresh interval of the token in seconds
newtype TokenPeriod = TokenPeriod {
  unTokenPeriod :: Word16
  }
  deriving newtype (Eq, Ord, Enum, Num, Real, Integral, Show, Aeson.FromJSON, Aeson.ToJSON)

data TokenSpec = TokenSpec {
  -- | TOTP/HOTP token
  tokenType        :: TokenType
  -- | A short unique label for this token used to identify it
  , tokenLabel     :: TokenLabel
  -- | The secret provided by the issuer to generate tokens
  , tokenSecret    :: TokenSecret
  -- | The name of the issuer
  , tokenIssuer    :: TokenIssuer
  -- | SHA algorithm used to generate tokens
  , tokenAlgorithm :: TokenAlgorithm
  -- | Number of digits in the token - 6 or 8
  , tokenDigits    :: TokenDigits
  -- | Refresh interval of the token - typically 30 sec
  , tokenPeriod    :: TokenPeriod
  }
  deriving stock    (Generic, Show)
  deriving anyclass (Aeson.FromJSON, Aeson.ToJSON)

-- An identifier for a token. This is derived from the label and issuer
newtype TokenIdentifier = TokenIdentifier {
  unTokenIdentifier :: Text
  }
  deriving newtype (Eq, Show, Hashable, IsString, Semigroup, ToString
                   , Aeson.FromJSON, Aeson.ToJSON
                   , Aeson.FromJSONKey, Aeson.ToJSONKey)

getIdentifier :: TokenSpec -> TokenIdentifier
getIdentifier spec =
  let
    TokenLabel label = tokenLabel spec
    TokenIssuer issuer = tokenIssuer spec
  in
    TokenIdentifier $ if | Text.null issuer                      -> label
                         | Text.isPrefixOf (issuer <> ":") label -> label
                         | otherwise                             -> issuer <> ":" <> label

type Tokens = HashMap TokenIdentifier TokenSpec

----------------------------------------------------------------------------------------------------
-- Gamgee Configuration
----------------------------------------------------------------------------------------------------

data Config = Config {
  configVersion  :: Word32
  , configTokens :: Tokens
  }
  deriving stock    (Generic)
  deriving anyclass (Aeson.FromJSON, Aeson.ToJSON)

currentConfigVersion :: Word32
currentConfigVersion = 1

initialConfig :: Config
initialConfig = Config {
  configVersion = currentConfigVersion
  , configTokens = fromList []
  }