{-# OPTIONS_GHC -Wno-unused-imports #-}
{-# LANGUAGE MultiWayIf #-}

module Main
  ( main
  )
where


import           Control.Concurrent             ( threadDelay )
import qualified Control.Concurrent.Async      as A
import           Control.Concurrent.MVar
import           Control.Exception              ( AsyncException(UserInterrupt)
                                                , IOException
                                                , bracket
                                                , catch
                                                , mask
                                                , throwIO
                                                , try
                                                )
import           Control.Monad                  ( forM_
                                                , forever
                                                , replicateM
                                                , unless
                                                , when
                                                , zipWithM_
                                                )
import           Control.Monad.IO.Class         ( MonadIO
                                                , liftIO
                                                )
import           Control.Monad.Trans.State.Strict
                                                ( StateT(StateT)
                                                , evalStateT
                                                , execStateT
                                                , get
                                                , gets
                                                , modify
                                                , put
                                                )
import qualified Data.Char                     as Char
import           Data.List                      ( isInfixOf
                                                , isPrefixOf
                                                , isSuffixOf
                                                )
import           Data.Maybe                     ( listToMaybe
                                                , mapMaybe
                                                )
import qualified GHC.IO.Encoding
import           Lens.Micro                     ( (<&>) )
import           System.Clock                   ( Clock(RealtimeCoarse)
                                                , TimeSpec
                                                , diffTimeSpec
                                                , getTime
                                                , toNanoSecs
                                                )
import qualified System.Console.ANSI           as Ansi
import           System.Console.Concurrent      ( errorConcurrent
                                                , flushConcurrentOutput
                                                , outputConcurrent
                                                , withConcurrentOutput
                                                )
import           System.Console.Regions         ( ConsoleRegion
                                                , RegionLayout(Linear)
                                                , closeConsoleRegion
                                                , displayConsoleRegions
                                                , openConsoleRegion
                                                , setConsoleRegion
                                                )
import qualified System.Environment
import           System.Exit                    ( exitSuccess
                                                , exitWith
                                                )
import           System.IO                      ( Handle
                                                , IOMode(WriteMode)
                                                )
import qualified System.IO
import qualified System.Process                as P
import qualified Text.PrettyPrint.HughesPJ     as PP
import           Text.Printf                    ( printf )
import           Text.Read                      ( readMaybe )
import qualified UI.Butcher.Monadic            as B

import           Util


data StreamKind = StdOut | StdErr
  deriving (Eq, Show)

data JoinMode
  = JoinYield    -- i.e. don't join: We want to yield that exact line
  | JoinAll      -- join with any other JoinAll-tagged lines/patterns
  | JoinSpecific -- join with this pattern only

data JoinedInfo
  = JoinedNot     -- Line did not match any pattern
  | JoinedYield   -- Line matched a yield pattern, must be forwarded as-is
  | JoinedAll Int
  | Joined Int String [String] -- pattern, prefix

data KeepMode
  = Drop -- dont forward
  | Keep -- forward each line, apart from summaries
  | Conflate -- summarize non-summarized lines as "*"
  deriving (Eq, Show)

data Config = Config
  { c_label       :: String
  , c_lines       :: Int
  , c_keepStdout  :: KeepMode
  , c_keepStderr  :: KeepMode
  , c_summarize   :: [(JoinMode, String)]
  , c_outFile     :: Maybe Handle
  , c_errFile     :: Maybe Handle
  , c_sectionChar :: Maybe Char
  , c_termSize    :: Maybe (Int, Int)
  }

data State = State
  { s_config       :: Config
  , s_regions      :: [ConsoleRegion]
  , s_history      :: [(StreamKind, String)]
  , s_lines        :: [(StreamKind, String)]
  , s_countOut     :: Int
  , s_countErr     :: Int
  , s_globalStart  :: TimeSpec
  , s_lastLineTime :: TimeSpec
  , s_summary      :: Maybe ((StreamKind, String), JoinedInfo)
  }


getTimeDiff :: Bool -> StateT State IO (Float, Float)
getTimeDiff updateCur = do
  now <- liftIO $ getTime RealtimeCoarse
  when updateCur $ modify $ \s -> s { s_lastLineTime = now }
  s <- get
  let diffNanos1 = toNanoSecs $ diffTimeSpec (s_globalStart s) now
  let diffNanos2 = toNanoSecs $ diffTimeSpec (s_lastLineTime s) now
  pure
    ( fromIntegral (diffNanos1 `div` 1000000) / (1000 :: Float)
    , fromIntegral (diffNanos2 `div` 1000000) / (1000 :: Float)
    )


stateLine :: Bool -> Bool -> StateT State IO String
stateLine updateCur showCur = do
  (diffFloat1, diffFloat2) <- getTimeDiff updateCur
  s                        <- get
  let outStr = if showCur && diffFloat2 > 1.0
        then printf
          "waiting since %0.0fs … %0.1fs total, %i/%i lines stdout/stderr"
          diffFloat2
          diffFloat1
          (s_countOut s)
          (s_countErr s)
        else printf "%0.3fs total, %i lines stdout %i lines stderr"
                    diffFloat1
                    (s_countOut s)
                    (s_countErr s)
  pure $ outStr

firstJust :: (a -> Maybe b) -> [a] -> Maybe b
firstJust f = listToMaybe . mapMaybe f

matchPattern :: String -> String -> Bool
matchPattern pat s = case break (== '*') pat of
  ("", "*"       ) -> True
  ("", '*' : rest) -> case break (== '*') rest of
    (start, "*") -> start `isInfixOf` s
    (_    , "" ) -> rest `isSuffixOf` s
    _            -> error $ "too many globs in pattern " ++ pat ++ "!"
  (start, '*' : rest) -> if any (== '*') rest
    then error "only one glob supported in patterns!"
    else start `isPrefixOf` s && rest `isSuffixOf` s
  (""   , "") -> error "empty pattern"
  (exact, "") -> exact == s
  _           -> undefined

dispatchLine :: (StreamKind, String) -> StateT State IO ()
dispatchLine line@(kind, str) = do
  conf <- gets s_config
  liftIO $ case kind of
    StdOut -> when (c_keepStdout conf /= Drop)
      $ outputConcurrent (fReset ++ str ++ "\n")
    StdErr ->
      when (c_keepStderr conf /= Drop) $ errorConcurrent (fReset ++ str ++ "\n")
  modify $ \s -> s { s_history = line : s_history s }

dispatchYielded :: (StreamKind, String) -> StateT State IO ()
dispatchYielded line@(kind, str) = do
  liftIO $ case kind of
    StdOut -> outputConcurrent (fReset ++ str ++ "\n")
    StdErr -> errorConcurrent (fReset ++ str ++ "\n")
  modify $ \s -> s { s_history = line : s_history s }

showPattern :: String -> String
showPattern p = p >>= \case
  '*' -> setFGColorVivid Ansi.Yellow ++ "…" ++ fReset
  x   -> [x]

dispatchPat :: StreamKind -> Int -> String -> [String] -> StateT State IO ()
dispatchPat oldKind i pat prefix = do
  let kindStr = case oldKind of
        StdOut -> "stdout"
        StdErr -> "stderr"
  let betterName =
        let a  = unwords prefix
            la = length a
        in  if
              | i == 1 && la < 70
              -> a
              | la > length pat && la < 70
              -> a ++ setFGColorVivid Ansi.Yellow ++ " …" ++ fReset
              | otherwise
              -> showPattern pat
  let prettyPat =
        fGrey
          ++ "("
          ++ show i
          ++ " lines "
          ++ kindStr
          ++ ")"
          ++ fReset
          ++ " "
          ++ betterName
          ++ "\n"
  conf <- gets s_config
  liftIO $ case oldKind of
    StdOut -> when (c_keepStdout conf /= Drop) $ outputConcurrent prettyPat
    StdErr -> when (c_keepStderr conf /= Drop) $ errorConcurrent prettyPat

dispatchSkipped :: StreamKind -> Int -> StateT State IO ()
dispatchSkipped oldKind i = do
  let kindStr = case oldKind of
        StdOut -> "stdout"
        StdErr -> "stderr"
  let prettyPat =
        fGrey
          ++ "("
          ++ show i
          ++ " lines "
          ++ kindStr
          ++ ") …skipped…"
          ++ fReset
          ++ "\n"
  conf <- gets s_config
  liftIO $ case oldKind of
    StdOut -> when (c_keepStdout conf /= Drop) $ outputConcurrent prettyPat
    StdErr -> when (c_keepStderr conf /= Drop) $ errorConcurrent prettyPat


summarizeLines :: (StreamKind, String) -> StateT State IO ()
summarizeLines cur@(kind, line) = do
  s <- get
  let conf = s_config s
  let match = firstJust
        (\joiner@(_, pat) ->
          if matchPattern pat line then Just joiner else Nothing
        )
        (c_summarize conf ++ case kind of
          StdOut -> [ (JoinAll, "*") | c_keepStdout conf == Conflate ]
          StdErr -> [ (JoinAll, "*") | c_keepStderr conf == Conflate ]
        )
  case (s_summary s, match) of
    (Nothing, _) -> put s
      { s_summary = Just
                      ( cur
                      , case match of
                        Nothing                  -> JoinedNot
                        Just (JoinYield   , _  ) -> JoinedYield
                        Just (JoinAll     , _  ) -> JoinedAll 1
                        Just (JoinSpecific, pat) -> Joined 1 pat (words line)
                      )
      }
    (Just (oldLine, JoinedNot), _) -> do
      dispatchLine oldLine
      put s
        { s_summary = Just
                        ( cur
                        , case match of
                          Nothing                  -> JoinedNot
                          Just (JoinYield   , _  ) -> JoinedYield
                          Just (JoinAll     , _  ) -> JoinedAll 1
                          Just (JoinSpecific, pat) -> Joined 1 pat (words line)
                        )
        }
    (Just (oldLine, JoinedYield), Nothing) -> do
      dispatchYielded oldLine
      put s { s_summary = Just (cur, JoinedNot) }
    (Just (oldLine, JoinedYield), _) -> do
      dispatchYielded oldLine
      put s
        { s_summary = Just
                        ( cur
                        , case match of
                          Nothing                  -> JoinedNot
                          Just (JoinAll     , _  ) -> JoinedAll 1
                          Just (JoinSpecific, pat) -> Joined 1 pat (words line)
                          Just (JoinYield   , _  ) -> JoinedYield
                        )
        }
    (Just ((oldKind, _), JoinedAll i), Nothing) -> do
      dispatchSkipped oldKind i
      put s { s_summary = Just (cur, JoinedNot) }
    (Just ((oldKind, _), Joined i oldPat oldPrefix), Nothing) -> do
      dispatchPat oldKind i oldPat oldPrefix
      put s { s_summary = Just (cur, JoinedNot) }
    (Just ((oldKind, _), JoinedAll i), Just joiner) -> case joiner of
      (JoinYield, _pat) -> do
        dispatchSkipped oldKind i
        put s { s_summary = Just (cur, JoinedYield) }
      (JoinAll, _)
        | kind == oldKind -> do
          put s { s_summary = Just (cur, JoinedAll (i + 1)) }
        | otherwise -> do
          dispatchSkipped oldKind i
          put s { s_summary = Just (cur, JoinedAll 1) }
      (JoinSpecific, pat) -> do
        dispatchSkipped oldKind i
        put s { s_summary = Just (cur, Joined 1 pat (words line)) }
    (Just ((oldKind, _), Joined i oldPat oldPrefix), Just joiner) ->
      case joiner of
        (JoinSpecific, pat) | oldPat == pat && kind == oldKind -> do
          let newPrefix =
                let go [] = []
                    go ((a, b) : rest) | a == b    = a : go rest
                                       | otherwise = []
                in  go $ zip oldPrefix (words line)
          put s { s_summary = Just (cur, Joined (i + 1) pat newPrefix) }
        _ -> do
          dispatchPat oldKind i oldPat oldPrefix
          put s
            { s_summary = Just
                            ( cur
                            , case joiner of
                              (JoinYield   , _  ) -> JoinedYield
                              (JoinAll     , _  ) -> JoinedAll 1
                              (JoinSpecific, pat) -> Joined 1 pat (words line)
                            )
            }


processLine :: (StreamKind, String) -> State -> IO State
processLine newPair@(kind, _) = execStateT $ do
  conf <- gets s_config
  modify $ \s -> s { s_lines = newPair : s_lines s }
  do
    s0 <- get
    let (keep, over) = splitAt (c_lines conf - 1) (s_lines s0)
    put s0 { s_lines = keep }
    over `forM_` summarizeLines
  case kind of
    StdOut -> modify $ \s -> s { s_countOut = s_countOut s + 1 }
    StdErr -> modify $ \s -> s { s_countErr = s_countErr s + 1 }
  curLines               <- gets s_lines
  prettyLinesWithSummary <- do
    let ellipse =
          let go _ ""       = ""
              go 0 _        = "…"
              go n (x : xs) = x : go (n - 1) xs
          in  case c_termSize conf of
                Nothing     -> id
                Just (_, w) -> go (w - 3)
    let prettyLines = reverse $ take (c_lines conf) curLines <&> \case
          (StdOut, line) -> fWhiteDis ++ "│ " ++ fReset ++ ellipse line
          (StdErr, line) -> fRedDis ++ "│ " ++ fReset ++ ellipse line
    summary <- gets s_summary
    pure $ case summary of
      Nothing -> prettyLines
      Just ((StdOut, line), JoinedNot) ->
        (fWhiteDis ++ "│ " ++ fReset ++ ellipse line) : prettyLines
      Just ((StdOut, line), JoinedAll 1) ->
        (fWhiteDis ++ "│ " ++ fReset ++ ellipse line) : prettyLines
      Just ((StdOut, _line), JoinedAll i) ->
        (  fWhiteDis
          ++ "│ "
          ++ fGrey
          ++ "…skipped… ("
          ++ show i
          ++ " lines)"
          ++ fReset
          )
          : prettyLines
      Just ((StdOut, line), Joined 1 _ _) ->
        (fWhiteDis ++ "│ " ++ fReset ++ ellipse line) : prettyLines
      Just ((StdOut, _), Joined i pat _) ->
        (  fWhiteDis
          ++ "│ "
          ++ fReset
          ++ showPattern pat
          ++ fGrey
          ++ " ("
          ++ show i
          ++ " lines)"
          ++ fReset
          )
          : prettyLines
      Just ((StdOut, line), JoinedYield) ->
        (fWhiteDis ++ "│ " ++ fReset ++ ellipse line) : prettyLines
      Just ((StdErr, line), JoinedNot) ->
        (fRedDis ++ "│ " ++ fReset ++ ellipse line) : prettyLines
      Just ((StdErr, line), JoinedYield) ->
        (fRedDis ++ "│ " ++ fReset ++ ellipse line) : prettyLines
      Just ((StdErr, line), JoinedAll 1) ->
        (fRedDis ++ "│ " ++ fReset ++ ellipse line) : prettyLines
      Just ((StdErr, _line), JoinedAll i) ->
        (  fRedDis
          ++ "│ "
          ++ fGrey
          ++ "…skipped… ("
          ++ show i
          ++ " lines)"
          ++ fReset
          )
          : prettyLines
      Just ((StdErr, line), Joined 1 _ _) ->
        (fRedDis ++ "│ " ++ fReset ++ ellipse line) : prettyLines
      Just ((StdErr, _), Joined i pat _) ->
        (  fRedDis
          ++ "│ "
          ++ fReset
          ++ showPattern pat
          ++ fGrey
          ++ " ("
          ++ show i
          ++ " lines)"
          ++ fReset
          )
          : prettyLines
  let showCount = min (c_lines conf) (length prettyLinesWithSummary)
  do -- make sure we have enough regions allocated
    let need = showCount + 1
    got <- gets (length . s_regions)
    when (need > got) $ do
      new <- liftIO $ replicateM (need - got) (openConsoleRegion Linear)
      modify $ \s -> s { s_regions = s_regions s ++ new }
  do
    regions <- gets s_regions
    liftIO $ zipWithM_
      (\x region -> do
        setConsoleRegion region x
      )
      prettyLinesWithSummary
      (take showCount regions)
  updateStateLine True

updateStateLine :: Bool -> StateT State IO ()
updateStateLine updateCur = do
  line <- stateLine updateCur True
  s    <- get
  liftIO $ setConsoleRegion
    (last $ s_regions s)
    (  fGrey
    ++ "╰─ … "
    ++ line
    ++ ", "
    ++ setFGColorVivid Ansi.Blue
    ++ (c_label $ s_config s)
    ++ fReset
    )

quoteIfSpaces :: String -> String
quoteIfSpaces s = if any Char.isSpace s then "\"" ++ s ++ "\"" else s

main :: IO ()
main = B.mainFromCmdParser $ do
  B.reorderStart
  numLines :: Int <- B.addFlagReadParam "n" ["lines"] "LINES" (B.flagDefault 5)
  maxLines        <- B.addSimpleBoolFlag "" ["max-lines"] mempty
  keepStdout      <- B.addSimpleBoolFlag "" ["keep-out"] mempty
  keepStderr      <- B.addSimpleBoolFlag "" ["keep-err"] mempty
  keepBoth        <- B.addSimpleBoolFlag "" ["keep"] mempty
  dropStdout      <- B.addSimpleBoolFlag "" ["drop-out"] mempty
  dropStderr      <- B.addSimpleBoolFlag "" ["drop-err"] mempty
  dropBoth        <- B.addSimpleBoolFlag "" ["drop"] mempty
  conflateStdout  <- B.addSimpleBoolFlag "" ["conflate-out"] mempty
  conflateStderr  <- B.addSimpleBoolFlag "" ["conflate-err"] mempty
  conflateBoth    <- B.addSimpleBoolFlag "" ["conflate"] mempty
  summarize       <- B.addFlagStringParams "s" ["summarize"] "PATTERN" mempty
  skip            <- B.addFlagStringParams "x" ["skip"] "PATTERN" mempty
  label           <- B.addFlagStringParams "" ["label"] "STRING" mempty
  yield           <- B.addFlagStringParams "y" ["yield"] "PATTERN" mempty
  omitSummary     <- B.addSimpleBoolFlag "" ["omit-summary"] mempty
  tee             <- B.addFlagStringParams
    ""
    ["tee"]
    "BASENAMEBASEPATH"
    ( B.flagHelp
    $ PP.text "Write copy of stdout/stderr to BASEPATH.{out/err}.txt"
    )
  teeBoth <- B.addFlagStringParams
    ""
    ["tee-both"]
    "FILENAMEFILEPATH"
    (B.flagHelp $ PP.text "Write copy of stdout and stderr to FILEPATH")
  -- section         <- B.addSimpleBoolFlag "" ["section"] mempty
  B.reorderStop
  rest <- B.addParamRestOfInputRaw "COMMAND" mempty <&> \case
    B.InputString ('-' : '-' : ' ' : r) -> words r
    B.InputString ('-'       : '-' : r) -> words r
    B.InputString (r                  ) -> words r
    B.InputArgs   ("--" : r           ) -> r
    B.InputArgs   r                     -> r
  helpDesc <- B.peekCmdDesc
  B.addCmdImpl $ do
    when (null rest) $ do
      print $ B.ppHelpShallow helpDesc
      exitSuccess
    let (restPath : restArgs) = rest
    recursiveMay <- System.Environment.lookupEnv "IN_HXBRIEF"
    case recursiveMay of
      Just _ -> do
        -- TODO: Arguably, we should do _something_ here, e.g. summarizing
        --       and filtering etc.
        P.callProcess restPath restArgs
        exitSuccess
      Nothing -> pure ()
    let mainBracket = bracket
          (do
            System.IO.hSetEcho System.IO.stdin False
            System.IO.hSetBuffering System.IO.stdin System.IO.LineBuffering
            GHC.IO.Encoding.setLocaleEncoding GHC.IO.Encoding.utf8
            case (tee, teeBoth) of
              ([]       , []) -> pure (Nothing, Nothing)
              ([teeName], []) -> do
                h1 <- System.IO.openFile (teeName ++ ".out.txt") WriteMode
                h2 <- System.IO.openFile (teeName ++ ".err.txt") WriteMode
                pure (Just h1, Just h2)
              ([], [teeBothName]) -> do
                h <- System.IO.openFile teeBothName WriteMode
                pure (Just h, Just h)
              _ -> error "too many/conflicting tee arguments!"
          )
          (\teeHandles -> do
            fst teeHandles `forM_` System.IO.hClose
            snd teeHandles `forM_` System.IO.hClose
            -- ^ may be closed already, this is not an error according to docs!
            System.IO.hSetEcho System.IO.stdin True
          )
    withConcurrentOutput $ mainBracket $ \teeHandles -> mask $ \restore -> do
      -- restore $ GHC.IO.Encoding.setFileSystemEncoding GHC.IO.Encoding.utf8
      -- restore $ System.IO.hSetEncoding System.IO.stdout GHC.IO.Encoding.utf8
      -- restore $ System.IO.hSetEncoding System.IO.stderr GHC.IO.Encoding.utf8
      termSizeMay <- restore $ do
        support <- Ansi.hSupportsANSI System.IO.stdin
        if support
          then Ansi.getTerminalSize
          else do
            envLines <- System.Environment.lookupEnv "LINES"
            envCols  <- System.Environment.lookupEnv "COLUMNS"
            pure $ (,) <$> (envLines >>= readMaybe) <*> (envCols >>= readMaybe)
      let stdoutCheckCount =
            length
              $  [ () | keepStdout || keepBoth ]
              ++ [ () | conflateStdout || conflateBoth ]
              ++ [ () | dropStdout || dropBoth ]
      let stderrCheckCount =
            length
              $  [ () | keepStderr || keepBoth ]
              ++ [ () | conflateStderr || conflateBoth ]
              ++ [ () | dropStderr || dropBoth ]
      adjustedNumLines <- case termSizeMay of
        Just (termLines, _) | maxLines -> pure $ max 1 (termLines - 3)
        Just (termLines, _) | termLines < numLines + 3 -> do
          let actual = max 1 (termLines - 3)
          errorConcurrent
            $  "Warning: output is too small, only showing "
            ++ show actual
            ++ " lines!\n"
          pure actual
        _ -> pure numLines
      (lastLine, ecMay) <- displayConsoleRegions $ do
        initialState <- do
          startTime <- getTime RealtimeCoarse
          line0     <- openConsoleRegion Linear
          pure State
            { s_config       = Config
              { c_label       = case label of
                                  [] ->
                                    let full = unwords $ map quoteIfSpaces rest
                                    in  if length full < 80 then full else head rest
                                  [labelStr] -> labelStr
                                  _          -> error "too many labels!"
              , c_lines       = adjustedNumLines
              , c_keepStdout  = if
                                  | stdoutCheckCount > 1 -> error
                                    "too many keep/drop/conflate for stdout!"
                                  | keepStdout || keepBoth -> Keep
                                  | conflateStdout || conflateBoth -> Conflate
                                  | dropStdout || dropBoth -> Drop
                                  | otherwise -> if null summarize
                                    then Drop
                                    else Conflate
              , c_keepStderr  = if
                                  | stderrCheckCount > 1 -> error
                                    "too many keep/drop/conflate for stderr!"
                                  | keepStderr || keepBoth -> Keep
                                  | conflateStderr || conflateBoth -> Conflate
                                  | dropStderr || dropBoth -> Drop
                                  | otherwise -> Keep
              , c_summarize   = (yield <&> \x -> (JoinYield, x))
                                ++ (summarize <&> \x -> (JoinSpecific, x))
                                ++ (skip <&> \x -> (JoinAll, x))
              , c_outFile     = Nothing
              , c_errFile     = Nothing
              , c_sectionChar = Nothing -- if section then Just '#' else Nothing
              , c_termSize    = termSizeMay
              }
            , s_regions      = [line0]
            , s_history      = []
            , s_lines        = []
            , s_countOut     = 0
            , s_countErr     = 0
            , s_globalStart  = startTime
            , s_lastLineTime = startTime
            , s_summary      = Nothing
            }
        stateVar :: MVar State <- newMVar initialState

        let inHandler inp =
              let go = do
                    x <- try getLine
                    case x of
                      Left  (_ :: IOException) -> System.IO.hClose inp
                      Right line               -> do
                        System.IO.hPutStrLn inp line
                        System.IO.hFlush inp
                        go
              in  go
        let outHandler out = forever $ do
              x <- filter (/= '\r') <$> System.IO.hGetLine out
              fst teeHandles `forM_` \h -> System.IO.hPutStrLn h x
              modifyMVar_ stateVar (processLine (StdOut, x))
        let errHandler err = forever $ do
              x <- filter (/= '\r') <$> System.IO.hGetLine err
              snd teeHandles `forM_` \h -> System.IO.hPutStrLn h x
              modifyMVar_ stateVar (processLine (StdErr, x))
        let tickHandler = forever $ do
              threadDelay 333333
              modifyMVar_ stateVar $ execStateT $ updateStateLine False
        innerEnv <- do
          env <- System.Environment.getEnvironment
          pure (env ++ [("IN_HXBRIEF", "1")])

        let mainBlock =
              P.withCreateProcess
                  ((P.proc restPath restArgs) { P.std_in  = P.CreatePipe
                                              , P.std_out = P.CreatePipe
                                              , P.std_err = P.CreatePipe
                                              , P.env     = Just innerEnv
                                              }
                  )
                $ \(Just inp) (Just out) (Just err) hdl -> do
                    A.withAsync (inHandler inp) $ \inAsync ->
                      A.withAsync (outHandler out) $ \outAsync ->
                        A.withAsync (errHandler err) $ \errAsync ->
                          A.withAsync tickHandler $ \_tickAsync -> do
                            ec <- P.waitForProcess hdl
                            A.cancel inAsync
                            _a <- A.waitCatch outAsync
                            _b <- A.waitCatch errAsync
                            pure (Just ec)
        ecMay <- restore mainBlock `catch` (\UserInterrupt -> pure Nothing)
        modifyMVar_ stateVar $ execStateT $ do
          finalLines <- gets s_lines
          countOut   <- gets s_countOut
          countErr   <- gets s_countErr
          if countOut == 0 && countErr == 1
            then do
              modify
                $ \s -> s { s_config = (s_config s) { c_keepStderr = Keep } }
              reverse finalLines `forM_` dispatchLine
            else do
              -- we leave the lines in final state, but process them
              reverse finalLines `forM_` summarizeLines
              gets s_summary >>= \case
                Nothing                       -> pure ()
                Just (line     , JoinedNot  ) -> dispatchLine line
                Just (line     , JoinedYield) -> dispatchYielded line
                Just ((kind, _), JoinedAll i) -> dispatchSkipped kind i
                Just ((kind, _), Joined i pat prefix) ->
                  dispatchPat kind i pat prefix
        finalState <- takeMVar stateVar
        line       <- evalStateT (stateLine False False) finalState
        s_regions finalState `forM_` \r -> closeConsoleRegion r
        let prefix =
              fGrey
                ++ line
                ++ ", "
                ++ setFGColorVivid Ansi.Blue
                ++ (c_label $ s_config finalState)
                ++ fGrey
        let lastLine = case ecMay of
              Nothing -> prefix ++ ", UserInterrupt\n" ++ fReset
              Just ec -> prefix ++ ", ec=" ++ showEC ec ++ "\n"
        pure (lastLine, ecMay)

      flushConcurrentOutput
      unless omitSummary $ errorConcurrent lastLine
      case ecMay of
        Nothing -> throwIO UserInterrupt -- essentially re-throw
        Just ec -> exitWith ec