runProjectEnvironment :: FilePath -> Maybe EnvironmentName -> Project -> IO ()
runProjectEnvironment yxExe possiblyEnvName project@Project{..} = do
    projectCfg <- initializeProject yxExe root
    (projEnvName, _projEnv) <- getProjEnv projectCfg
    setCurrentDirectory root
    env <- modifyEnvVariables project projEnvName <*> getEnvironment
    shell <- findShell
        (lookup "PATH" env)
        (lookup "SHELL" env)
        defaultShells
    executeFile shell True ["--rcfile", bashrc projEnvName] (Just env)
  where
    -- We currently support only bash, so it makes sense to have it as default.
    defaultShells = shellExePaths Bash
    bashrc projEnvName = yxShellStuff root projEnvName Bash </> "bashrc"

    root = Text.unpack _path

    getProjEnv cfg = case possiblyEnvName of
        Just envName -> case lookupWhenProvided envName of
            Just env -> pure env
            Nothing -> error $ envName <> ": No such environment found."
        Nothing -> case ProjectConfig.defaultEnvironment cfg of
            Just (n, e) -> pure (Text.unpack n, e)
            Nothing -> error "Unable to find default environment. Exaclty one\
                \ environment has to have 'is-default: true', but no such\
                \ environment was found."
      where
        lookupWhenProvided n =
            (n, ) <$> ProjectConfig.getEnvironment cfg (fromString n)

newProject :: DbConnection -> FilePath -> IO Project
newProject conn relativeDir = do
    -- Function canonicalizePath preserves trailing path separator.
    dir <- dropTrailingPathSeparator <$> canonicalizePath relativeDir
    let project = Project
            { _id = 0 -- Ignored during INSERT.
            , _name = fromString $ takeFileName dir
            , _path = fromString dir
            }
    addProject conn project
    pure project

modifyEnvVariables
    :: Project
    -> EnvironmentName
    -> IO ([(String, String)] -> [(String, String)])
modifyEnvVariables project projEnvName = (. go) <$> addYxEnvVariables project
  where
    go = (getRidOfStackEnvVariables .) . map $ \case
        (n@"PATH", path) -> (n, updatePathEnvVariable project projEnvName path)
            -- TODO: Correct implementation witouth hardcoded environment.
        ev -> ev

updatePathEnvVariable :: Project -> EnvironmentName -> String -> String
updatePathEnvVariable project environment path = yxBinDir <> ":" <> path
  where
    yxBinDir = yxExeStuff root environment
    root = Text.unpack $ _path project

getRidOfStackEnvVariables :: [(String, String)] -> [(String, String)]
getRidOfStackEnvVariables = (catMaybes .) . map $ \case
    -- These variables are exported by stack and occur when we are already in a
    -- "stack exec" environment. For the purposes of isolation we need to get
    -- rid of them. Keeping them means that e.g. changing GHC version would
    -- fail due to trying to open incorrect package DB.
    ("STACK_EXE", _) -> Nothing
    ("GHC_VERSION", _) -> Nothing
    ("GHC_PACKAGE_PATH", _) -> Nothing
    ("CABAL_INSTALL_VERSION", _) -> Nothing
    ("HASKELL_PACKAGE_SANDBOX", _) -> Nothing
    ("HASKELL_PACKAGE_SANDBOXES", _) -> Nothing
    ("HASKELL_DIST_DIR", _) -> Nothing
    e -> Just e

addYxEnvVariables
    :: Project
    -> IO ([(String, String)] -> [(String, String)])
