{-# LANGUAGE BangPatterns               #-}
{-# LANGUAGE DataKinds                  #-}
{-# LANGUAGE DeriveAnyClass             #-}
{-# LANGUAGE DeriveGeneric              #-}
{-# LANGUAGE DerivingStrategies         #-}
{-# LANGUAGE FlexibleInstances          #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase                 #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE RecordWildCards            #-}
{-# LANGUAGE ScopedTypeVariables        #-}
{-# LANGUAGE StandaloneDeriving         #-}
{-# LANGUAGE TemplateHaskell            #-}
{-# LANGUAGE TupleSections              #-}
{-# LANGUAGE TypeApplications           #-}
{-# LANGUAGE TypeFamilies               #-}
{-# LANGUAGE TypeOperators              #-}

module Server ( loadConfig
              , Org(..)
              , Sha1
              , Config, tmpdir, port, verbose, orgs, interval, exneuland
              , RemoteRef(..)
              , ServerState, emptyState, unState
              , JobStatus(..)
              , setJobStatus
              , prettySha,getJobStatus) where

import           Universum

import           CheckDir                   (DirResult)
import           Control.Arrow              ((>>>))
import           Control.Concurrent         (modifyMVar_, withMVar)
import           Crypto.Hash.SHA1           (hash)
import           Data.Aeson                 (FromJSON, ToJSON, ToJSONKey (..),
                                             eitherDecodeFileStrict')
import qualified Data.Aeson                 as A
import qualified Data.ByteString.Base64.URL as Base64
import           Data.Functor.Contravariant (contramap)
import qualified Data.Map                   as M
import           Lens.Micro.Platform        (at, ix, makeLenses, traverseOf)
import           LintConfig                 (LintConfig')
import           Servant                    (FromHttpApiData)
import           Servant.Client             (BaseUrl,
                                             parseBaseUrl)
import           Toml                       (BiMap (BiMap), TomlBiMap,
                                             TomlBiMapError (ArbitraryError),
                                             TomlCodec,
                                             prettyTomlDecodeErrors, (.=))
import qualified Toml                       as T
import Data.Either.Extra (mapLeft)

-- | a reference in a remote git repository
data RemoteRef = RemoteRef
  { repourl  :: Text
  , reporef  :: Text
  , reponame :: Text
  -- ^ the "world name" for the hub / world:// links
  } deriving (Generic, FromJSON, ToJSON, Eq, Ord, Show)

type family ConfigRes (b :: Bool) a where
  ConfigRes True a = a
  ConfigRes False a = FilePath

-- | the internal text is actually already base64-encoded
newtype Sha1 = Sha1 Text
  deriving newtype (Eq, Show, Ord, FromHttpApiData, ToJSON)

-- | base64-encoded sha1
prettySha :: Sha1 -> Text
prettySha (Sha1 text) = text

instance ToJSONKey Sha1

toSha :: RemoteRef -> Sha1
toSha ref = Sha1
  . decodeUtf8
  . Base64.encode
  . hash
  . encodeUtf8
  $ (show ref :: Text)

data Org (loaded :: Bool) = Org
  { orgSlug       :: Text
  , orgLintconfig :: ConfigRes loaded LintConfig'
  , orgEntrypoint :: FilePath
  , orgRepos      :: [RemoteRef]
  , orgUrl        :: Text
  , orgWebdir     :: Text
  } deriving Generic

-- | Orgs are compared via their slugs only
-- TODO: the server should probably refuse to start if two orgs have the
-- same slug … (or really the toml format shouldn't allow that syntactically)
instance Eq (Org True) where
  a == b = orgSlug a == orgSlug b

instance Ord (Org True) where
  a <= b = orgSlug a <= orgSlug b

-- this instance exists since it's required for ToJSONKey,
-- but it shouldn't really be used
instance ToJSON (Org True) where
  toJSON Org { .. } = A.object [ "slug" A..= orgSlug ]

-- orgs used as keys just reduce to their slug
instance ToJSONKey (Org True) where
  toJSONKey = contramap orgSlug (toJSONKey @Text)

-- | the server's configuration
data Config (loaded :: Bool) = Config
  { _tmpdir    :: FilePath
  -- ^ dir to clone git things in
  , _port      :: Int
  , _verbose   :: Bool
  , _interval  :: Int
  -- ^ port to bind to
  , _exneuland :: BaseUrl
  , _orgs      :: [Org loaded]
  } deriving Generic

makeLenses ''Config


remoteCodec :: TomlCodec RemoteRef
remoteCodec = RemoteRef
  <$> T.text "url" .= repourl
  <*> T.text "ref" .= reporef
  <*> T.text "name" .= reponame

orgCodec :: TomlCodec (Org False)
orgCodec = Org
  <$> T.text "slug" .= orgSlug
  <*> T.string "lintconfig" .= orgLintconfig
  <*> T.string "entrypoint" .= orgEntrypoint
  <*> T.list remoteCodec "repo" .= orgRepos
  <*> T.text "url" .= orgUrl
  <*> T.text "webdir" .= orgWebdir

-- why exactly does everything in tomland need to be invertable
urlBimap :: TomlBiMap BaseUrl String
urlBimap = BiMap
  (Right . show)
  (mapLeft (ArbitraryError . show) . parseBaseUrl)

configCodec :: TomlCodec (Config False)
configCodec = Config
    <$> T.string "tmpdir" .= _tmpdir
    <*> T.int "port" .= _port
    <*> T.bool "verbose" .= _verbose
    <*> T.int "interval" .= _interval
    <*> T.match (urlBimap >>> T._String) "exneuland" .= _exneuland
    <*> T.list orgCodec "org" .= _orgs

-- | a job status (of a specific uuid)
data JobStatus =
  Pending | Linted DirResult Text | Failed Text
  deriving (Generic, ToJSON)

-- | the server's global state; might eventually end up with more
-- stuff in here, hence the newtype
newtype ServerState = ServerState
  { _unState :: Map (Org True) (Map Sha1 (RemoteRef, JobStatus)) }

makeLenses ''ServerState

-- | the inital state must already contain empty orgs, since setJobStatus
-- will default to a noop otherwise
emptyState :: Config True -> ServerState
emptyState config = ServerState
  $ M.fromList $ map (, mempty) (view orgs config)

-- | loads a config, along with all things linked in it
-- (e.g. linterconfigs for each org)
loadConfig :: FilePath -> IO (Config True)
loadConfig path = do
  res <- T.decodeFileEither configCodec path
  case res of
    Right config -> traverseOf orgs (mapM loadOrg) config
    Left err     -> error $ prettyTomlDecodeErrors err
    where
      loadOrg :: Org False -> IO (Org True)
      loadOrg org = do
        lintconfig <- eitherDecodeFileStrict' (orgLintconfig org) >>= \case
          Right c  -> pure c
          Left err -> error $ show err
        pure $ org { orgLintconfig = lintconfig }


-- | NOTE: this does not create the org if it does not yet exist!
setJobStatus :: MVar ServerState -> Org True -> RemoteRef -> JobStatus -> IO ()
setJobStatus mvar !org !ref !status = modifyMVar_ mvar
  $ pure . over (unState . ix org . at (toSha ref)) (const $ Just (ref, status))

getJobStatus :: MVar ServerState -> Text -> Sha1 -> IO (Maybe (RemoteRef, JobStatus))
getJobStatus mvar orgslug sha = withMVar mvar $ \state ->
  pure (M.lookup sha (view (unState . ix (Org { orgSlug = orgslug })) state))