{-# LANGUAGE CPP             #-}
{-# LANGUAGE RecordWildCards #-}

module Main (main) where

import Control.Concurrent.Async
import Control.Concurrent.STM
import Control.Monad
import Data.Monoid ((<>))
import Data.Version (showVersion)
import Numeric.Natural
import Options.Applicative hiding (str)
import Path
import Path.IO
import Paths_hapistrano (version)
import System.Exit
import System.Hapistrano.Types
import System.IO
import qualified Config                     as C
import qualified Data.Yaml                  as Yaml
import qualified System.Hapistrano          as Hap
import qualified System.Hapistrano.Commands as Hap
import qualified System.Hapistrano.Core     as Hap

#if !MIN_VERSION_base(4,8,0)
import Control.Applicative
#endif

----------------------------------------------------------------------------
-- Command line options

-- | Command line options.

data Opts = Opts
  { optsCommand :: Command
  , optsConfigFile :: FilePath
  }

-- | Command to execute and command-specific options.

data Command
  = Deploy ReleaseFormat Natural -- ^ Deploy a new release (with timestamp
    -- format and how many releases to keep)
  | Rollback Natural -- ^ Rollback to Nth previous release

parserInfo :: ParserInfo Opts
parserInfo =
  info
    (helper <*> versionOption <*> optionParser)
    (fullDesc <> progDesc "Deploy tool for Haskell applications" <>
     header "Hapistrano - A deployment library for Haskell applications")
  where
    versionOption :: Parser (a -> a)
    versionOption =
      infoOption
        (showVersion version)
        (long "version" <> short 'v' <> help "Show version of the program")

optionParser :: Parser Opts
optionParser = Opts
  <$> hsubparser
  ( command "deploy"
    (info deployParser (progDesc "Deploy a new release")) <>
    command "rollback"
    (info rollbackParser (progDesc "Roll back to Nth previous release")) )
  <*> strOption
  ( long "config"
  <> short 'c'
  <> value "hap.yaml"
  <> metavar "PATH"
  <> showDefault
  <> help "Configuration file to use" )

deployParser :: Parser Command
deployParser = Deploy
  <$> option pReleaseFormat
  ( long "release-format"
  <> short 'r'
  <> value ReleaseShort
  <> help "Which format release timestamp format to use: ‘long’ or ‘short’, default is ‘short’." )
  <*> option auto
  ( long "keep-releases"
  <> short 'k'
  <> value 5
  <> showDefault
  <> help "How many releases to keep" )

rollbackParser :: Parser Command
rollbackParser = Rollback
  <$> option auto
  ( long "use-nth"
  <> short 'n'
  <> value 1
  <> showDefault
  <> help "How many deployments back to go?" )

pReleaseFormat :: ReadM ReleaseFormat
pReleaseFormat = eitherReader $ \s ->
  case s of
    "long"  -> Right ReleaseLong
    "short" -> Right ReleaseShort
    _       -> Left ("Unknown format: " ++ s ++ ", try ‘long’ or ‘short’.")

----------------------------------------------------------------------------
-- Main

-- | Message that is used for communication between worker threads and the
-- printer thread.

data Message
  = PrintMsg OutputDest String -- ^ Print a message to specified 'OutputDest'
  | FinishMsg                  -- ^ The worker has finished
  deriving (Eq, Ord, Show, Read)

main :: IO ()
main = do
  Opts {..} <- execParser parserInfo
  econfig <- Yaml.decodeFileEither optsConfigFile
  case econfig of
    Left err -> do
      putStrLn (Yaml.prettyPrintParseException err)
      exitFailure
    Right C.Config {..} -> do
      chan <- newTChanIO
      let task rf = Task { taskDeployPath    = configDeployPath
                         , taskRepository    = configRepo
                         , taskRevision      = configRevision
                         , taskReleaseFormat = rf }
      let printFnc dest str = atomically $
            writeTChan chan (PrintMsg dest str)
          hap sshOpts =  do
            r <- Hap.runHapistrano sshOpts printFnc $
              case optsCommand of
                Deploy releaseFormat n -> do
                  forM_ configRunLocally Hap.playScriptLocally
                  release <- case configVcAction of
                               True -> Hap.pushRelease (task releaseFormat)
                               False -> Hap.pushReleaseWithoutVc (task releaseFormat)
                  rpath <- Hap.releasePath configDeployPath release
                  forM_ configCopyFiles $ \(C.CopyThing src dest) -> do
                    srcPath  <- resolveFile' src
                    destPath <- parseRelFile dest
                    let dpath = rpath </> destPath
                    (Hap.exec . Hap.MkDir . parent) dpath
                    Hap.scpFile srcPath dpath
                  forM_ configCopyDirs $ \(C.CopyThing src dest) -> do
                    srcPath  <- resolveDir' src
                    destPath <- parseRelDir dest
                    let dpath = rpath </> destPath
                    (Hap.exec . Hap.MkDir . parent) dpath
                    Hap.scpDir srcPath dpath
                  forM_ configBuildScript (Hap.playScript configDeployPath release)
                  Hap.registerReleaseAsComplete configDeployPath release
                  Hap.activateRelease configTargetSystem configDeployPath release
                  Hap.dropOldReleases configDeployPath n
                  forM_ configRestartCommand Hap.exec
                Rollback n -> do
                  Hap.rollback configTargetSystem configDeployPath n
                  forM_ configRestartCommand Hap.exec
            atomically (writeTChan chan FinishMsg)
            return r
          printer :: Int -> IO ()
          printer n = when (n > 0) $ do
            msg <- atomically (readTChan chan)
            case msg of
              PrintMsg StdoutDest str ->
                putStr str >> printer n
              PrintMsg StderrDest str ->
                hPutStr stderr str >> printer n
              FinishMsg ->
                printer (n - 1)
          haps :: [IO (Either Int ())]
          haps =
            case configHosts of
              [] -> [hap Nothing] -- localhost, no SSH
              xs ->
                let f (host, port) = SshOptions host port
                in hap . Just . f <$> xs
      results <- (runConcurrently . sequenceA . fmap Concurrently)
        ((Right () <$ printer (length haps)) : haps)
      case sequence_ results of
        Left n -> exitWith (ExitFailure n)
        Right () -> putStrLn "Success."