{-# LANGUAGE ScopedTypeVariables #-}

-- | Operations with files mostly.

module OrgStat.IO
       ( readOrgFile
       , readConfig
       ) where

import qualified Base             as Base
import qualified Data.ByteString  as BS
import qualified Data.Text        as T
import qualified Data.Text.IO     as TIO
import           Data.Yaml        (decodeEither)
import           System.Directory (doesFileExist)
import           System.FilePath  (takeBaseName, takeExtension)
import           System.Wlog      (WithLogger, logDebug)
import           Turtle           (ExitCode (..), procStrict)
import           Universum

import           OrgStat.Ast      (Org)
import           OrgStat.Config   (ConfigException (ConfigParseException), OrgStatConfig)
import           OrgStat.Parser   (runParser)
import           OrgStat.Util     (dropEnd)

data OrgIOException
    = OrgIOException Text
      -- ^ All exceptions related to reading files
    | ExternalException Text
      -- ^ Failed to run some external app (gpg)
    deriving (Typeable)

instance Base.Show OrgIOException where
    show (OrgIOException r)    = "IOException: " <> T.unpack r
    show (ExternalException r) = "ExternalException: " <> T.unpack r

instance Exception OrgIOException

-- | Attempts to read a file. If extension is ".gpg", asks a user to
-- decrypt it first. Returns a pair @(filename, content)@. It also
-- takes a list of TODO-keywords to take header names correctly.
readOrgFile
    :: (MonadIO m, MonadCatch m, WithLogger m)
    => [Text] -> FilePath -> m (Text, Org)
readOrgFile todoKeywords fp = do
    logDebug $ "Reading org file " <> fpt
    unlessM (liftIO $ doesFileExist fp) $
        throwM $ OrgIOException $ "Org file " <> fpt <> " doesn't exist"
    (content, fname) <- case takeExtension fp of
        ".gpg" -> (,dropEnd 4 fp) <$> decryptGpg
        ".org" -> (,fp) <$> liftIO (TIO.readFile fp)
        _ -> throwM $ OrgIOException $
            "File " <> fpt <> " has unknown extension. Need to be .org or .org.gpg"
    let filename = T.pack $ takeBaseName fname
    logDebug $ "Parsing org file " <> fpt
    parsed <- runParser todoKeywords content
    pure (filename, parsed)
  where
    fpt = T.pack fp
    failExternal = throwM . ExternalException
    decryptGpg = do
        logDebug $ "Decrypting gpg file: " <> fpt
        (exCode, output) <-
            (procStrict "gpg" ["--quiet", "--decrypt", fpt] empty)
            `catch`
            (\(e :: SomeException) -> failExternal $ "gpg procStrict failed: " <> show e)
        case exCode of
            ExitSuccess   -> pass
            ExitFailure n -> failExternal $ "Gpg failed with code " <> show n
        pure output

-- | Reads yaml config
readConfig :: (MonadIO m, MonadThrow m, WithLogger m) => FilePath -> m OrgStatConfig
readConfig fp = do
    logDebug $ "Reading config file from: " <> fpt
    unlessM (liftIO $ doesFileExist fp) $
        throwM $ OrgIOException $ "Config file " <> fpt <> " doesn't exist"
    res <- liftIO $ BS.readFile fp
    either (throwM . ConfigParseException . T.pack) pure $ decodeEither res
  where
    fpt = T.pack fp