{-# LANGUAGE DataKinds             #-}
{-# LANGUAGE DeriveGeneric         #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE KindSignatures        #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE RecordWildCards       #-}
{-# LANGUAGE ScopedTypeVariables   #-}

{-|
Module      : Kubernetes.KubeConfig
Description : Data model for the kubeconfig.

This module contains the definition of the data model of the kubeconfig.

The official definition of the kubeconfig is defined in https://github.com/kubernetes/client-go/blob/master/tools/clientcmd/api/v1/types.go.

This is a mostly straightforward translation into Haskell, with 'FromJSON' and 'ToJSON' instances defined.
-}
module Kubernetes.KubeConfig where

import           Data.Aeson     (FromJSON (..), Options, ToJSON (..),
                                 Value (..), camelTo2, defaultOptions,
                                 fieldLabelModifier, genericParseJSON,
                                 genericToJSON, object, omitNothingFields,
                                 withObject, (.:), (.=))
import qualified Data.Map       as Map
import           Data.Proxy
import           Data.Semigroup ((<>))
import           Data.Text      (Text)
import qualified Data.Text      as T
import           Data.Typeable
import           GHC.Generics
import           GHC.TypeLits

camelToWithOverrides :: Char -> Map.Map String String -> Options
camelToWithOverrides c overrides = defaultOptions
    { fieldLabelModifier = modifier
    , omitNothingFields  = True
    }
    where modifier s = Map.findWithDefault (camelTo2 c s) s overrides

-- |Represents a kubeconfig.
data Config = Config
  { kind           :: Maybe Text
  , apiVersion     :: Maybe Text
  , preferences    :: Preferences
  , clusters       :: [NamedEntity Cluster "cluster"]
  , authInfos      :: [NamedEntity AuthInfo "user"]
  , contexts       :: [NamedEntity Context "context"]
  , currentContext :: Text
  } deriving (Eq, Generic, Show)

configJSONOptions = camelToWithOverrides
    '-'
    (Map.fromList [("apiVersion", "apiVersion"), ("authInfos", "users")])

instance ToJSON Config where
  toJSON = genericToJSON configJSONOptions

instance FromJSON Config where
  parseJSON = genericParseJSON configJSONOptions

newtype Preferences = Preferences
  { colors :: Maybe Bool
  } deriving (Eq, Generic, Show)

instance ToJSON Preferences where
  toJSON = genericToJSON $ camelToWithOverrides '-' Map.empty

instance FromJSON Preferences where
  parseJSON = genericParseJSON $ camelToWithOverrides '-' Map.empty

data Cluster = Cluster
  { server                   :: Text
  , insecureSkipTLSVerify    :: Maybe Bool
  , certificateAuthority     :: Maybe Text
  , certificateAuthorityData :: Maybe Text
  } deriving (Eq, Generic, Show, Typeable)

instance ToJSON Cluster where
  toJSON = genericToJSON $ camelToWithOverrides '-' Map.empty

instance FromJSON Cluster where
  parseJSON = genericParseJSON $ camelToWithOverrides '-' Map.empty

data NamedEntity a (typeKey :: Symbol) = NamedEntity
  { name   :: Text
  , entity :: a } deriving (Eq, Generic, Show)

instance (FromJSON a, Typeable a, KnownSymbol s) =>
         FromJSON (NamedEntity a s) where
  parseJSON = withObject ("Named" <> (show $ typeOf (undefined :: a))) $ \v ->
    NamedEntity <$> v .: "name" <*> v .: T.pack (symbolVal (Proxy :: Proxy s))

instance (ToJSON a, KnownSymbol s) =>
         ToJSON (NamedEntity a s) where
  toJSON (NamedEntity {..}) = object
      ["name" .= toJSON name, T.pack (symbolVal (Proxy :: Proxy s)) .= toJSON entity]

toMap :: [NamedEntity a s] -> Map.Map Text a
toMap = Map.fromList . fmap (\NamedEntity {..} -> (name, entity))

data AuthInfo = AuthInfo
  { clientCertificate     :: Maybe FilePath
  , clientCertificateData :: Maybe Text
  , clientKey             :: Maybe FilePath
  , clientKeyData         :: Maybe Text
  , token                 :: Maybe Text
  , tokenFile             :: Maybe FilePath
  , impersonate           :: Maybe Text
  , impersonateGroups     :: Maybe [Text]
  , impersonateUserExtra  :: Maybe (Map.Map Text [Text])
  , username              :: Maybe Text
  , password              :: Maybe Text
  , authProvider          :: Maybe AuthProviderConfig
  } deriving (Eq, Generic, Show, Typeable)

authInfoJSONOptions = camelToWithOverrides
    '-'
    ( Map.fromList
        [ ("tokenFile"           , "tokenFile")
        , ("impersonate"         , "as")
        , ("impersonateGroups"   , "as-groups")
        , ("impersonateUserExtra", "as-user-extra")
        ]
    )

instance ToJSON AuthInfo where
  toJSON = genericToJSON authInfoJSONOptions

instance FromJSON AuthInfo where
  parseJSON = genericParseJSON authInfoJSONOptions

data Context = Context
  { cluster   :: Text
  , authInfo  :: Text
  , namespace :: Maybe Text
  } deriving (Eq, Generic, Show, Typeable)

contextJSONOptions =
    camelToWithOverrides '-' (Map.fromList [("authInfo", "user")])

instance ToJSON Context where
  toJSON = genericToJSON contextJSONOptions

instance FromJSON Context where
  parseJSON = genericParseJSON contextJSONOptions

data AuthProviderConfig = AuthProviderConfig
  { name   :: Text
  , config :: Maybe (Map.Map Text Text)
  } deriving (Eq, Generic, Show)

instance ToJSON AuthProviderConfig where
  toJSON = genericToJSON $ camelToWithOverrides '-' Map.empty

instance FromJSON AuthProviderConfig where
  parseJSON = genericParseJSON $ camelToWithOverrides '-' Map.empty

-- |Returns the currently active context.
getContext :: Config -> Either String Context
getContext Config {..} =
    let maybeContext = Map.lookup currentContext (toMap contexts)
    in  case maybeContext of
            Just ctx -> Right ctx
            Nothing  -> Left ("No context named " <> T.unpack currentContext)

-- |Returns the currently active user.
getAuthInfo :: Config -> Either String (Text, AuthInfo)
getAuthInfo cfg@Config {..} = do
    Context {..} <- getContext cfg
    let maybeAuth = Map.lookup authInfo (toMap authInfos)
    case maybeAuth of
        Just auth -> Right (authInfo, auth)
        Nothing   -> Left ("No user named " <> T.unpack authInfo)

-- |Returns the currently active cluster.
getCluster :: Config -> Either String Cluster
getCluster cfg@Config {clusters=clusters} = do
    Context {cluster=clusterName} <- getContext cfg
    let maybeCluster = Map.lookup clusterName (toMap clusters)
    case maybeCluster of
        Just cluster -> Right cluster
        Nothing      -> Left ("No cluster named " <> T.unpack clusterName)