addYxEnvVariables Project{..} = do
    yxExe <- getExecutablePath
    yxInvokedName <- getProgName
    pure . foldr (.) id $ catMaybes
        [ add "YX_ENVIRONMENT_DIR"  $ Just projectEnvironmentDir
        , add "YX_ENVIRONMENT"      $ Just projectEnvironment
        , add "YX_EXE"              $ Just yxExe
        , add "YX_INVOKED_AS"       $ Just yxInvokedName
        , add "YX_PROJECT"          . Just $ Text.unpack _name
        , add "YX_PROJECT_ROOT"     $ Just projectRoot
        , add "YX_STUFF"            $ Just yxStuffDir
        , add "YX_VERSION"          . Just $ showVersion version
        ]
  where
    projectRoot = Text.unpack _path
    yxStuffDir = yxStuffPath (Just projectRoot) YxStuff
    projectEnvironment = "default"
    projectEnvironmentDir = yxStuffPath (Just projectRoot) . EnvironmentStuff
        $ Just projectEnvironment

    add :: String
        -> Maybe String
        -> Maybe ([(String, String)] -> [(String, String)])
    add n = fmap $ (:) . (n,)

getYxConfigDir :: IO FilePath
getYxConfigDir = do
    dir <- getXdgDirectory XdgConfig "yx"
    createDirectoryIfMissing True dir
    pure dir

getYxDatabaseFile :: IO FilePath
getYxDatabaseFile = do
    dbFile <- (</> "data.db") <$> getYxConfigDir
    haveDbFile <- doesFileExist dbFile
    unless haveDbFile $ SQLite.withConnection dbFile $ \conn ->
        SQLite.execute_ conn
            "CREATE TABLE Project\
                \(id INTEGER PRIMARY KEY AUTOINCREMENT,\
                \ name TEXT UNIQUE NOT NULL,\
                \ path TEXT UNIQUE NOT NULL\
                \);"
    pure dbFile
runProjectEnvironment :: FilePath -> Maybe EnvironmentName -> Project -> IO ()
runProjectEnvironment yxExe possiblyEnvName project@Project{..} = do
    projectCfg <- initializeProject yxExe root
    (projEnvName, _projEnv) <- getProjEnv projectCfg
    setCurrentDirectory root
    env <- modifyEnvVariables project projEnvName <*> getEnvironment
    shell <- findShell
        (lookup "PATH" env)
        (lookup "SHELL" env)
        defaultShells
    executeFile shell True ["--rcfile", bashrc projEnvName] (Just env)
  where
    -- We currently support only bash, so it makes sense to have it as default.
    defaultShells = shellExePaths Bash
    bashrc projEnvName = yxShellStuff root projEnvName Bash </> "bashrc"

    root = Text.unpack _path

    getProjEnv cfg = case possiblyEnvName of
        Just envName -> case lookupWhenProvided envName of
            Just env -> pure env
            Nothing -> error $ envName <> ": No such environment found."
        Nothing -> case ProjectConfig.defaultEnvironment cfg of
            Just (n, e) -> pure (Text.unpack n, e)
            Nothing -> error "Unable to find default environment. Exaclty one\
                \ environment has to have 'is-default: true', but no such\
                \ environment was found."
      where
        lookupWhenProvided n =
            (n, ) <$> ProjectConfig.getEnvironment cfg (fromString n)

newProject :: DbConnection -> FilePath -> IO Project
newProject conn relativeDir = do
    -- Function canonicalizePath preserves trailing path separator.
    dir <- dropTrailingPathSeparator <$> canonicalizePath relativeDir
    let project = Project
            { _id = 0 -- Ignored during INSERT.
            , _name = fromString $ takeFileName dir
            , _path = fromString dir
            }
    addProject conn project
    pure project

modifyEnvVariables
    :: Project
    -> EnvironmentName
    -> IO ([(String, String)] -> [(String, String)])
modifyEnvVariables project projEnvName = (. go) <$> addYxEnvVariables project
  where
    go = (getRidOfStackEnvVariables .) . map $ \case
        (n@"PATH", path) -> (n, updatePathEnvVariable project projEnvName path)
            -- TODO: Correct implementation witouth hardcoded environment.
        ev -> ev

updatePathEnvVariable :: Project -> EnvironmentName -> String -> String
updatePathEnvVariable project environment path = yxBinDir <> ":" <> path
  where
    yxBinDir = yxExeStuff root environment
    root = Text.unpack $ _path project

