content: -- Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
  -- SPDX-License-Identifier: Apache-2.0
  
  
  module DA.Daml.Assistant.Env
      ( EnvF (..)
      , DamlPath (..)
      , PackagePath (..)
      , SdkPath (..)
      , SdkVersion (..)
      , getDamlEnv
      , testDamlEnv
      , getDamlPath
      , getCachePath
      , getPackagePath
      , getSdk
      , getDispatchEnv
      , envUseCache
      , forceEnv
      , mkUseCache
      ) where
  
  import DA.Daml.Assistant.Types
  import DA.Daml.Assistant.Util
  import DA.Daml.Assistant.Version
  import DA.Daml.Project.Consts hiding (getDamlPath, getPackagePath)
  import System.Directory
  import System.FilePath
  import System.Environment.Blank
  import System.Info.Extra
  import Control.Monad.Extra
  import Control.Exception.Safe
  import Data.Foldable (asum)
  import Data.Maybe
  
  -- | Calculate the environment variables in which to run daml commands.
  getDamlEnv :: DamlPath -> LookForPackagePath -> IO Env
  getDamlEnv envDamlPath lookForProjectPath = do
      envDamlAssistantPath <- getDamlAssistantPath envDamlPath
      envProjectPath <- getPackagePath lookForProjectPath
      envCachePath <- getCachePath
      let useCache = mkUseCache envCachePath envDamlPath
      envDamlAssistantSdkVersion <- getDamlAssistantSdkVersion useCache
      (envSdkVersion, envSdkPath) <- getSdk useCache envDamlPath envProjectPath
      envFreshStableSdkVersionForCheck <- getFreshStableSdkVersionForCheck useCache
      pure Env {..}
  
  envUseCache :: Env -> UseCache
  envUseCache Env {..} =
    mkUseCache envCachePath envDamlPath
  
  mkUseCache :: CachePath -> DamlPath -> UseCache
  mkUseCache cachePath damlPath =
    UseCache { cachePath, damlPathUnsafe = damlPath, overrideTimeout = Nothing }
  
  -- | (internal) Override function with environment variable
  -- if it is available.
  overrideWithEnvVar
      :: String                   -- ^ env var name
      -> (String -> IO String)    -- ^ normalize env var
      -> (String -> t)            -- ^ parser for env var
      -> IO t                     -- ^ calculation to override
      -> IO t
  overrideWithEnvVar envVar normalize parse calculate =
      maybeM calculate (normalize >=> pure . parse) (getEnv envVar)
  
  -- | (internal) Same as overrideWithEnvVar but accepts "" as
  -- Nothing and throws exception on parse failure.
  overrideWithEnvVarsMaybe
      :: Exception e
      => [String]                 -- ^ env var names
      -> (String -> IO String)    -- ^ normalize env var
      -> (String -> Either e t)   -- ^ parser for env var
      -> IO (Maybe t)             -- ^ calculation to override
      -> IO (Maybe t)
  overrideWithEnvVarsMaybe envVars normalize parse calculate = do
    overrideWithEnvVarsMaybeIO envVars normalize (pure . parse) calculate
  
  -- | (internal) Same as overrideWithEnvVarsMaybe but parser can run in IO
  overrideWithEnvVarsMaybeIO
      :: Exception e
      => [String]                    -- ^ env var name
      -> (String -> IO String)       -- ^ normalize env var
      -> (String -> IO (Either e t)) -- ^ parser for env var
      -> IO (Maybe t)                -- ^ calculation to override
      -> IO (Maybe t)
  overrideWithEnvVarsMaybeIO envVars normalize parse calculate = do
      valueM <- asum <$> traverse (\varName -> fmap (varName,) <$> getEnv varName) envVars
      case valueM of
          Nothing -> calculate
          Just (_, "") -> pure Nothing
          Just (envVar, value) -> do
              value <- normalize value
              parsed <- parse value
              Just <$> requiredE
                  ("Invalid value for environment variable " <> pack envVar <> ".")
                  parsed
  
  -- | Get the latest stable SDK version. Can be overriden with
  -- DAML_SDK_LATEST_VERSION environment variable.
  getFreshStableSdkVersionForCheck :: UseCache -> IO (IO (Maybe ReleaseVersion))
  getFreshStableSdkVersionForCheck useCache = do
    val <- getEnv sdkVersionLatestEnvVar
    case val of
      Nothing -> pure $
        freshMaximumOfVersions (getAvailableReleaseVersions useCache)
      Just "" -> pure (pure Nothing)
      Just value -> do
        parsed <- requiredE
          ("Invalid value for environment variable " <> pack sdkVersionLatestEnvVar <> ".")
          (parseVersion (pack value))
        pure (Just <$> resolveReleaseVersionUnsafe useCache parsed)
  
  -- | Determine the viability of running sdk commands in the environment.
  -- Returns the first failing test's error message.
  testDamlEnv :: Env -> IO (Maybe Text)
  testDamlEnv Env{..} = firstJustM (\(test, msg) -> unlessMaybeM test (pure msg))
      [ ( doesDirectoryExist (unwrapDamlPath envDamlPath)
        ,  "The Daml home directory does not exist. Please check if DAML_HOME is incorrectly set, "
        <> "or run \n\n    daml install --initial\n\nto create the daml home directory and "
        <> "install the SDK." )
      , ( pure (isJust envSdkVersion)
        ,  "Could not determine SDK version. Please check if DAML_HOME is incorrectly set, or "
        <> "check the daml package config file, or run \"daml install\" to install the latest "
        <> "version of the SDK." )
      , ( maybe (pure False) (doesDirectoryExist . unwrapSdkPath) envSdkPath
        ,  "The SDK directory does not exist. Please check if DAML_SDK or DAML_SDK_VERSION "
        <> "are incorrectly set, or run \"daml install\" to install the appropriate SDK version.")
      , ( maybe (pure True) (doesDirectoryExist . unwrapPackagePath) envProjectPath
        , "The package directory does not exist. Please check if DAML_PACKAGE (or DAML_PROJECT) is incorrectly set.")
      ]
  
  -- | Determine the absolute path to the assistant. Can be overriden with
  -- DAML_ASSISTANT env var.
  getDamlAssistantPath :: DamlPath -> IO DamlAssistantPath
  getDamlAssistantPath damlPath =
      overrideWithEnvVar damlAssistantEnvVar makeAbsolute DamlAssistantPath $
          pure (getDamlAssistantPathDefault damlPath)
  
  -- | Determine the absolute path to the assistant. Note that there is no
  -- daml-head on Windows at the moment.
  getDamlAssistantPathDefault :: DamlPath -> DamlAssistantPath
  getDamlAssistantPathDefault (DamlPath damlPath) =
    let commandNameNoExt = "daml"
        commandName
            | isWindows = commandNameNoExt <.> "cmd"
            | otherwise = commandNameNoExt
        path = damlPath </> "bin" </> commandName
    in DamlAssistantPath path
  
  -- | Determine SDK version of running daml assistant. Can be overriden
  -- with DAML_ASSISTANT_VERSION env var.
  getDamlAssistantSdkVersion :: UseCache -> IO (Maybe DamlAssistantSdkVersion)
  getDamlAssistantSdkVersion useCache =
      overrideWithEnvVarsMaybeIO [damlAssistantVersionEnvVar] pure
          (fmap (fmap DamlAssistantSdkVersion) . traverse (resolveReleaseVersionUnsafe useCache) . parseVersion . pack)
          (fmap DamlAssistantSdkVersion <$> tryAssistantM (getAssistantSdkVersion useCache))
  
  -- | Determine absolute path of daml home directory.
  --
  -- On Linux and Mac this is ~/.daml by default.
  -- On Windows this is %APPDATA%/daml by default.
  --
  -- This default can be overriden with the DAML_HOME environment variable,
  -- or by running from within an installed daml distribution, as
  -- determined by the presence of a "daml-config.yaml" in the ascendants
  -- of the executable path.
  --
  -- Raises an AssistantError if the path is missing.
  getDamlPath :: IO DamlPath
  getDamlPath = wrapErr "Determining daml home directory." $ do
      overrideWithEnvVar damlPathEnvVar makeAbsolute DamlPath $ do
          maybeM
              (DamlPath <$> getAppUserDataDirectory "daml")
              (pure . DamlPath)
              (findM hasDamlConfig . ascendants =<< getExecutablePath)
  
      where
          hasDamlConfig :: FilePath -> IO Bool
          hasDamlConfig p = doesFileExist (p </> damlConfigName)
  
  -- | Get the Daml cache folder. This defaults to $XDG_CACHE_HOME/daml.
  getCachePath :: IO CachePath
  getCachePath =
      wrapErr "Determing daml cache directory." $ do
          overrideWithEnvVar damlCacheEnvVar makeAbsolute CachePath $ do
              errOrcacheDir <- tryIO $ getXdgDirectory XdgCache "daml"
              case errOrcacheDir of
                  Left _err -> do
                      -- if getXdgDirectory fails we fall back to the daml path.
                      -- TODO (drsk) This fallback can be removed when we upgrade "directory" to 1.3.6.1.
                      damlPath <- unwrapDamlPath <$> getDamlPath
                      pure $ CachePath damlPath
                  Right cacheDir -> pure $ CachePath cacheDir
  
  
  -- | Calculate the package path. This is done by starting at the current
  -- working directory, checking if "daml.yaml" is present. If it is found,
  -- that's the package path. Otherwise, go up one level and repeat
  -- until you can't go up.
  --
  -- The package path can be overriden by passing the DAML_PROJECT
  -- environment variable.
  getPackagePath :: LookForPackagePath -> IO (Maybe PackagePath)
  getPackagePath (LookForPackagePath False) = pure Nothing
  getPackagePath (LookForPackagePath True) = wrapErr "Detecting daml package." $ do
          pathM <- overrideWithEnvVarsMaybe @SomeException [packagePathEnvVar, projectPathEnvVar] makeAbsolute Right $ do
              cwd <- getCurrentDirectory
              findM hasPackageConfig (ascendants cwd)
          pure (PackagePath <$> pathM)
  
      where
          hasPackageConfig :: FilePath -> IO Bool
          hasPackageConfig p = doesFileExist (p </> packageConfigName)
  
  -- | Calculate the current SDK version and path.
  --
  -- These can be overriden by the environment variables DAML_SDK_VERSION
  -- and DAML_SDK_PATH (and it ought to be enough to supply one of these
  -- and have the other be inferred).
  getSdk :: UseCache
         -> DamlPath
         -> Maybe PackagePath
         -> IO (Maybe UnresolvedReleaseVersion, Maybe SdkPath)
  getSdk useCache damlPath mPackagePath =
      wrapErr "Determining SDK version and path." $ do
          unresolvedVersion <-
              let parseAndResolve = pure . parseVersion . pack
              in
              overrideWithEnvVarsMaybeIO
                  [sdkVersionEnvVar]
                  pure
                  parseAndResolve $
                  firstJustM id
                      [ maybeM (pure Nothing)
                          ((fmap . fmap) (UnresolvedReleaseVersion . releaseVersionFromReleaseVersion) . tryAssistantM . getReleaseVersionFromSdkPath useCache . SdkPath)
                          (getEnv sdkPathEnvVar)
                      , mapM getUnresolvedReleaseVersionFromPackagePath mPackagePath
                      , (fmap . fmap) (UnresolvedReleaseVersion . releaseVersionFromReleaseVersion) $ tryAssistantM $ getDefaultSdkVersion damlPath
                      ]
  
          sdkPath <- overrideWithEnvVarsMaybe @SomeException [sdkPathEnvVar] makeAbsolute (Right . SdkPath) $
              useInstalledPath damlPath unresolvedVersion
  
          return (unresolvedVersion, sdkPath)
  
      where
          useInstalledPath :: DamlPath -> Maybe UnresolvedReleaseVersion -> IO (Maybe SdkPath)
          useInstalledPath _ Nothing = pure Nothing
          useInstalledPath damlPath (Just unresolvedVersion) = do
              let sdkPath = defaultSdkPathUnresolved damlPath unresolvedVersion
              test <- doesDirectoryExist (unwrapSdkPath sdkPath)
              pure (guard test >> Just sdkPath)
  
  -- | Calculate the environment for dispatched commands (i.e. the environment
  -- with updated DAML_HOME, DAML_PROJECT, DAML_SDK, etc).
  getDispatchEnv :: Env -> IO [(String, String)]
  getDispatchEnv Env{..} = do
      originalEnv <- getEnvironment
      envFreshStableSdkVersionForCheck <- envFreshStableSdkVersionForCheck
      pure $ filter ((`notElem` damlEnvVars) . fst) originalEnv
          ++ [ (damlPathEnvVar, unwrapDamlPath envDamlPath)
             , (damlCacheEnvVar, unwrapCachePath envCachePath)
             , (projectPathEnvVar, maybe "" unwrapPackagePath envProjectPath)
             , (packagePathEnvVar, maybe "" unwrapPackagePath envProjectPath)
             , (sdkPathEnvVar, maybe "" unwrapSdkPath envSdkPath)
             , (sdkVersionEnvVar, maybe "" versionToString envSdkVersion)
             , (sdkVersionLatestEnvVar, maybe "" versionToString envFreshStableSdkVersionForCheck)
             , (damlAssistantEnvVar, unwrapDamlAssistantPath envDamlAssistantPath)
             , (damlAssistantVersionEnvVar, maybe ""
                 (versionToString . unwrapDamlAssistantSdkVersion)
                 envDamlAssistantSdkVersion)
             ]
  