{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeOperators #-}
module Imm.Boot (imm) where

-- {{{ Imports
import qualified Imm.Core as Core
import Imm.Database.FeedTable as Database
import Imm.Database as Database
import Imm.Dyre as Dyre
import Imm.Feed
import Imm.HTTP as HTTP
import Imm.Hooks
import Imm.Logger as Logger
import Imm.Options as Options hiding(logLevel)
import Imm.Prelude

import Control.Comonad.Cofree
import Control.Monad.Trans.Free

import System.IO (hFlush)
-- }}}

-- | Main function, meant to be used in your personal configuration file,
-- by default located at @$XDG_CONFIG_HOME\/imm\/imm.hs@.
--
-- For more information about the dynamic reconfiguration system, please consult "Config.Dyre".
--
-- Here is an example:
--
-- > import           Imm.Boot
-- > import           Imm.Database.JsonFile
-- > import           Imm.Feed
-- > import           Imm.Hooks.SendMail
-- > import           Imm.HTTP.Simple
-- > import           Imm.Logger.Simple
-- >
-- > main :: IO ()
-- > main = do
-- >   logger <- defaultLogger
-- >   manager <- defaultManager
-- >   database <- defaultDatabase
-- >
-- >   imm (mkCoHttpClient, manager) (mkCoDatabase, database) (mkCoLogger, logger) (mkCoHooks, sendmail)
-- >
-- > sendmail :: SendMailSettings
-- > sendmail = SendMailSettings smtpServer formatMail
-- >
-- > formatMail :: FormatMail
-- > formatMail = FormatMail
-- >   (\a b -> (defaultFormatFrom a b) { addressEmail = "user@host" } )
-- >   defaultFormatSubject
-- >   defaultFormatBody
-- >   (\_ _ -> [Address Nothing "user@host"])
-- >
-- > smtpServer :: Feed -> FeedElement -> SMTPServer
-- > smtpServer _ _ = SMTPServer
-- >   (Just $ Authentication PLAIN "user" "password")
-- >   (StartTls "smtp.host" defaultSettingsSMTPSTARTTLS)
imm :: (a -> CoHttpClientF IO a, a)  -- ^ HTTP client interpreter (cf "Imm.HTTP")
    -> (b -> CoDatabaseF' IO b, b)  -- ^ Database interpreter (cf "Imm.Database")
    -> (c -> CoLoggerF IO c, c)     -- ^ Logger interpreter (cf "Imm.Logger")
    -> (d -> CoHooksF IO d, d)      -- ^ Hooks interpreter (cf "Imm.Hooks")
    -> IO ()
imm coHttpClient coDatabase coLogger coHooks = void $ do
  options <- parseOptions
  Dyre.wrap (optionDyreMode options) realMain (optionCommand options, optionLogLevel options, coiter next start)
  where (next, start) = mkCoImm coHttpClient coDatabase coLogger coHooks

realMain :: (MonadIO m, PairingM (CoImmF m) ImmF m, MonadCatch m)
         => (Command, LogLevel, Cofree (CoImmF m) a) -> m ()
realMain (command, logLevel, interpreter) = void $ interpret (\_ b -> return b) interpreter $ do
  setLogLevel logLevel
  logDebug $ "Executing: " <> show (pretty command)

  handleAll (logError . fromString . displayException) $ case command of
    Check t        -> Core.check            =<< resolveTarget ByPassConfirmation t
    Import         -> Core.importOPML
    Read t         -> mapM_ Database.markAsRead   =<< resolveTarget AskConfirmation t
    Run t          -> Core.run              =<< resolveTarget ByPassConfirmation t
    Show t         -> Core.showFeed         =<< resolveTarget ByPassConfirmation t
    ShowVersion    -> Core.printVersions
    Subscribe u c  -> Core.subscribe u c
    Unread t       -> mapM_ Database.markAsUnread =<< resolveTarget AskConfirmation t
    Unsubscribe t  -> Database.deleteList FeedTable =<< resolveTarget AskConfirmation t
    _              -> return ()

  Database.commit FeedTable

  return ()

-- * DSL/interpreter model

type CoImmF m = CoHttpClientF m :*: CoDatabaseF' m :*: CoLoggerF m :*: CoHooksF m
type ImmF = HttpClientF :+: DatabaseF' :+: LoggerF :+: HooksF

mkCoImm :: (Functor m)
        => (a -> CoHttpClientF m a, a) -> (b -> CoDatabaseF' m b, b) -> (c -> CoLoggerF m c, c) -> (d -> CoHooksF m d, d)
        -> ((a ::: b ::: c ::: d) -> CoImmF m (a ::: b ::: c ::: d), a ::: b ::: c ::: d)
mkCoImm (coHttpClient, a) (coDatabase, b) (coLogger, c) (coHooks, d) =
  (coHttpClient *:* coDatabase *:* coLogger *:* coHooks, a >: b >: c >: d)


-- * Util

data SafeGuard = AskConfirmation | ByPassConfirmation
  deriving(Eq, Show)

data InterruptedException = InterruptedException deriving(Eq, Show)
instance Exception InterruptedException where
  displayException _ = "Process interrupted"

promptConfirm :: (MonadIO m, MonadThrow m) => Text -> m ()
promptConfirm s = do
  hPut stdout $ s <> " Confirm [Y/n] "
  io $ hFlush stdout
  x <- getLine
  when (x /= ("" :: Text) && x /= ("Y" :: Text)) $ throwM InterruptedException


resolveTarget :: (MonadIO m, MonadThrow m, Functor f, MonadFree f m, DatabaseF' :<: f, LoggerF :<: f)
              => SafeGuard -> Maybe Core.FeedRef -> m [FeedID]
resolveTarget s Nothing = do
  result <- keys <$> Database.fetchAll FeedTable
  when (s == AskConfirmation) . promptConfirm $ "This will affect " <> show (length result) <> " feeds."
  return result
resolveTarget _ (Just (FeedRef (Left i))) = do
  result <- fst . (!! i) . mapToList <$> Database.fetchAll FeedTable
  -- logInfo $ "Target(s): " <> show (pretty result)
  return $ singleton result
resolveTarget _ (Just (FeedRef (Right uri))) = return [FeedID uri]