{-# LANGUAGE QuasiQuotes     #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}

module Lib.Error where

import           Startlude

import           Control.Carrier.Error.Church
import           Data.Aeson              hiding ( Error )
import           Data.String.Interpolate.IsString
import qualified Data.Yaml                     as Yaml
import qualified GHC.Show                       ( Show(..) )
import           Network.HTTP.Types
import           System.Process
import           Yesod.Core              hiding ( ErrorResponse )

import           Lib.SystemPaths
import           Lib.Types.Core
import           Lib.Types.Emver


type S9ErrT m = ExceptT S9Error m

data S9Error =
      ProductKeyE
    | RegistrationE
    | NoCompliantAgentE VersionRange
    | PersistentE Text
    | WifiConnectionE
    | AppMgrParseE Text Text String
    | AppMgrInvalidConfigE Text
    | AppMgrE Text Int
    | EjectE Int
    | AvahiE Text
    | MetricE Text
    | AppMgrVersionE Version VersionRange
    | RegistryUnreachableE
    | RegistryParseE Text Text
    | AppNotInstalledE AppId
    | AppStateActionIncompatibleE AppId AppStatus AppAction
    | UpdateSelfE UpdateSelfStep Text
    | InvalidSshKeyE Text
    | InvalidSsidE
    | InvalidPskE
    | InvalidRequestE Value Text
    | NotFoundE Text Text
    | UpdateInProgressE
    | TemporarilyForbiddenE AppId Text Text
    | TorServiceTimeoutE
    | NginxSslE Text
    | WifiOrphaningE
    | NoPasswordExistsE
    | HostsParamsE Text
    | ParamsE Text
    | MissingFileE SystemPath
    | ClientCryptographyE Text
    | TTLExpirationE Text
    | ManifestParseE AppId Yaml.ParseException
    | EnvironmentValE AppId
    | InternalE Text
    | BackupE AppId Text
    | BackupPassInvalidE
    | OpenSslE Text Int String String
data UpdateSelfStep =
      GetLatestCompliantVersion
    | GetYoungAgentBinary
    | ShutdownWeb
    | StartupYoungAgent
    | PingYoungAgent ProcessHandle
instance Show S9Error where
    show = show . toError

instance Exception S9Error

newtype InternalS9Error = InternalS9Error Text deriving (Eq, Show)
instance Exception InternalS9Error

-- | Redact any sensitive data in this function
toError :: S9Error -> ErrorResponse
toError = \case
    ProductKeyE            -> ErrorResponse PRODUCT_KEY_ERROR "The product key is invalid"
    RegistrationE          -> ErrorResponse REGISTRATION_ERROR "The product already has an owner"
    NoCompliantAgentE spec -> ErrorResponse AGENT_UPDATE_ERROR [i|No valid agent version for spec #{spec}|]
    PersistentE       t    -> ErrorResponse DATABASE_ERROR t
    WifiConnectionE        -> ErrorResponse WIFI_ERROR "Could not connect to wifi"
    AppMgrInvalidConfigE e -> ErrorResponse APPMGR_CONFIG_ERROR e
    AppMgrParseE cmd result e ->
        ErrorResponse APPMGR_PARSE_ERROR [i|"appmgr #{cmd}" yielded an unparseable result:#{result}\nError: #{e}|]
    AppMgrE cmd code -> ErrorResponse APPMGR_ERROR [i|"appmgr #{cmd}" exited with #{code}|]
    EjectE code -> ErrorResponse EJECT_ERROR [i|"eject" command exited with #{code}|]
    AppMgrVersionE av avs ->
        ErrorResponse APPMGR_ERROR [i|"appmgr version #{av}" fails to satisfy requisite spec #{avs}|]
    AvahiE  e               -> ErrorResponse AVAHI_ERROR [i|#{e}|]
    MetricE m               -> ErrorResponse METRICS_ERROR [i|failed to provide metrics: #{m}|]
    RegistryUnreachableE    -> ErrorResponse REGISTRY_ERROR [i|registry is unreachable|]
    RegistryParseE path msg -> ErrorResponse REGISTRY_ERROR [i|registry "#{path}" failed to parse: #{msg}|]
    AppNotInstalledE appId  -> ErrorResponse APP_NOT_INSTALLED [i|#{appId} is not installed|]
    AppStateActionIncompatibleE appId status action -> ErrorResponse APP_ACTION_FORBIDDEN $ case (status, action) of
        (AppStatusAppMgr Dead, _) -> [i|#{appId} cannot be #{action}ed because it is dead...contact support?|]
        (AppStatusAppMgr Removing, _) -> [i|#{appId} cannot be #{action}ed because it is being removed|]
        (AppStatusAppMgr Running, Start) -> [i|#{appId} is already running|]
        (AppStatusAppMgr Stopped, Stop) -> [i|#{appId} is already stopped|]
        (AppStatusAppMgr Restarting, Start) -> [i|#{appId} is already running|]
        (AppStatusAppMgr Running, Stop) -> [i|Running apps should be stoppable, this is a bug, contact support|]
        (AppStatusAppMgr Stopped, Start) -> [i|Stopped apps should be startable, this is a bug, contact support|]
        (AppStatusAppMgr Restarting, Stop) -> [i|Restarting apps should be stoppable, this is a bug, contact support|]
        (AppStatusAppMgr Paused, _) -> [i|Paused is not an externally visible state, this is a bug, contact support|]
        (AppStatusTmp NeedsConfig, Start) -> [i|#{appId} cannot be started because it is not configured|]
        (AppStatusTmp NeedsConfig, Stop) -> [i|#{appId} is already stopped|]
        (AppStatusTmp BrokenDependencies, Start) -> [i|Cannot start service: Dependency Issue|]
        (AppStatusTmp _, _) -> [i|Cannot issue control actions to apps in temporary states|]
    UpdateSelfE step e -> ErrorResponse SELF_UPDATE_ERROR $ case step of
        GetLatestCompliantVersion -> [i|could not find a compliant version for the specification|]
        GetYoungAgentBinary       -> [i|could not get young agent binary: #{e}|]
        ShutdownWeb               -> [i|could not shutdown web: #{e}|]
        StartupYoungAgent         -> [i|could not startup young agent: #{e}|]
        PingYoungAgent _          -> [i|could not ping young agent: #{e}|]
    InvalidSshKeyE key -> ErrorResponse INVALID_SSH_KEY [i|The ssh key "#{key}" is invalid|]
    InvalidSsidE -> ErrorResponse INVALID_SSID [i|The ssid is invalid. Only ASCII characters allowed.|]
    InvalidPskE -> ErrorResponse INVALID_SSID [i|The wifi password is invalid. Only ASCII characters allowed.|]
    InvalidRequestE val reason -> ErrorResponse INVALID_REQUEST [i|The body #{encode val} is invalid: #{reason}|]
    NotFoundE resource val -> ErrorResponse RESOURCE_NOT_FOUND [i|The #{resource} #{val} was not found|]
    UpdateInProgressE ->
        ErrorResponse UPDATE_IN_PROGRESS [i|Your request could not be completed because your server is updating|]
    TemporarilyForbiddenE appId action st ->
        ErrorResponse APP_ACTION_FORBIDDEN [i|The #{action} for #{appId} is temporarily forbidden because it is #{st}|]
    TorServiceTimeoutE ->
        ErrorResponse INTERNAL_ERROR [i|The MeshOS Tor Service could not be started...contact support|]
    NginxSslE e    -> ErrorResponse INTERNAL_ERROR [i|MeshOS could not be started with SSL #{e}|]
    WifiOrphaningE -> ErrorResponse
        WIFI_ERROR
        [i|You cannot delete the wifi network you are currently connected to unless on ethernet|]
    ManifestParseE appId e ->
        ErrorResponse INTERNAL_ERROR [i|There was an error inspecting the manifest for #{appId}: #{e}|]
    NoPasswordExistsE         -> ErrorResponse REGISTRATION_ERROR [i|Unauthorized. No password has been registered|]
    MissingFileE        sp    -> ErrorResponse RESOURCE_NOT_FOUND [i|File not found as #{leaf sp}|]
    ClientCryptographyE desc  -> ErrorResponse REGISTRATION_ERROR [i|Cryptography failure: #{desc}|]
    TTLExpirationE      desc  -> ErrorResponse REGISTRATION_ERROR [i|TTL Expiration failure: #{desc}|]
    EnvironmentValE     appId -> ErrorResponse SYNCHRONIZATION_ERROR [i|Could not read environment values for #{appId}|]
    HostsParamsE        key   -> ErrorResponse REGISTRATION_ERROR [i|Missing or invalid parameter #{key}|]
    ParamsE             key   -> ErrorResponse INVALID_REQUEST [i|Missing or invalid parameter #{key}|]
    InternalE           msg   -> ErrorResponse INTERNAL_ERROR msg
    BackupE appId reason      -> ErrorResponse BACKUP_ERROR [i|Backup failed for #{appId}: #{reason}|]
    BackupPassInvalidE        -> ErrorResponse BACKUP_ERROR [i|Password provided for backups is invalid|]
    OpenSslE cert ec stdout' stderr' ->
        ErrorResponse OPENSSL_ERROR [i|OPENSSL ERROR: #{cert} - #{show ec <> "\n" <> stdout' <> "\n" <> stderr'}|]

data ErrorCode =
      PRODUCT_KEY_ERROR
    | REGISTRATION_ERROR
    | AGENT_UPDATE_ERROR
    | DATABASE_ERROR
    | WIFI_ERROR
    | APPMGR_CONFIG_ERROR
    | APPMGR_PARSE_ERROR
    | APPMGR_ERROR
    | EJECT_ERROR
    | AVAHI_ERROR
    | REGISTRY_ERROR
    | APP_NOT_INSTALLED
    | APP_NOT_CONFIGURED
    | APP_ACTION_FORBIDDEN
    | SELF_UPDATE_ERROR
    | INVALID_SSH_KEY
    | INVALID_SSID
    | INVALID_PSK
    | INVALID_REQUEST
    | INVALID_HEADER
    | MISSING_HEADER
    | METRICS_ERROR
    | RESOURCE_NOT_FOUND
    | UPDATE_IN_PROGRESS
    | INTERNAL_ERROR
    | SYNCHRONIZATION_ERROR
    | BACKUP_ERROR
    | OPENSSL_ERROR
    deriving (Eq, Show)
instance ToJSON ErrorCode where
    toJSON = String . show

data ErrorResponse = ErrorResponse
    { errorCode    :: ErrorCode
    , errorMessage :: Text
    }
    deriving (Eq, Show)
instance ToJSON ErrorResponse where
    toJSON ErrorResponse {..} = object ["code" .= errorCode, "message" .= errorMessage]
instance ToContent ErrorResponse where
    toContent = toContent . toJSON
instance ToTypedContent ErrorResponse where
    toTypedContent = toTypedContent . toJSON

instance ToTypedContent S9Error where
    toTypedContent = toTypedContent . toJSON . toError
instance ToContent S9Error where
    toContent = toContent . toJSON . toError

toStatus :: S9Error -> Status
toStatus = \case
    ProductKeyE            -> status401
    RegistrationE          -> status403
    NoCompliantAgentE _    -> status404
    PersistentE       _    -> status500
    WifiConnectionE        -> status500
    AppMgrParseE{}         -> status500
    AppMgrInvalidConfigE _ -> status400
    AppMgrE        _ _     -> status500
    EjectE  _              -> status500
    AppMgrVersionE _ _     -> status500
    AvahiE  _              -> status500
    MetricE _              -> status500
    RegistryUnreachableE   -> status500
    RegistryParseE _ _     -> status500
    AppNotInstalledE _     -> status404
    AppStateActionIncompatibleE _ status action -> case (status, action) of
        (AppStatusAppMgr Dead       , _    ) -> status500
        (AppStatusAppMgr Removing   , _    ) -> status403
        (AppStatusAppMgr Running    , Start) -> status200
        (AppStatusAppMgr Running    , Stop ) -> status200
        (AppStatusAppMgr Restarting , Start) -> status200
        (AppStatusAppMgr Restarting , Stop ) -> status200
        (AppStatusAppMgr Stopped    , Start) -> status200
        (AppStatusAppMgr Stopped    , Stop ) -> status200
        (AppStatusAppMgr Paused     , _    ) -> status403
        (AppStatusTmp    NeedsConfig, Start) -> status403
        (AppStatusTmp    NeedsConfig, Stop ) -> status200
        (AppStatusTmp    _          , _    ) -> status403
    UpdateSelfE _ _         -> status500
    InvalidSshKeyE _        -> status400
    InvalidSsidE            -> status400
    InvalidPskE             -> status400
    InvalidRequestE _ _     -> status400
    NotFoundE       _ _     -> status404
    UpdateInProgressE       -> status403
    TemporarilyForbiddenE{} -> status403
    TorServiceTimeoutE      -> status500
    NginxSslE _             -> status500
    WifiOrphaningE          -> status403
    ManifestParseE _ _      -> status500
    NoPasswordExistsE       -> status401
    MissingFileE        _   -> status500
    ClientCryptographyE _   -> status401
    TTLExpirationE      _   -> status403
    EnvironmentValE     _   -> status500
    HostsParamsE        _   -> status400
    ParamsE             _   -> status400
    BackupE _ _             -> status500
    BackupPassInvalidE      -> status403
    InternalE _             -> status500
    OpenSslE{}              -> status500

handleS9ErrC :: (MonadHandler m, MonadLogger m) => ErrorC S9Error m a -> m a
handleS9ErrC action =
    let handleIt e = do
            $logError $ show e
            toStatus >>= sendResponseStatus $ e
    in  runErrorC action handleIt pure

handleS9ErrT :: (MonadHandler m, MonadLogger m) => S9ErrT m a -> m a
handleS9ErrT action = runExceptT action >>= \case
    Left e -> do
        $logError $ show e
        toStatus >>= sendResponseStatus $ e
    Right a -> pure a

runS9ErrT :: MonadIO m => S9ErrT m a -> m (Either S9Error a)
runS9ErrT = runExceptT

logS9ErrT :: (MonadIO m, MonadLogger m) => S9ErrT m a -> m (Maybe a)
logS9ErrT x = runS9ErrT x >>= \case
    Left e -> do
        $logError $ show e
        pure Nothing
    Right a -> pure $ Just a

handleS9ErrNuclear :: MonadIO m => S9ErrT m a -> m a
handleS9ErrNuclear action = runExceptT action >>= \case
    Left  e -> throwIO e
    Right a -> pure a

orThrowM :: Has (Error e) sig m => m (Maybe a) -> e -> m a
orThrowM action e = action >>= maybe (throwError e) pure
{-# INLINE orThrowM #-}

orThrowPure :: Has (Error e) sig m => Maybe a -> e -> m a
orThrowPure thing e = maybe (throwError e) pure thing
{-# INLINE orThrowPure #-}

