module Sos.Job
  ( Job(jobEvent, jobCommands)
  , JobResult(..)
  , ShellCommand
  , newJob
  , runJob
  , restartJob
  , unrestartJob
  , shouldRestartJob
  ) where

import Sos.FileEvent
import Sos.Utils

import Control.Applicative
import Control.Concurrent.STM
import Control.Exception
import Control.Monad
import Data.List.NonEmpty     (NonEmpty)
import Data.Monoid
import System.Exit
import System.Process
import Text.Printf

import qualified Data.List.NonEmpty as NonEmpty


type ShellCommand = String


data JobResult = JobSuccess | JobFailure


-- | A 'Job' is an interruptible list of shell commands to run.
data Job = Job
  { jobEvent    :: FileEvent
    -- ^ Event that triggered this job.
  , jobCommands :: NonEmpty ShellCommand
    -- ^ The list of shell commands to run.
  , jobRestart  :: TMVar ()
    -- ^ A TMVar that, when written to, indicates this job should be
    -- immediately canceled and restarted.
  }

newJob :: FileEvent -> NonEmpty ShellCommand -> STM Job
newJob event cmds = do
  tmvar <- newEmptyTMVar
  pure (Job event cmds tmvar)

restartJob :: Job -> STM ()
restartJob job = void (tryPutTMVar (jobRestart job) ())

-- | Clear any previous restart "ping"s that were sent to this job while it was
-- sitting in the job queue (not at the front).
unrestartJob :: Job -> STM ()
unrestartJob = void . tryTakeTMVar . jobRestart

-- | An STM action that returns when this job should be restarted, and retries
-- otherwise.
shouldRestartJob :: Job -> STM ()
shouldRestartJob = readTMVar . jobRestart

-- | Run a Job's list of shell commands sequentially. If a command returns
-- ExitFailure, or an exception is thrown, don't run the rest (but also don't
-- propagate the exception). Return whether or not all commands completed
-- successfully.
runJob :: Job -> IO JobResult
runJob (NonEmpty.toList . jobCommands -> cmds0) = go 1 cmds0
 where
  go :: Int -> [ShellCommand] -> IO JobResult
  go _ [] = pure JobSuccess
  go n (cmd:cmds) = do
    putStrLn (magenta (printf "[%d/%d] " n (length cmds0)) <> cmd)

    let acquire :: IO ProcessHandle
        acquire = do
          (_, _, _, ph) <- createProcess (shell cmd)
          pure ph

    try (bracket acquire terminateProcess waitForProcess) >>= \case
      Left (ex :: SomeException) -> do
          -- We expect to get ThreadKilled exceptions when we get canceled and
          -- restarted. Any other exception would be bizarre; just print it and
          -- move on.
          case fromException ex of
            Just ThreadKilled ->
              case length cmds0 of
                -- If this was a one-command job, just print that it's been
                -- canceled. Otherwise, print a little graphic showing how much
                -- of the job was completed before being restarted
                1 -> putStrLn (yellow ("Restarting job: " ++ cmd))
                _ -> do
                  let (xs, ys) = splitAt (n-1) cmds0
                  putStrLn (yellow "Restarting job:")
                  mapM_ (putStrLn . yellow . printf "[✓] %s") xs
                  mapM_ (putStrLn . yellow . printf "[ ] %s") ys
            _ -> putStrLn (red ("Exception: " ++ show ex))
          pure JobFailure

      Right ExitSuccess -> do
        putStrLn (green "Success ✓")
        go (n+1) cmds

      Right (ExitFailure c) -> do
        putStrLn (red (printf "Failure ✗ (%d)" c))
        pure JobFailure