{-# LANGUAGE LambdaCase        #-}
{-# LANGUAGE OverloadedStrings #-}

module System.Handsy.Remote
  ( runRemote
  , Host

  -- * Options
  , SSHOptions (..)

  -- * Helpers
  , pushFile
  , pullFile

  -- * Re-exports
  , def
  ) where

import           Prelude                hiding (appendFile, readFile, writeFile)

import           System.Handsy
import           System.Handsy.Internal (interpret, interpretSimple)
import           System.Handsy.Util

import           Control.Applicative
import           Control.Concurrent
import           Control.Monad
import           Control.Retry
import           Data.Bool
import qualified Data.ByteString.Lazy   as B

import           Control.Monad.IO.Class
import           Data.Default.Class

type Host = String
data SSHOptions =
  SSHOptions {
    -- | Path of `ssh` command
    sshPath       :: FilePath,

    -- | Port to connect
    sshPort       :: Int,

    {-| Whether to use control master for SSH connections.
        This significantly reduces execution time.
    -}
    controlMaster :: Bool
  }

instance Default SSHOptions where
  def = SSHOptions "ssh" 22 True

acquireCM :: Host -> SSHOptions -> IO FilePath
acquireCM host opts = do
  cm <- run def $ head . strLines . fst <$> command_ "mktemp" ["-u", "--suffix=.handsy"] def

  let (ssh, params) = genSsh opts (Just cm)
  _ <- forkIO . run def . void $ command_ ssh (params ++ ["-M", "-N", host]) def
  bool (error "Error establishing ControlMaster connection") () <$> waitForCM cm

  return cm
  where
    checkCM :: FilePath -> IO Bool
    checkCM p = run def $ do
      let args = snd (genSsh opts Nothing) ++ ["-o", "ControlPath=" ++ p, "-O", "check"]
      command (sshPath opts) args def >>= return . \case
        (ExitSuccess, _, _) -> True
        _                   -> False
    waitForCM p = retrying (limitRetries 30) (\_ n -> return (not n)) (checkCM p)


releaseCM :: FilePath -> IO ()
releaseCM p = run def{debug=False} $ void $ command_ "rm" ["-f", p] def

genSsh :: SSHOptions -> Maybe FilePath -> (FilePath, [String])
genSsh opts cm = (sshPath opts, ["-p", show $ sshPort opts] ++ maybe [] (\i->["-S", i]) cm)

runSsh :: Host -> SSHOptions -> Maybe FilePath -> String -> B.ByteString
       -> IO (ExitCode, B.ByteString, B.ByteString)
runSsh host opts cm cmdline stdin' =
  let (ssh, params) = genSsh opts cm
  in  run def{debug=False} $ command ssh (params ++ [host] ++ [cmdline]) def{stdin=stdin'}

-- | Executes the actions at a remote host
runRemote :: Options -> Host -> SSHOptions -> Handsy a -> IO a
runRemote opts host sshOpts =
  case controlMaster sshOpts of
    False -> interpretSimple (runSsh host sshOpts Nothing) opts
    True  -> interpret (acquireCM host sshOpts)
                       releaseCM
                       (runSsh host sshOpts . Just)
                       opts

-- | Copies a local file to remote host
pushFile :: FilePath -- ^ Local path of source
         -> FilePath -- ^ Remote path of destination
         -> Handsy ()
pushFile local remote = liftIO (B.readFile local) >>= writeFile remote

-- | Fetches a file from remote host
pullFile :: FilePath -- ^ Remote path of source
         -> FilePath -- ^ Local path of destination
         -> Handsy ()
pullFile remote local = readFile remote >>= liftIO . B.writeFile local