{-# LANGUAGE OverloadedStrings #-}
module Arion.Nix
  ( evaluateComposition
  , withEvaluatedComposition
  , buildComposition
  , withBuiltComposition
  , replForComposition
  , EvaluationArgs(..)
  , EvaluationMode(..)
  ) where

import           Prelude                        ( )
import           Protolude
import           Arion.Aeson                    ( pretty )
import           Data.Aeson
import qualified Data.String
import qualified System.Directory              as Directory
import           System.Process
import qualified Data.ByteString.Lazy          as BL
import           Paths_arion_compose

import qualified Data.Text.IO                  as T

import qualified Data.List.NonEmpty            as NE

import           Control.Arrow                  ( (>>>) )
import           System.IO.Temp                 ( withTempFile )
import           System.IO                      ( hClose )

data EvaluationMode =
  ReadWrite | ReadOnly

data EvaluationArgs = EvaluationArgs
 { evalUid :: Int
 , evalModules :: NonEmpty FilePath
 , evalPkgs :: Text
 , evalWorkDir :: Maybe FilePath
 , evalMode :: EvaluationMode
 , evalUserArgs :: [Text]
 }

evaluateComposition :: EvaluationArgs -> IO Value
evaluateComposition ea = do
  evalComposition <- getEvalCompositionFile
  let commandArgs =
        [ "--eval"
        , "--strict"
        , "--json"
        , "--attr"
        , "config.out.dockerComposeYamlAttrs"
        ]
      args =
        [ evalComposition ]
        ++ commandArgs
        ++ modeArguments (evalMode ea)
        ++ argArgs ea
        ++ map toS (evalUserArgs ea)
      procSpec = (proc "nix-instantiate" args)
        { cwd = evalWorkDir ea
        , std_out = CreatePipe
        }

  withCreateProcess procSpec $ \_in outHM _err procHandle -> do
    let outHandle = fromMaybe (panic "stdout missing") outHM

    out <- BL.hGetContents outHandle

    v <- Protolude.evaluate (eitherDecode out)

    exitCode <- waitForProcess procHandle

    case exitCode of
      ExitSuccess -> pass
      ExitFailure 1 -> exitFailure
      ExitFailure {} -> do
        throwIO $ FatalError $ "evaluation failed with " <> show exitCode

    case v of
      Right r -> pure r
      Left  e -> throwIO $ FatalError ("Couldn't parse nix-instantiate output" <> show e)

-- | Run with docker-compose.yaml tmpfile
withEvaluatedComposition :: EvaluationArgs -> (FilePath -> IO r) -> IO r
withEvaluatedComposition ea f = do
  v <- evaluateComposition ea
  withTempFile "." ".tmp-arion-docker-compose.yaml" $ \path yamlHandle -> do
    T.hPutStrLn yamlHandle (pretty v)
    hClose yamlHandle
    f path


buildComposition :: FilePath -> EvaluationArgs -> IO ()
buildComposition outLink ea = do
  evalComposition <- getEvalCompositionFile
  let commandArgs =
        [ "--attr"
        , "config.out.dockerComposeYaml"
        , "--out-link"
        , outLink
        ]
      args =
        [ evalComposition ]
        ++ commandArgs
        ++ argArgs ea
        ++ map toS (evalUserArgs ea)
      procSpec = (proc "nix-build" args) { cwd = evalWorkDir ea }

  withCreateProcess procSpec $ \_in _out _err procHandle -> do

    exitCode <- waitForProcess procHandle

    case exitCode of
      ExitSuccess -> pass
      ExitFailure 1 -> exitFailure
      ExitFailure {} -> do
        throwIO $ FatalError $ "nix-build failed with " <> show exitCode

-- | Do something with a docker-compose.yaml.
withBuiltComposition :: EvaluationArgs -> (FilePath -> IO r) -> IO r
withBuiltComposition ea f = do
  withTempFile "." ".tmp-arion-docker-compose.yaml" $ \path emptyYamlHandle -> do
    hClose emptyYamlHandle
    -- Known problem: kills atomicity of withTempFile; won't fix because we should manage gc roots,
    -- impl of which will probably avoid this "problem". It seems unlikely to cause issues.
    Directory.removeFile path
    buildComposition path ea
    f path


replForComposition :: EvaluationArgs -> IO ()
replForComposition ea = do
    evalComposition <- getEvalCompositionFile
    let args =
          [ "repl", evalComposition ]
          ++ argArgs ea
          ++ map toS (evalUserArgs ea)
        procSpec = (proc "nix" args) { cwd = evalWorkDir ea }

    withCreateProcess procSpec $ \_in _out _err procHandle -> do

      exitCode <- waitForProcess procHandle

      case exitCode of
        ExitSuccess -> pass
        ExitFailure 1 -> exitFailure
        ExitFailure {} -> do
          throwIO $ FatalError $ "nix repl failed with " <> show exitCode

argArgs :: EvaluationArgs -> [[Char]]
argArgs ea =
      [ "--argstr"
      , "uid"
      , show $ evalUid ea
      , "--arg"
      , "modules"
      , modulesNixExpr $ evalModules ea
      , "--arg"
      , "pkgs"
      , toS $ evalPkgs ea
      ]

getEvalCompositionFile :: IO FilePath
getEvalCompositionFile = getDataFileName "nix/eval-composition.nix"

modeArguments :: EvaluationMode -> [[Char]]
modeArguments ReadWrite = [ "--read-write-mode" ]
modeArguments ReadOnly = [ "--readonly-mode" ]

modulesNixExpr :: NonEmpty FilePath -> [Char]
modulesNixExpr =
  NE.toList >>> fmap pathExpr >>> Data.String.unwords >>> wrapList
 where
  pathExpr :: FilePath -> [Char]
  pathExpr path | isAbsolute path = "(/. + \"/${" <> toNixStringLiteral path <> "}\")"
                | otherwise       = "(./. + \"/${" <> toNixStringLiteral path <> "}\")"

  isAbsolute ('/' : _) = True
  isAbsolute _         = False

  wrapList s = "[ " <> s <> " ]"

toNixStringLiteral :: [Char] -> [Char]
toNixStringLiteral = show -- FIXME: custom escaping including '$'