{-# LANGUAGE ScopedTypeVariables #-}

-- | Operations with files mostly.

module OrgStat.IO
       ( readOrgFile
       , readConfig
       ) where

import qualified Prelude
import Universum

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 Turtle (ExitCode (..), procStrict)

import OrgStat.Ast (Org)
import OrgStat.Config (ConfigException (ConfigParseException), OrgStatConfig)
import OrgStat.Logging
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 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)
    => [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) => FilePath -> m OrgStatConfig
readConfig fp = do
    unlessM (liftIO $ doesFileExist fp) $
        throwM $ OrgIOException $ "Config file " <> fpt <> " doesn't exist"
    res <- liftIO $ BS.readFile fp
    either (throwM . ConfigParseException . show) pure $ decodeEither' res
  where
    fpt = T.pack fp