getRidOfStackEnvVariables :: [(String, String)] -> [(String, String)]
getRidOfStackEnvVariables = (catMaybes .) . map $ \case
    -- These variables are exported by stack and occur when we are already in a
    -- "stack exec" environment. For the purposes of isolation we need to get
    -- rid of them. Keeping them means that e.g. changing GHC version would
    -- fail due to trying to open incorrect package DB.
    ("STACK_EXE", _) -> Nothing
    ("GHC_VERSION", _) -> Nothing
    ("GHC_PACKAGE_PATH", _) -> Nothing
    ("CABAL_INSTALL_VERSION", _) -> Nothing
    ("HASKELL_PACKAGE_SANDBOX", _) -> Nothing
    ("HASKELL_PACKAGE_SANDBOXES", _) -> Nothing
    ("HASKELL_DIST_DIR", _) -> Nothing
    e -> Just e

addYxEnvVariables
    :: Project
    -> IO ([(String, String)] -> [(String, String)])
addYxEnvVariables Project{..} = do
    yxExe <- getExecutablePath
    yxInvokedName <- getProgName
    pure . foldr (.) id $ catMaybes
        [ add "YX_ENVIRONMENT_DIR"  $ Just projectEnvironmentDir
        , add "YX_ENVIRONMENT"      $ Just projectEnvironment
        , add "YX_EXE"              $ Just yxExe
        , add "YX_INVOKED_AS"       $ Just yxInvokedName
        , add "YX_PROJECT"          . Just $ Text.unpack _name
        , add "YX_PROJECT_ROOT"     $ Just projectRoot
        , add "YX_STUFF"            $ Just yxStuffDir
        , add "YX_VERSION"          . Just $ showVersion version
        ]
  where
    projectRoot = Text.unpack _path
    yxStuffDir = yxStuffPath (Just projectRoot) YxStuff
    projectEnvironment = "default"
    projectEnvironmentDir = yxStuffPath (Just projectRoot) . EnvironmentStuff
        $ Just projectEnvironment

    add :: String
        -> Maybe String
        -> Maybe ([(String, String)] -> [(String, String)])
    add n = fmap $ (:) . (n,)

getYxConfigDir :: IO FilePath
getYxConfigDir = do
    dir <- getXdgDirectory XdgConfig "yx"
    createDirectoryIfMissing True dir
    pure dir

getYxDatabaseFile :: IO FilePath
getYxDatabaseFile = do
    dbFile <- (</> "data.db") <$> getYxConfigDir
    haveDbFile <- doesFileExist dbFile
    unless haveDbFile $ SQLite.withConnection dbFile $ \conn ->
        SQLite.execute_ conn
            "CREATE TABLE Project\
                \(id INTEGER PRIMARY KEY AUTOINCREMENT,\
                \ name TEXT UNIQUE NOT NULL,\
                \ path TEXT UNIQUE NOT NULL\
                \);"
    pure dbFile
runProjectEnvironment yxExe possiblyEnvName project@Project{..} = do
    projectCfg <- initializeProject yxExe root
    (projEnvName, _projEnv) <- getProjEnv projectCfg
    setCurrentDirectory root
    env <- modifyEnvVariables project projEnvName <*> getEnvironment
    shell <- findShell
        (lookup "PATH" env)
        (lookup "SHELL" env)
        defaultShells
    executeFile shell True ["--rcfile", bashrc projEnvName] (Just env)
  where
    -- We currently support only bash, so it makes sense to have it as default.
    defaultShells = shellExePaths Bash
    bashrc projEnvName = yxShellStuff root projEnvName Bash </> "bashrc"

    root = Text.unpack _path

    getProjEnv cfg = case possiblyEnvName of
        Just envName -> case lookupWhenProvided envName of
            Just env -> pure env
            Nothing -> error $ envName <> ": No such environment found."
        Nothing -> case ProjectConfig.defaultEnvironment cfg of
            Just (n, e) -> pure (Text.unpack n, e)
            Nothing -> error "Unable to find default environment. Exaclty one\
                \ environment has to have 'is-default: true', but no such\
                \ environment was found."
      where
        lookupWhenProvided n =
            (n, ) <$> ProjectConfig.getEnvironment cfg (fromString n)

