{-# LANGUAGE CPP                 #-}
{-# LANGUAGE NamedFieldPuns      #-}
{-# LANGUAGE NoImplicitPrelude   #-}
{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE ScopedTypeVariables #-}
module System.Etc.Internal.Resolver.File (resolveFiles) where

import           RIO
import           RIO.Directory (doesFileExist)
import qualified RIO.HashMap   as HashMap
import qualified RIO.Set       as Set
import qualified RIO.Text      as Text
import qualified RIO.Vector    as Vector

#ifdef WITH_YAML
import System.Etc.Internal.Spec.YAML (decodeYaml)
#endif

import qualified Data.Aeson as JSON
-- import qualified Data.Aeson.Internal as JSON (IResult (..), iparse)
import qualified RIO.ByteString.Lazy as LB8

import System.Environment (lookupEnv)

import           System.Etc.Internal.Errors
import qualified System.Etc.Internal.Spec.Parser as Spec
import qualified System.Etc.Internal.Spec.Types  as Spec
import           System.Etc.Internal.Types       hiding (filepath)

--------------------------------------------------------------------------------

data ConfigFile
  = JsonFile Text LB8.ByteString
  | YamlFile Text LB8.ByteString
  deriving (Show, Eq)

--------------------------------------------------------------------------------

parseConfigValue
  :: (MonadThrow m)
  => [Text]
  -> Spec.ConfigValue cmd
  -> Int
  -> FileSource
  -> JSON.Value
  -> m ConfigValue
parseConfigValue keys spec fileIndex fileSource json =
  let parentKeys = reverse keys
      currentKey = Text.intercalate "." parentKeys
  in  case (spec, json) of
        (Spec.SubConfig currentSpec, JSON.Object object) -> SubConfig <$> foldM
          (\acc (key, subConfigValue) -> case HashMap.lookup key currentSpec of
            Nothing ->
              throwM $ UnknownConfigKeyFound parentKeys key (HashMap.keys currentSpec)
            Just subConfigSpec -> do
              value1 <- parseConfigValue (key : keys)
                                         subConfigSpec
                                         fileIndex
                                         fileSource
                                         subConfigValue
              return $ HashMap.insert key value1 acc
          )
          HashMap.empty
          (HashMap.toList object)

        (Spec.SubConfig{}, _) -> throwM $ SubConfigEntryExpected currentKey json

        (Spec.ConfigValue { Spec.isSensitive, Spec.configValueType }, _) -> do
          either throwM return $ Spec.assertMatchingConfigValueType json configValueType
          return $ ConfigValue
            (Set.singleton $ File fileIndex fileSource $ markAsSensitive isSensitive json)



eitherDecode :: ConfigFile -> Either String JSON.Value
#ifdef WITH_YAML
eitherDecode contents0 =
  case contents0 of
    JsonFile _ contents ->
      JSON.eitherDecode contents
    YamlFile _ contents ->
      decodeYaml (LB8.toStrict contents)
#else
eitherDecode contents0 = case contents0 of
  JsonFile _        contents -> JSON.eitherDecode contents
  YamlFile filepath _        -> Left ("Unsupported yaml file: " <> Text.unpack filepath)
#endif


parseConfig
  :: MonadThrow m => Spec.ConfigValue cmd -> Int -> FileSource -> ConfigFile -> m Config
parseConfig spec fileIndex fileSource contents = case eitherDecode contents of
  Left err -> throwM $ ConfigInvalidSyntaxFound (fileSourcePath fileSource) (Text.pack err)
  -- Right json ->
  --   case JSON.iparse (parseConfigValue [] spec fileIndex fileSource) json of
  --     JSON.IError _ err    ->
  --       case readMaybe err of
  --         Just (e :: ConfigError) ->
  --           throwM e
  --         _ ->
  --           throwM $ InvalidConfiguration Nothing (Text.pack err)
  --     JSON.ISuccess result -> return (Config result)
  Right json -> Config <$> parseConfigValue [] spec fileIndex fileSource json

readConfigFile :: MonadThrow m => Text -> IO (m ConfigFile)
readConfigFile filepath =
  let filepathStr = Text.unpack filepath
  in
    do
      fileExists <- doesFileExist filepathStr
      if fileExists
        then do
          contents <- LB8.readFile filepathStr
          if ".json" `Text.isSuffixOf` filepath
          then
            return $ return (JsonFile filepath contents)
          else
            if (".yaml" `Text.isSuffixOf` filepath) || (".yml" `Text.isSuffixOf` filepath)
              then return $ return (YamlFile filepath contents)
              else return (throwM $ UnsupportedFileExtensionGiven filepath)
        else return $ throwM $ ConfigurationFileNotFound filepath

readConfigFromFileSources
  :: Spec.ConfigSpec cmd -> [FileSource] -> IO (Config, [SomeException])
readConfigFromFileSources spec fileSources =
  fileSources
    & zip [1 ..]
    & mapM
        (\(fileIndex, fileSource) -> do
          mContents <- readConfigFile (fileSourcePath fileSource)
          return
            (   mContents
            >>= parseConfig (Spec.SubConfig $ Spec.specConfigValues spec)
                            fileIndex
                            fileSource
            )
        )
    & (foldl'
        (\(result, errs) eCurrent -> case eCurrent of
          Left  err     -> (result, err : errs)
          Right current -> (result `mappend` current, errs)
        )
        (mempty, []) <$>
      )

processFilesSpec :: Spec.ConfigSpec cmd -> IO (Config, [SomeException])
processFilesSpec spec = case Spec.specConfigFilepaths spec of
  Nothing -> readConfigFromFileSources spec []
  Just (Spec.FilePathsSpec paths) ->
    readConfigFromFileSources spec (map FilePathSource paths)
  Just (Spec.FilesSpec fileEnvVar paths0) -> do
    let getPaths = case fileEnvVar of
          Nothing       -> return $ map FilePathSource paths0
          Just filePath -> do
            envFilePath <- lookupEnv (Text.unpack filePath)
            let envPath =
                  maybeToList (EnvVarFileSource filePath . Text.pack <$> envFilePath)
            return $ map FilePathSource paths0 ++ envPath

    paths <- getPaths
    readConfigFromFileSources spec paths

{-|

Gathers configuration values from a list of files specified on the
@etc/filepaths@ entry of a Config Spec. This will return a Configuration Map
with values from all filepaths merged in, and a list of errors in case there was
an error reading one of the filepaths.

-}
resolveFiles
  :: Spec.ConfigSpec cmd -- ^ Config Spec
  -> IO (Config, Vector SomeException) -- ^ Configuration Map with all values from files filled in and a list of warnings
resolveFiles spec = do
  (config, exceptions) <- processFilesSpec spec
  return (config, Vector.fromList exceptions)


-- validateConfigFiles :: Spec.ConfigSpec cmd -> Config -> IO ()
-- validateConfigFiles Spec.ConfigSpec {Spec.specConfigFilepaths, Spec.specConfigValues} =
--   undefined