{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StrictData #-}
{-# OPTIONS_HADDOCK hide #-}

module Moto.Internal.Cli
 ( RegistryConf(..)
 , Opts
 , getOpts
 , run
 ) where

import Control.Applicative ((<|>))
import qualified Control.Exception.Safe as Ex
import qualified Data.ByteString.Builder as BB
import Data.Foldable (for_, toList)
import qualified Data.Map.Strict as Map
import qualified Data.Set as Set
import Data.String (fromString)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Di.Df1 as Di
import qualified Options.Applicative as OA
import qualified System.Exit as IO
import qualified System.IO as IO

import qualified Moto.Internal as I

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

-- | Configuration for the 'I.Registry' that we'll use to keep track of the
-- migrations we've run so far.
data RegistryConf = forall r. RegistryConf
  { registryConf_help :: String
    -- ^ Help message for the @--registry@ command line option.
  , registryConf_parse :: String -> Either String r
    -- ^ Parse the string obtained from the @--registry@ command line option,
    -- refining it into some @r@ of our choosing acceptable as an input to
    -- 'registryConf_with'.
    --
    -- This could be something like @\/var\/db\/migrations@
    -- or @postgres:\/\/user:password\@host:port\/database@.
  , registryConf_with :: forall a. Di.Df1 -> r -> (I.Registry -> IO a) -> IO a
    -- ^ Given the @r@ obtained from 'registryConf_parse', get a 'I.Registry'
    -- that can be used within the given scope.
  }

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

-- | Run the command-line arguments parser, obtaining the 'Opts' necessary for
-- calling 'run' afterwards.
--
-- Notice that we can run the executable that calls 'getOpts' with a @--help@
-- command line switch for extensive documentation on how to interact with
-- @moto@.
getOpts
  :: RegistryConf
  -- ^ Configuration for the 'I.Registry' to use.
  --
  -- Among other things, this will dictate how we interpret the @--registry@
  -- command-line option.
  --
  -- Examples: @Moto.PostgreSQL.registryConf@ from
  -- the [moto-postgresql](https://hackage.haskell.org/package/moto-postgresql)
  -- library or @Moto.File.@'Moto.File.registryConf' from this library.
  -> OA.Parser a
  -- ^ This extra parser can be used to read some extra configuration
  -- values from the command-line arguments, besides @moto@'s own.
  --
  -- For example, we can obtain things such as the name of a configuration
  -- file or a database connection string we might want to use in our
  -- migrations.
  --
  -- If no such extra data is required, then @'pure' ()@ can be used.
  --
  -- Notice that @moto@'s own command-line argument's parser has precedence
  -- over this parser. Yet, in the command-line, the argument's for the parser
  -- for @a@ should come before @moto@'s own subcommand arguments, otherwise the
  -- command line program will complain about a malformed command-line.
  -> IO (Opts, a)
getOpts rc p_a = OA.customExecParser
   (OA.prefs (OA.showHelpOnEmpty <> OA.noBacktrack))
   (let pi0 = oa_pi_Opts rc
    in pi0 { OA.infoParser = (,) <$> OA.infoParser pi0 <*> p_a })

-- | Run @moto@ on the given migrations graph 'I.Migs', according to the
-- instructions in 'Opts'.
run
  :: Di.Df1
  -- ^ Root logger. If you don't have a 'Di.Df1' for your program yet, you can
  -- obtain one using @Di.new@ from the
  -- [di](https://hackage.haskell.org/package/di) library.
  -> I.Migs graph
  -- ^ Avaliable migrations graph.
  -> Opts
  -- ^ Instructions on how to interact with our migrations.
  -- Obtain with 'getOpts'.
  -> IO ()
run di0 migs opts = do
  case opts_sub opts of
     Sub_Run x -> run_Run di0 migs x
     Sub_ShowMigrations x -> run_ShowMigrations migs x
     Sub_CheckMigrations x -> run_CheckMigrations di0 migs x
     Sub_ShowRegistry x -> run_ShowRegistry di0 x
     Sub_CleanRegistry x -> run_CleanRegistry di0 migs x
     Sub_DeleteRecoveryData x -> run_DeleteRecoveryData di0 migs x

run_Run :: Di.Df1 -> I.Migs graph -> Opts_Run -> IO ()
run_Run di0 migs x = do
  runWithRegistry (opts_run_withRegistry x) di0 $ \reg -> do
     I.getPlan di0 migs reg (opts_run_target x) >>= \case
        Left e -> Ex.throwM e
        Right p -> case opts_run_dryRun x of
           False -> I.run di0 reg p
           True -> BB.hPutBuilder IO.stdout (renderPlan p)

run_ShowMigrations :: I.Migs graph -> Opts_ShowMigrations -> IO ()
run_ShowMigrations migs x = do
  let gf = opts_showMigrations_graphFormat x
  BB.hPutBuilder IO.stdout (renderMigs gf migs)

run_CheckMigrations :: Di.Df1 -> I.Migs graph -> Opts_CheckMigrations -> IO ()
run_CheckMigrations di0 migs x = do
  runWithRegistry (opts_checkMigrations_withRegistry x) di0 $ \reg -> do
    -- The 'I.Target' here is an unused dummy value.
    I.getPlan di0 migs reg (I.Target I.Forwards Set.empty) >>= \case
      Left _ -> IO.exitFailure
      Right _ -> IO.exitSuccess

run_ShowRegistry :: Di.Df1 -> Opts_ShowRegistry -> IO ()
run_ShowRegistry di0 x = do
  runWithRegistry (opts_showRegistry_withRegistry x) di0 $ \reg -> do
    state <- I.registry_state reg di0
    BB.hPutBuilder IO.stdout (renderState state)

run_CleanRegistry :: Di.Df1 -> I.Migs graph -> Opts_CleanRegistry -> IO ()
run_CleanRegistry di0 migs x = do
  runWithRegistry (opts_cleanRegistry_withRegistry x) di0 $ \reg -> do
    case opts_cleanRegistry_dryRun x of
      True -> fmap I.state_status (I.registry_state reg di0) >>= \case
        I.Dirty _ _ -> IO.exitFailure
        I.Clean -> pure ()
      False -> case opts_cleanRegistry_unsafe x of
        Just od -> I.unsafeCleanRegistry di0 reg od
        Nothing -> I.cleanRegistry di0 migs reg

run_DeleteRecoveryData
  :: Di.Df1 -> I.Migs graph -> Opts_DeleteRecoveryData -> IO ()
run_DeleteRecoveryData di0 migs x = do
  for_ (Set.toList (opts_store_migIds x)) $ \mId -> do
    let di1 = Di.attr "mig" mId di0
    case I.lookupMigs mId migs of
      Just (_, I.UMig store _ _) -> I.store_delete store di1 mId
      Just (_, I.UGone) -> do
        Di.error_ di1 "Migration code is gone."
        IO.exitFailure
      Nothing -> do
        Di.error_ di1 "Migration not unknown."
        IO.exitFailure

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

oa_pi_Opts :: RegistryConf -> OA.ParserInfo Opts
oa_pi_Opts rc = OA.info
  (oa_p_Opts rc OA.<**> OA.helper)
  (OA.fullDesc <> OA.progDesc "Command line interface to migrations.")

oa_p_Opts :: RegistryConf -> OA.Parser Opts
oa_p_Opts rc = Opts <$> oa_p_Sub rc

-- | This is the input required by 'run', obtained from the command line
-- arguments by using 'getOpts'.
data Opts = Opts
  { opts_sub :: Sub
    -- ^ Subcommand to run.
  }

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

oa_p_Sub :: RegistryConf -> OA.Parser Sub
oa_p_Sub rc = OA.hsubparser $ mconcat
  [ OA.command "run"
      (fmap Sub_Run (oa_pi_Run rc))
  , OA.command "show-migrations"
      (fmap Sub_ShowMigrations oa_pi_ShowMigrations)
  , OA.command "check-migrations"
      (fmap Sub_CheckMigrations (oa_pi_CheckMigrations rc))
  , OA.command "show-registry"
      (fmap Sub_ShowRegistry (oa_pi_ShowRegistry rc))
  , OA.command "clean-registry"
      (fmap Sub_CleanRegistry (oa_pi_CleanRegistry rc))
  , OA.command "delete-recovery-data"
      (fmap Sub_DeleteRecoveryData oa_pi_DeleteRecoveryData)
  ]

data Sub
  = Sub_Run Opts_Run
  -- ^ Run migrations.
  | Sub_ShowMigrations Opts_ShowMigrations
  -- ^ Show available migrations.
  | Sub_CheckMigrations Opts_CheckMigrations
  -- ^ Check that the available migrations are compatible with the registry.
  | Sub_ShowRegistry Opts_ShowRegistry
  -- ^ Show migrations registry.
  | Sub_CleanRegistry Opts_CleanRegistry
  -- ^ I.Clean the registry if dirty.
  | Sub_DeleteRecoveryData Opts_DeleteRecoveryData
  -- ^ Delete content from the store.

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

oa_pi_Run :: RegistryConf -> OA.ParserInfo Opts_Run
oa_pi_Run rc = OA.info (oa_p_Run rc) (OA.progDesc "Run migrations.")

oa_p_Run :: RegistryConf -> OA.Parser Opts_Run
oa_p_Run rc = Opts_Run
  <$> oa_p_WithRegistry rc
  <*> oa_p_Target
  <*> OA.flag True False
        (OA.long "no-dry-run" <>
         OA.help "Don't just show the execution plan, run it!")

data Opts_Run = Opts_Run
  { opts_run_withRegistry :: WithRegistry
  -- ^ Acquire a 'I.Registry' to use within a limited scope..
  , opts_run_target :: I.Target
  -- ^ Migration target.
  , opts_run_dryRun :: Bool
  -- ^ Don't run migrations, just show the execution plan.
  }

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

oa_pi_ShowMigrations :: OA.ParserInfo Opts_ShowMigrations
oa_pi_ShowMigrations = OA.info oa_p_ShowMigrations
  (OA.progDesc "Show available migrations.")

oa_p_ShowMigrations :: OA.Parser Opts_ShowMigrations
oa_p_ShowMigrations = Opts_ShowMigrations <$> oa_p_GraphFormat

data Opts_ShowMigrations = Opts_ShowMigrations
  { opts_showMigrations_graphFormat :: GraphFormat
  -- ^ Format in which to render the migrations graph.
  }

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

oa_pi_CheckMigrations :: RegistryConf -> OA.ParserInfo Opts_CheckMigrations
oa_pi_CheckMigrations rc = OA.info (oa_p_CheckMigrations rc)
  (OA.progDesc "Exit immediately with status 0 if the available \
               \migrations are compatible with the registry. \
               \Otherwise, exit with status 1.")

oa_p_CheckMigrations :: RegistryConf -> OA.Parser Opts_CheckMigrations
oa_p_CheckMigrations rc = Opts_CheckMigrations <$> oa_p_WithRegistry rc

data Opts_CheckMigrations = Opts_CheckMigrations
  { opts_checkMigrations_withRegistry :: WithRegistry
  -- ^ Acquire a 'I.Registry' to use within a limited scope..
  }

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

oa_pi_ShowRegistry :: RegistryConf -> OA.ParserInfo Opts_ShowRegistry
oa_pi_ShowRegistry rc = OA.info
  (oa_p_ShowRegistry rc)
  (OA.progDesc "Show migrations registry.")

oa_p_ShowRegistry :: RegistryConf -> OA.Parser Opts_ShowRegistry
oa_p_ShowRegistry rc = Opts_ShowRegistry <$> oa_p_WithRegistry rc

data Opts_ShowRegistry = Opts_ShowRegistry
  { opts_showRegistry_withRegistry :: WithRegistry
  -- ^ Acquire a 'I.Registry' to use within a limited scope..
  }

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

oa_pi_CleanRegistry :: RegistryConf -> OA.ParserInfo Opts_CleanRegistry
oa_pi_CleanRegistry rc = OA.info
  (oa_p_CleanRegistry rc)
  (OA.progDesc "Clean a dirty migrations registry.")

oa_p_CleanRegistry :: RegistryConf -> OA.Parser Opts_CleanRegistry
oa_p_CleanRegistry rc = Opts_CleanRegistry
  <$> oa_p_WithRegistry rc
  <*> OA.switch (OA.long "dry-run" <>
                 OA.help "Don't clean registry, just show whether it is \
                         \clean and exit immediately with status 0 if so, \
                         \otherwise exit with status 1.")
  <*> (OA.flag' (Just I.OnDirty_Abort)
         (OA.long "unsafe-abort" <>
          OA.help "If the registry is dirty, unsafely abort the pending \
                  \migration without performing any actual clean-up.")
       <|>
       OA.flag' (Just I.OnDirty_Commit)
         (OA.long "unsafe-commit" <>
          OA.help "If the registry is dirty, unsafely commit the pending \
                  \migration without performing any actual clean-up.")
       <|>
       pure Nothing)

data Opts_CleanRegistry = Opts_CleanRegistry
  { opts_cleanRegistry_withRegistry :: WithRegistry
  -- ^ Acquire a 'I.Registry' to use within a limited scope.
  , opts_cleanRegistry_dryRun :: Bool
  -- ^ Whether to just show whether the registry is clean
  -- and exit immediately with status 0 if the so,
  -- otherwise exit with status 1.
  , opts_cleanRegistry_unsafe :: Maybe I.OnDirty
  -- ^ Whether to unsafely mark the registy as clean, without
  -- actualy performing any clean-up.
  }

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

oa_pi_DeleteRecoveryData :: OA.ParserInfo Opts_DeleteRecoveryData
oa_pi_DeleteRecoveryData = OA.info oa_p_DeleteRecoveryData
  (OA.progDesc "Delete contents from the migrations data store.")

oa_p_DeleteRecoveryData :: OA.Parser Opts_DeleteRecoveryData
oa_p_DeleteRecoveryData = Opts_DeleteRecoveryData
  <$> fmap Set.fromList (OA.some (OA.option OA.str (OA.long "mig")))

data Opts_DeleteRecoveryData = Opts_DeleteRecoveryData
  { opts_store_migIds :: Set.Set I.MigId
  -- ^ 'I.MigId's for which to remove contents from the data store.
  }

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

data WithRegistry = WithRegistry
  (forall a. Di.Df1 -> (I.Registry -> IO a) -> IO a)

runWithRegistry :: WithRegistry -> Di.Df1 -> (I.Registry -> IO a) -> IO a
runWithRegistry (WithRegistry f) di0 k = f (Di.push "registry" di0) k

oa_p_WithRegistry :: RegistryConf -> OA.Parser WithRegistry
oa_p_WithRegistry (RegistryConf rh rp rw) = OA.option
  (OA.eitherReader $ \s -> case rp s of
     Left e -> Left e
     Right r -> Right (WithRegistry (flip rw r)))
  (OA.long "registry" <> OA.metavar "SETTINGS" <> OA.help rh)

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

oa_p_Target :: OA.Parser I.Target
oa_p_Target = I.Target
  <$> OA.flag I.Forwards I.Backwards (OA.long "backwards")
  <*> fmap Set.fromList (OA.many (OA.option OA.str
        (OA.long "mig" <> OA.metavar "ID" <>
         OA.help "If specified, only consider running the migration identified \
                 \by this ID. Use multiple times for multiple migrations.")))

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

oa_p_GraphFormat :: OA.Parser GraphFormat
oa_p_GraphFormat =
  OA.flag GraphFormatText GraphFormatDot
    (OA.long "dot" <> OA.help "Render graph in DOT (Graphviz) format.")

data GraphFormat
  = GraphFormatText -- ^ Render as plain text.
  | GraphFormatDot -- ^ Render as DOT (Graphviz).

renderMigs :: GraphFormat -> I.Migs graph -> BB.Builder
renderMigs = \case
  GraphFormatText -> renderMigs_Text
  GraphFormatDot -> renderMigs_Dot

renderMigs_Text :: I.Migs graph -> BB.Builder
renderMigs_Text (I.Migs m0) = mconcat $ do
   (here, deps) <- Map.toList (fmap (toList . fst) m0)
   case deps of
     [] -> [ f here <> " has no dependencies.\n" ]
     _  -> [ f here <> " depends on:\n" <>
             mconcat (map (\mId -> "  * " <> f mId <> "\n") deps) ]
 where
   f :: I.MigId -> BB.Builder
   f (I.MigId x) = T.encodeUtf8Builder (T.pack (show x))

renderMigs_Dot :: I.Migs graph -> BB.Builder
renderMigs_Dot (I.Migs m0) = mconcat
   [ "digraph G {\n"
   , mconcat $ do
       (here, deps) <- Map.toList (fmap (toList . fst) m0)
       dep <- deps
       [ "  " <> f dep <> " -> " <> f here <> ";\n" ]
   , "}\n"
   ]
 where
   f :: I.MigId -> BB.Builder
   f (I.MigId x) = T.encodeUtf8Builder (T.pack (show x))

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

renderState :: I.State -> BB.Builder
renderState s =
  "Status: " <> fromString (show (I.state_status s)) <>
  "\nCommitted migrations: " <>
  fromString (show (length (I.state_committed s))) <> "\n" <>
  mconcat (map (\x -> "  " <> fromString (show x) <> "\n")
               (I.state_committed s))

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

-- | Renders each 'MigId' in the 'Plan' preceded by its direction, and followed
-- by a trailing newline.
renderPlan :: I.Plan -> BB.Builder
renderPlan (I.Plan _ []) = "Execution plan is empty. Nothing to do.\n"
renderPlan (I.Plan d s) = mconcat
    [ "Execution plan:\n"
    , mconcat (map (\(mId,_) -> "  Run " <> d' <> f mId <> "\n") (toList s))
    , "\nTo actually run the migrations, add --no-dry-run to "
    , "the command-line arguments.\n" ]
  where
    d' :: BB.Builder
    d' = I.direction "backwards " "forwards " d
    f :: I.MigId -> BB.Builder
    f (I.MigId x) = T.encodeUtf8Builder (T.pack (show x))