newProject :: DbConnection -> FilePath -> IO Project
newProject conn relativeDir = do
    -- Function canonicalizePath preserves trailing path separator.
    dir <- dropTrailingPathSeparator <$> canonicalizePath relativeDir
    let project = Project
            { _id = 0 -- Ignored during INSERT.
            , _name = fromString $ takeFileName dir
            , _path = fromString dir
            }
    addProject conn project
    pure project

modifyEnvVariables
    :: Project
    -> EnvironmentName
    -> IO ([(String, String)] -> [(String, String)])
modifyEnvVariables project projEnvName = (. go) <$> addYxEnvVariables project
  where
    go = (getRidOfStackEnvVariables .) . map $ \case
        (n@"PATH", path) -> (n, updatePathEnvVariable project projEnvName path)
            -- TODO: Correct implementation witouth hardcoded environment.
        ev -> ev

updatePathEnvVariable :: Project -> EnvironmentName -> String -> String
updatePathEnvVariable project environment path = yxBinDir <> ":" <> path
  where
    yxBinDir = yxExeStuff root environment
    root = Text.unpack $ _path project

getRidOfStackEnvVariables :: [(String, String)] -> [(String, String)]
getRidOfStackEnvVariables = (catMaybes .) . map $ \case
    -- These variables are exported by stack and occur when we are already in a
    -- "stack exec" environment. For the purposes of isolation we need to get
    -- rid of them. Keeping them means that e.g. changing GHC version would
    -- fail due to trying to open incorrect package DB.
    ("STACK_EXE", _) -> Nothing
    ("GHC_VERSION", _) -> Nothing
    ("GHC_PACKAGE_PATH", _) -> Nothing
    ("CABAL_INSTALL_VERSION", _) -> Nothing
    ("HASKELL_PACKAGE_SANDBOX", _) -> Nothing
    ("HASKELL_PACKAGE_SANDBOXES", _) -> Nothing
    ("HASKELL_DIST_DIR", _) -> Nothing
    e -> Just e

addYxEnvVariables
    :: Project
    -> IO ([(String, String)] -> [(String, String)])
addYxEnvVariables Project{..} = do
    yxExe <- getExecutablePath
    yxInvokedName <- getProgName
    pure . foldr (.) id $ catMaybes
        [ add "YX_ENVIRONMENT_DIR"  $ Just projectEnvironmentDir
        , add "YX_ENVIRONMENT"      $ Just projectEnvironment
        , add "YX_EXE"              $ Just yxExe
        , add "YX_INVOKED_AS"       $ Just yxInvokedName
        , add "YX_PROJECT"          . Just $ Text.unpack _name
        , add "YX_PROJECT_ROOT"     $ Just projectRoot
        , add "YX_STUFF"            $ Just yxStuffDir
        , add "YX_VERSION"          . Just $ showVersion version
        ]
  where
    projectRoot = Text.unpack _path
    yxStuffDir = yxStuffPath (Just projectRoot) YxStuff
    projectEnvironment = "default"
    projectEnvironmentDir = yxStuffPath (Just projectRoot) . EnvironmentStuff
        $ Just projectEnvironment

    add :: String
        -> Maybe String
        -> Maybe ([(String, String)] -> [(String, String)])
    add n = fmap $ (:) . (n,)

getYxConfigDir :: IO FilePath
getYxConfigDir = do
    dir <- getXdgDirectory XdgConfig "yx"
    createDirectoryIfMissing True dir
    pure dir

getYxDatabaseFile :: IO FilePath
getYxDatabaseFile = do
    dbFile <- (</> "data.db") <$> getYxConfigDir
    haveDbFile <- doesFileExist dbFile
    unless haveDbFile $ SQLite.withConnection dbFile $ \conn ->
        SQLite.execute_ conn
            "CREATE TABLE Project\
                \(id INTEGER PRIMARY KEY AUTOINCREMENT,\
                \ name TEXT UNIQUE NOT NULL,\
                \ path TEXT UNIQUE NOT NULL\
                \);"
    pure dbFile