{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeApplications #-}

-- | cabal-install CLI command: repl
module Distribution.Client.CmdRepl
  ( -- * The @repl@ CLI and action
    replCommand
  , replAction
  , ReplFlags (..)

    -- * Internals exposed for testing
  , matchesMultipleProblem
  , selectPackageTargets
  , selectComponentTarget
  , MultiReplDecision (..)
  ) where

import Distribution.Client.Compat.Prelude
import Prelude ()

import Distribution.Compat.Lens
import qualified Distribution.Types.Lens as L

import Distribution.Client.CmdErrorMessages
  ( Plural (..)
  , componentKind
  , renderComponentKind
  , renderListCommaAnd
  , renderListSemiAnd
  , renderTargetProblem
  , renderTargetSelector
  , showTargetSelector
  , sortGroupOn
  , targetSelectorRefersToPkgs
  )
import Distribution.Client.DistDirLayout
  ( DistDirLayout (..)
  )
import Distribution.Client.Errors
import qualified Distribution.Client.InstallPlan as InstallPlan
import Distribution.Client.NixStyleOptions
  ( NixStyleFlags (..)
  , cfgVerbosity
  , defaultNixStyleFlags
  , nixStyleOptions
  )
import Distribution.Client.ProjectBuilding
  ( improveInstallPlanWithUpToDatePackages
  , rebuildTargetsDryRun
  )
import Distribution.Client.ProjectOrchestration
import Distribution.Client.ProjectPlanning
  ( ElaboratedInstallPlan
  , ElaboratedSharedConfig (..)
  )
import Distribution.Client.ProjectPlanning.Types
  ( elabOrderExeDependencies
  , showElaboratedInstallPlan
  )
import Distribution.Client.ScriptUtils
  ( AcceptNoTargets (..)
  , TargetContext (..)
  , fakeProjectSourcePackage
  , lSrcpkgDescription
  , updateContextAndWriteProjectFile
  , updateContextAndWriteProjectFile'
  , withContextAndSelectors
  )
import Distribution.Client.Setup
  ( ConfigFlags (..)
  , GlobalFlags
  )
import qualified Distribution.Client.Setup as Client
import Distribution.Client.TargetProblem
  ( TargetProblem (..)
  )
import Distribution.Client.Targets
  ( UserConstraint (..)
  , UserConstraintScope (..)
  )
import Distribution.Client.Types
  ( PackageSpecifier (..)
  , UnresolvedSourcePackage
  )
import Distribution.Compiler
  ( CompilerFlavor (GHC)
  )
import Distribution.Package
  ( Package (..)
  , UnitId
  , installedUnitId
  , mkPackageName
  , packageName
  )
import Distribution.Simple.Command
  ( CommandUI (..)
  , usageAlternatives
  )
import Distribution.Simple.Compiler
  ( Compiler
  , PackageDBX (..)
  , compilerCompatVersion
  )
import Distribution.Simple.Program.GHC
import Distribution.Simple.Setup
  ( Flag
  , ReplOptions (..)
  , commonSetupTempFileOptions
  )
import Distribution.Simple.Utils
  ( debugNoWrap
  , dieWithException
  , withTempDirectoryEx
  , wrapText
  )
import Distribution.Solver.Types.ConstraintSource
  ( ConstraintSource (ConstraintSourceMultiRepl, ConstraintSourceWithRepl)
  )
import Distribution.Solver.Types.PackageConstraint
  ( PackageProperty (PackagePropertyVersion)
  )
import Distribution.Solver.Types.SourcePackage
  ( SourcePackage (..)
  )
import Distribution.Types.BuildInfo
  ( BuildInfo (..)
  , emptyBuildInfo
  )
import Distribution.Types.ComponentName
  ( componentNameString
  )
import Distribution.Types.CondTree
  ( CondTree (..)
  )
import Distribution.Types.Dependency
  ( Dependency (..)
  , mainLibSet
  )
import Distribution.Types.Library
  ( Library (..)
  , emptyLibrary
  )
import Distribution.Types.ParStrat
import Distribution.Types.Version
  ( Version
  , mkVersion
  )
import Distribution.Types.VersionRange
  ( anyVersion
  , orLaterVersion
  )
import Distribution.Utils.Generic
  ( safeHead
  )
import Distribution.Verbosity
  ( lessVerbose
  , normal
  )
import Language.Haskell.Extension
  ( Language (..)
  )

import Control.Monad (mapM)
import qualified Data.ByteString.Lazy as BS
import Data.List
  ( (\\)
  )
import qualified Data.Map as Map
import qualified Data.Set as Set
import Distribution.Client.ProjectConfig
  ( ProjectConfig (..)
  , ProjectConfigShared (..)
  )
import Distribution.Client.ReplFlags
  ( EnvFlags (envIncludeTransitive, envPackages)
  , ReplFlags (..)
  , defaultReplFlags
  , topReplOptions
  )
import Distribution.Compat.Binary (decode)
import Distribution.Simple.Flag (flagToMaybe, fromFlagOrDefault, pattern Flag)
import Distribution.Simple.Program.Builtin (ghcProgram)
import Distribution.Simple.Program.Db (requireProgram)
import Distribution.Simple.Program.Types
import Distribution.Types.PackageName.Magic (fakePackageId)
import System.Directory
  ( doesFileExist
  , getCurrentDirectory
  , listDirectory
  , makeAbsolute
  )
import System.FilePath
  ( searchPathSeparator
  , splitSearchPath
  , (</>)
  )
import Text.PrettyPrint hiding ((<>))

replCommand :: CommandUI (NixStyleFlags ReplFlags)
replCommand =
  Client.installCommand
    { commandName = "v2-repl"
    , commandSynopsis = "Open an interactive session for the given component."
    , commandUsage = usageAlternatives "v2-repl" ["[TARGET] [FLAGS]"]
    , commandDescription = Just $ \_ ->
        wrapText $
          "Open an interactive session for a component within the project. The "
            ++ "available targets are the same as for the 'v2-build' command: "
            ++ "individual components within packages in the project, including "
            ++ "libraries, executables, test-suites or benchmarks. Packages can "
            ++ "also be specified in which case the library component in the "
            ++ "package will be used, or the (first listed) executable in the "
            ++ "package if there is no library.\n\n"
            ++ "Dependencies are built or rebuilt as necessary. Additional "
            ++ "configuration flags can be specified on the command line and these "
            ++ "extend the project configuration from the 'cabal.project', "
            ++ "'cabal.project.local' and other files."
    , commandNotes = Just $ \pname ->
        "Examples, open an interactive session:\n"
          ++ "  "
          ++ pname
          ++ " v2-repl\n"
          ++ "    for the default component in the package in the current directory\n"
          ++ "  "
          ++ pname
          ++ " v2-repl pkgname\n"
          ++ "    for the default component in the package named 'pkgname'\n"
          ++ "  "
          ++ pname
          ++ " v2-repl ./pkgfoo\n"
          ++ "    for the default component in the package in the ./pkgfoo directory\n"
          ++ "  "
          ++ pname
          ++ " v2-repl cname\n"
          ++ "    for the component named 'cname'\n"
          ++ "  "
          ++ pname
          ++ " v2-repl pkgname:cname\n"
          ++ "    for the component 'cname' in the package 'pkgname'\n\n"
          ++ "  "
          ++ pname
          ++ " v2-repl --build-depends lens\n"
          ++ "    add the latest version of the library 'lens' to the default component "
          ++ "(or no componentif there is no project present)\n"
          ++ "  "
          ++ pname
          ++ " v2-repl --build-depends \"lens >= 4.15 && < 4.18\"\n"
          ++ "    add a version (constrained between 4.15 and 4.18) of the library 'lens' "
          ++ "to the default component (or no component if there is no project present)\n"
    , commandDefaultFlags = defaultNixStyleFlags defaultReplFlags
    , commandOptions = nixStyleOptions topReplOptions
    }

data MultiReplDecision = MultiReplDecision
  { compilerVersion :: Maybe Version
  , enabledByFlag :: Bool
  }
  deriving (Eq, Show)

useMultiRepl :: MultiReplDecision -> Bool
useMultiRepl MultiReplDecision{compilerVersion, enabledByFlag} =
  compilerVersion >= Just minMultipleHomeUnitsVersion && enabledByFlag

multiReplDecision :: ProjectConfigShared -> Compiler -> ReplFlags -> MultiReplDecision
multiReplDecision ctx compiler flags =
  MultiReplDecision
    -- Check if the compiler is new enough, need at least 9.4 to start a multi session
    (compilerCompatVersion GHC compiler)
    -- Then check the user actually asked for it, either via the project file, the global config or
    -- a repl specific option.
    (fromFlagOrDefault False (projectConfigMultiRepl ctx <> replUseMulti flags))

-- | The @repl@ command is very much like @build@. It brings the install plan
-- up to date, selects that part of the plan needed by the given or implicit
-- repl target and then executes the plan.
--
-- Compared to @build@ the difference is that multiple targets are handled
-- specially and the target type is repl rather than build. The
-- general plan execution infrastructure handles both build and repl targets.
--
-- For more details on how this works, see the module
-- "Distribution.Client.ProjectOrchestration"
replAction :: NixStyleFlags ReplFlags -> [String] -> GlobalFlags -> IO ()
replAction flags@NixStyleFlags{extraFlags = replFlags@ReplFlags{..}, configFlags} targetStrings globalFlags = do
  withCtx verbosity targetStrings $ \targetCtx ctx userTargetSelectors -> do
    when (buildSettingOnlyDeps (buildSettings ctx)) $
      dieWithException verbosity ReplCommandDoesn'tSupport
    let projectRoot = distProjectRootDirectory $ distDirLayout ctx
        distDir = distDirectory $ distDirLayout ctx

    -- After ther user selectors have been resolved, and it's decided what context
    -- we're in, implement repl-specific behaviour.
    (baseCtx, targetSelectors) <- case targetCtx of
      -- If in the project context, and no selectors are provided
      -- then produce an error.
      ProjectContext -> do
        let projectFile = projectConfigProjectFile . projectConfigShared $ projectConfig ctx
        let pkgs = projectPackages $ projectConfig ctx
        case userTargetSelectors of
          [] ->
            dieWithException verbosity $
              RenderReplTargetProblem [render (reportProjectNoTarget projectFile pkgs)]
          _ -> return (ctx, userTargetSelectors)
      -- In the global context, construct a fake package which can be used to start
      -- a repl with extra arguments if `-b` is given.
      GlobalContext -> do
        unless (null userTargetSelectors) $
          dieWithException verbosity $
            ReplTakesNoArguments targetStrings
        let
          sourcePackage =
            fakeProjectSourcePackage projectRoot
              & ( (lSrcpkgDescription . L.condLibrary)
                    ?~ (CondNode library [baseDep] [])
                )
          library = emptyLibrary{libBuildInfo = lBuildInfo}
          lBuildInfo =
            emptyBuildInfo
              { targetBuildDepends = [baseDep] ++ envPackages replEnvFlags
              , defaultLanguage = Just Haskell2010
              }
          baseDep = Dependency "base" anyVersion mainLibSet

        -- Write the fake package
        updatedCtx <- updateContextAndWriteProjectFile' ctx sourcePackage
        -- Specify the selector for this package
        let fakeSelector = TargetPackage TargetExplicitNamed [fakePackageId] Nothing
        return (updatedCtx, [fakeSelector])

      -- For the script context, no special behaviour.
      ScriptContext scriptPath scriptExecutable -> do
        unless (length targetStrings == 1) $
          dieWithException verbosity $
            ReplTakesSingleArgument targetStrings
        existsScriptPath <- doesFileExist scriptPath
        unless existsScriptPath $
          dieWithException verbosity $
            ReplTakesSingleArgument targetStrings

        updatedCtx <- updateContextAndWriteProjectFile ctx scriptPath scriptExecutable
        return (updatedCtx, userTargetSelectors)

    -- If multi-repl is used, we need a Cabal recent enough to handle it.
    -- We need to do this before solving, but the compiler version is only known
    -- after solving (phaseConfigureCompiler), so instead of using
    -- multiReplDecision we just check the flag.
    let multiReplEnabled =
          fromFlagOrDefault False $
            projectConfigMultiRepl (projectConfigShared $ projectConfig baseCtx)
              <> replUseMulti

        withReplEnabled =
          isJust $ flagToMaybe $ replWithRepl configureReplOptions

        addConstraintWhen cond constraint base_ctx =
          if cond
            then
              base_ctx
                & lProjectConfig . lProjectConfigShared . lProjectConfigConstraints
                  %~ (constraint :)
            else base_ctx

        -- This is the constraint setup.Cabal>=3.11. 3.11 is when Cabal options
        -- used for multi-repl were introduced.
        -- Idelly we'd apply this constraint only on the closure of repl targets,
        -- but that would require another solver run for marginal advantages that
        -- will further shrink as 3.11 is adopted.
        addMultiReplConstraint = addConstraintWhen multiReplEnabled $ requireCabal [3, 11] ConstraintSourceMultiRepl

        -- Similarly, if you use `--with-repl` then your version of `Cabal` needs to
        -- support the `--with-repl` flag.
        addWithReplConstraint = addConstraintWhen withReplEnabled $ requireCabal [3, 15] ConstraintSourceWithRepl

        baseCtx' = addMultiReplConstraint $ addWithReplConstraint baseCtx

    (originalComponent, baseCtx'') <-
      if null (envPackages replEnvFlags)
        then return (Nothing, baseCtx')
        else -- Unfortunately, the best way to do this is to let the normal solver
        -- help us resolve the targets, but that isn't ideal for performance,
        -- especially in the no-project case.
        withInstallPlan (lessVerbose verbosity) baseCtx' $ \elaboratedPlan sharedConfig -> do
          -- targets should be non-empty map, but there's no NonEmptyMap yet.
          targets <- validatedTargets' (projectConfigShared (projectConfig ctx)) (pkgConfigCompiler sharedConfig) elaboratedPlan targetSelectors

          let
            (unitId, _) = fromMaybe (error "panic: targets should be non-empty") $ safeHead $ Map.toList targets
            originalDeps = installedUnitId <$> InstallPlan.directDeps elaboratedPlan unitId
            oci = OriginalComponentInfo unitId originalDeps
            pkgId = fromMaybe (error $ "cannot find " ++ prettyShow unitId) $ packageId <$> InstallPlan.lookup elaboratedPlan unitId
            baseCtx'' = addDepsToProjectTarget (envPackages replEnvFlags) pkgId baseCtx'

          return (Just oci, baseCtx'')

    -- Now, we run the solver again with the added packages. While the graph
    -- won't actually reflect the addition of transitive dependencies,
    -- they're going to be available already and will be offered to the REPL
    -- and that's good enough.
    --
    -- In addition, to avoid a *third* trip through the solver, we are
    -- replicating the second half of 'runProjectPreBuildPhase' by hand
    -- here.
    (buildCtx, compiler, platform, replOpts', targets) <- withInstallPlan verbosity baseCtx'' $
      \elaboratedPlan elaboratedShared' -> do
        let ProjectBaseContext{..} = baseCtx''

        -- Recalculate with updated project.
        targets <- validatedTargets' (projectConfigShared projectConfig) (pkgConfigCompiler elaboratedShared') elaboratedPlan targetSelectors

        let
          elaboratedPlan' =
            pruneInstallPlanToTargets
              TargetActionRepl
              targets
              elaboratedPlan
          includeTransitive = fromFlagOrDefault True (envIncludeTransitive replEnvFlags)

        pkgsBuildStatus <-
          rebuildTargetsDryRun
            distDirLayout
            elaboratedShared'
            elaboratedPlan'

        let elaboratedPlan'' =
              improveInstallPlanWithUpToDatePackages
                pkgsBuildStatus
                elaboratedPlan'
        debugNoWrap verbosity (showElaboratedInstallPlan elaboratedPlan'')

        let
          buildCtx =
            ProjectBuildContext
              { elaboratedPlanOriginal = elaboratedPlan
              , elaboratedPlanToExecute = elaboratedPlan''
              , elaboratedShared = elaboratedShared'
              , pkgsBuildStatus
              , targetsMap = targets
              }

          ElaboratedSharedConfig{pkgConfigCompiler = compiler, pkgConfigPlatform = platform} = elaboratedShared'

          repl_flags = case originalComponent of
            Just oci -> generateReplFlags includeTransitive elaboratedPlan' oci
            Nothing -> []

        return (buildCtx, compiler, platform, configureReplOptions & lReplOptionsFlags %~ (++ repl_flags), targets)

    -- Multi Repl implementation see: https://well-typed.com/blog/2023/03/cabal-multi-unit/ for
    -- a high-level overview about how everything fits together.
    if Set.size (distinctTargetComponents targets) > 1
      then withTempDirectoryEx verbosity tempFileOptions distDir "multi-out" $ \dir' -> do
        -- multi target repl
        dir <- makeAbsolute dir'
        -- Modify the replOptions so that the ./Setup repl command will write options
        -- into the multi-out directory.
        replOpts'' <- case targetCtx of
          ProjectContext -> return $ replOpts'{replOptionsFlagOutput = Flag dir}
          _ -> usingGhciScript compiler projectRoot replOpts'

        let buildCtx' = buildCtx & lElaboratedShared . lPkgConfigReplOptions .~ replOpts''
        printPlan verbosity baseCtx'' buildCtx'

        -- The project build phase will call `./Setup repl` but write the options
        -- out into a file without starting a repl.
        buildOutcomes <- runProjectBuildPhase verbosity baseCtx'' buildCtx'
        runProjectPostBuildPhase verbosity baseCtx'' buildCtx' buildOutcomes

        -- calculate PATH, we construct a PATH which is the union of all paths from
        -- the units which have been loaded. This is not quite right but usually works fine.
        path_files <- listDirectory (dir </> "paths")

        -- Note: decode is partial. Should we use Structured here?
        -- This might blow up with @build-type: Custom@ stuff.
        ghcProgs <- mapM (\f -> decode @ConfiguredProgram <$> BS.readFile (dir </> "paths" </> f)) path_files

        let all_paths = concatMap programOverrideEnv ghcProgs
        let sp = intercalate [searchPathSeparator] (map fst (sortBy (comparing @Int snd) $ Map.toList (combine_search_paths all_paths)))
        -- HACK: Just combine together all env overrides, placing the most common things last

        -- ghc program with overridden PATH
        (ghcProg, _) <- requireProgram verbosity ghcProgram (pkgConfigCompilerProgs (elaboratedShared buildCtx'))
        let ghcProg' = ghcProg{programOverrideEnv = [("PATH", Just sp)]}

        -- Find what the unit files are, and start a repl based on all the response
        -- files which have been created in the directory.
        -- unit files for components
        unit_files <- (filter (/= "paths")) <$> listDirectory dir

        -- Order the unit files so that the find target becomes the active unit
        let active_unit_fp :: Maybe FilePath
            active_unit_fp = do
              -- Get the first target selectors from the cli
              activeTarget <- safeHead targetSelectors
              -- Lookup the targets :: Map UnitId [(ComponentTarget, NonEmpty TargetSelector)]
              unitId <-
                Map.toList targets
                  -- Keep the UnitId matching the desired target selector
                  & find (\(_, xs) -> any (\(_, selectors) -> activeTarget `elem` selectors) xs)
                  & fmap fst
              -- Convert to filename (adapted from 'storePackageDirectory')
              pure (prettyShow unitId)
            unit_files_ordered :: [FilePath]
            unit_files_ordered =
              let (active_unit_files, other_units) = partition (\fp -> Just fp == active_unit_fp) unit_files
               in -- GHC considers the last unit passed to be the active one
                  other_units ++ active_unit_files

            convertParStrat :: ParStratX Int -> ParStratX String
            convertParStrat Serial = Serial
            convertParStrat (UseSem n) = NumJobs (Just n)
            convertParStrat (NumJobs mn) = NumJobs mn

        let ghc_opts =
              mempty
                { ghcOptMode = Flag GhcModeInteractive
                , ghcOptUnitFiles = map (dir </>) unit_files_ordered
                , ghcOptNumJobs = Flag (convertParStrat (buildSettingNumJobs (buildSettings ctx)))
                , ghcOptPackageDBs = [GlobalPackageDB]
                }

        -- run ghc --interactive with
        runReplProgram (flagToMaybe $ replWithRepl replOpts') tempFileOptions verbosity ghcProg' compiler platform Nothing ghc_opts
      else do
        -- single target repl
        replOpts'' <- case targetCtx of
          ProjectContext -> return replOpts'
          _ -> usingGhciScript compiler projectRoot replOpts'

        let buildCtx' = buildCtx & lElaboratedShared . lPkgConfigReplOptions .~ replOpts''
        printPlan verbosity baseCtx'' buildCtx'

        buildOutcomes <- runProjectBuildPhase verbosity baseCtx'' buildCtx'
        runProjectPostBuildPhase verbosity baseCtx'' buildCtx' buildOutcomes
  where
    combine_search_paths paths =
      foldl' go Map.empty paths
      where
        go m ("PATH", Just s) = foldl' (\m' f -> Map.insertWith (+) f 1 m') m (splitSearchPath s)
        go m _ = m

    withCtx ctxVerbosity strings =
      withContextAndSelectors ctxVerbosity AcceptNoTargets (Just LibKind) flags strings globalFlags ReplCommand

    verbosity = cfgVerbosity normal flags
    tempFileOptions = commonSetupTempFileOptions $ configCommonFlags configFlags

    validatedTargets' = validatedTargets verbosity replFlags

-- | Create a constraint which requires a later version of Cabal.
-- This is used for commands which require a specific feature from the Cabal library
-- such as multi-repl or the --with-repl flag.
requireCabal :: [Int] -> ConstraintSource -> (UserConstraint, ConstraintSource)
requireCabal version source =
  ( UserConstraint
      (UserAnySetupQualifier (mkPackageName "Cabal"))
      (PackagePropertyVersion $ orLaterVersion $ mkVersion version)
  , source
  )

reportProjectNoTarget :: Flag FilePath -> [String] -> Doc
reportProjectNoTarget projectFile pkgs =
  case (null pkgs, projectName) of
    (True, Just project) ->
      text "There are no packages in"
        <+> (project <> char '.')
        <+> text "Please add a package to the project and"
        <+> pickComponent
    (True, Nothing) ->
      text "Please add a package to the project and" <+> pickComponent
    (False, Just project) ->
      text "Please"
        <+> pickComponent
        <+> text "The packages in"
        <+> project
        <+> (text "from which to select a component target are" <> colon)
        $+$ nest 1 (vcat [text "-" <+> text pkg | pkg <- sort pkgs])
    (False, Nothing) ->
      text "Please"
        <+> pickComponent
        <+> (text "The packages from which to select a component in 'cabal.project'" <> comma)
        <+> (text "the implicit default as if `--project-file=cabal.project` was added as a command option" <> comma)
        <+> (text "are" <> colon)
        $+$ nest 1 (vcat [text "-" <+> text pkg | pkg <- sort pkgs])
  where
    projectName = case projectFile of
      Flag "" -> Nothing
      Flag n -> Just $ quotes (text n)
      _ -> Nothing
    pickComponent = text "pick a single [package:][ctype:]component (or all) as target for the REPL command."

-- | Invariant: validatedTargets returns at least one target for the REPL.
validatedTargets
  :: Verbosity
  -> ReplFlags
  -> ProjectConfigShared
  -> Compiler
  -> ElaboratedInstallPlan
  -> [TargetSelector]
  -> IO TargetsMap
validatedTargets verbosity replFlags ctx compiler elaboratedPlan targetSelectors = do
  let multi_repl_enabled = multiReplDecision ctx compiler replFlags
  -- Interpret the targets on the command line as repl targets (as opposed to
  -- say build or haddock targets).
  targets <-
    either (reportTargetProblems verbosity) return $
      resolveTargetsFromSolver
        (selectPackageTargets multi_repl_enabled)
        selectComponentTarget
        elaboratedPlan
        Nothing
        targetSelectors

  -- Reject multiple targets, or at least targets in different components. It is
  -- ok to have two module/file targets in the same component, but not two that
  -- live in different components.
  when (Set.size (distinctTargetComponents targets) > 1 && not (useMultiRepl multi_repl_enabled)) $
    reportTargetProblems
      verbosity
      [multipleTargetsProblem multi_repl_enabled targets]

  return targets

-- | First version of GHC which supports multiple home packages
minMultipleHomeUnitsVersion :: Version
minMultipleHomeUnitsVersion = mkVersion [9, 4]

data OriginalComponentInfo = OriginalComponentInfo
  { ociUnitId :: UnitId
  , ociOriginalDeps :: [UnitId]
  }
  deriving (Show)

addDepsToProjectTarget
  :: [Dependency]
  -> PackageId
  -> ProjectBaseContext
  -> ProjectBaseContext
addDepsToProjectTarget deps pkgId ctx =
  (\p -> ctx{localPackages = p}) . fmap addDeps . localPackages $ ctx
  where
    addDeps
      :: PackageSpecifier UnresolvedSourcePackage
      -> PackageSpecifier UnresolvedSourcePackage
    addDeps (SpecificSourcePackage pkg)
      | packageId pkg /= pkgId = SpecificSourcePackage pkg
      | SourcePackage{..} <- pkg =
          SpecificSourcePackage $
            pkg
              { srcpkgDescription =
                  -- New dependencies are added to the original ones found in the
                  -- `targetBuildDepends` field.
                  -- `traverseBuildInfos` is used in order to update _all_ the
                  -- occurrences of the field `targetBuildDepends`. It ensures that
                  -- fields depending on the latter are also consistently updated.
                  srcpkgDescription
                    & (L.traverseBuildInfos . L.targetBuildDepends)
                      %~ (deps ++)
              }
    addDeps spec = spec

generateReplFlags :: Bool -> ElaboratedInstallPlan -> OriginalComponentInfo -> [String]
generateReplFlags includeTransitive elaboratedPlan OriginalComponentInfo{..} = flags
  where
    exeDeps :: [UnitId]
    exeDeps =
      foldMap
        (InstallPlan.foldPlanPackage (const []) elabOrderExeDependencies)
        (InstallPlan.dependencyClosure elaboratedPlan [ociUnitId])

    deps, deps', trans, trans' :: [UnitId]
    flags :: [String]
    deps = installedUnitId <$> InstallPlan.directDeps elaboratedPlan ociUnitId
    deps' = deps \\ ociOriginalDeps
    trans = installedUnitId <$> InstallPlan.dependencyClosure elaboratedPlan deps'
    trans' = trans \\ ociOriginalDeps
    flags =
      fmap (("-package-id " ++) . prettyShow) . (\\ exeDeps) $
        if includeTransitive then trans' else deps'

-- | Add repl options to ensure the repl actually starts in the current working directory.
--
-- In a global or script context, when we are using a fake package, @cabal repl@
-- starts in the fake package directory instead of the directory it was called from,
-- so we need to tell ghci to change back to the correct directory.
--
-- The @-ghci-script@ flag is path to the ghci script responsible for changing to the
-- correct directory. Only works on GHC >= 7.6, though. 🙁
usingGhciScript :: Compiler -> FilePath -> ReplOptions -> IO ReplOptions
usingGhciScript compiler projectRoot replOpts
  | compilerCompatVersion GHC compiler >= Just minGhciScriptVersion = do
      let ghciScriptPath = projectRoot </> "setcwd.ghci"
      cwd <- getCurrentDirectory
      writeFile ghciScriptPath (":cd " ++ cwd)
      return $ replOpts & lReplOptionsFlags %~ (("-ghci-script" ++ ghciScriptPath) :)
  | otherwise = return replOpts

-- | First version of GHC where GHCi supported the flag we need.
-- https://downloads.haskell.org/~ghc/7.6.1/docs/html/users_guide/release-7-6-1.html
minGhciScriptVersion :: Version
minGhciScriptVersion = mkVersion [7, 6]

-- | This defines what a 'TargetSelector' means for the @repl@ command.
-- It selects the 'AvailableTarget's that the 'TargetSelector' refers to,
-- or otherwise classifies the problem.
--
-- For repl we select:
--
-- * the library if there is only one and it's buildable; or
--
-- * the exe if there is only one and it's buildable; or
--
-- * any other buildable component.
--
-- Fail if there are no buildable lib\/exe components, or if there are
-- multiple libs or exes.
selectPackageTargets
  :: MultiReplDecision
  -> TargetSelector
  -> [AvailableTarget k]
  -> Either ReplTargetProblem [k]
selectPackageTargets multiple_targets_allowed =
  -- If explicitly enabled, then select the targets like we would for multi-repl but
  -- might still fail later because of compiler version.
  if enabledByFlag multiple_targets_allowed
    then selectPackageTargetsMulti
    else selectPackageTargetsSingle multiple_targets_allowed

selectPackageTargetsMulti
  :: TargetSelector
  -> [AvailableTarget k]
  -> Either ReplTargetProblem [k]
selectPackageTargetsMulti targetSelector targets
  | not (null targetsBuildable) =
      Right targetsBuildable
  -- If there are no targets at all then we report that
  | otherwise =
      Left (TargetProblemNoTargets targetSelector)
  where
    ( targetsBuildable
      , _
      ) =
        selectBuildableTargetsWith'
          (isRequested targetSelector)
          targets

    -- When there's a target filter like "pkg:tests" then we do select tests,
    -- but if it's just a target like "pkg" then we don't build tests unless
    -- they are requested by default (i.e. by using --enable-tests)
    isRequested (TargetAllPackages Nothing) TargetNotRequestedByDefault = False
    isRequested (TargetPackage _ _ Nothing) TargetNotRequestedByDefault = False
    isRequested _ _ = True

-- | Target selection behaviour which only select a single target.
-- This is used when the compiler version doesn't support multi-repl or the user
-- didn't request it.
selectPackageTargetsSingle
  :: MultiReplDecision
  -> TargetSelector
  -> [AvailableTarget k]
  -> Either ReplTargetProblem [k]
selectPackageTargetsSingle decision targetSelector targets
  -- If there is exactly one buildable library then we select that
  | [target] <- targetsLibsBuildable =
      Right [target]
  -- but fail if there are multiple buildable libraries.
  | not (null targetsLibsBuildable) =
      Left (matchesMultipleProblem decision targetSelector targetsLibsBuildable')
  -- If there is exactly one buildable executable then we select that
  | [target] <- targetsExesBuildable =
      Right [target]
  -- but fail if there are multiple buildable executables.
  | not (null targetsExesBuildable) =
      Left (matchesMultipleProblem decision targetSelector targetsExesBuildable')
  -- If there is exactly one other target then we select that
  | [target] <- targetsBuildable =
      Right [target]
  -- but fail if there are multiple such targets
  | not (null targetsBuildable) =
      Left (matchesMultipleProblem decision targetSelector targetsBuildable')
  -- If there are targets but none are buildable then we report those
  | not (null targets) =
      Left (TargetProblemNoneEnabled targetSelector targets')
  -- If there are no targets at all then we report that
  | otherwise =
      Left (TargetProblemNoTargets targetSelector)
  where
    targets' = forgetTargetsDetail targets
    ( targetsLibsBuildable
      , targetsLibsBuildable'
      ) =
        selectBuildableTargets'
          . filterTargetsKind LibKind
          $ targets
    ( targetsExesBuildable
      , targetsExesBuildable'
      ) =
        selectBuildableTargets'
          . filterTargetsKind ExeKind
          $ targets
    ( targetsBuildable
      , targetsBuildable'
      ) =
        selectBuildableTargetsWith'
          (isRequested targetSelector)
          targets

    -- When there's a target filter like "pkg:tests" then we do select tests,
    -- but if it's just a target like "pkg" then we don't build tests unless
    -- they are requested by default (i.e. by using --enable-tests)
    isRequested (TargetAllPackages Nothing) TargetNotRequestedByDefault = False
    isRequested (TargetPackage _ _ Nothing) TargetNotRequestedByDefault = False
    isRequested _ _ = True

-- | For a 'TargetComponent' 'TargetSelector', check if the component can be
-- selected.
--
-- For the @repl@ command we just need the basic checks on being buildable etc.
selectComponentTarget
  :: SubComponentTarget
  -> AvailableTarget k
  -> Either ReplTargetProblem k
selectComponentTarget = selectComponentTargetBasic

data ReplProblem
  = TargetProblemMatchesMultiple MultiReplDecision TargetSelector [AvailableTarget ()]
  | -- | Multiple 'TargetSelector's match multiple targets
    TargetProblemMultipleTargets MultiReplDecision TargetsMap
  deriving (Eq, Show)

-- | The various error conditions that can occur when matching a
-- 'TargetSelector' against 'AvailableTarget's for the @repl@ command.
type ReplTargetProblem = TargetProblem ReplProblem

matchesMultipleProblem
  :: MultiReplDecision
  -> TargetSelector
  -> [AvailableTarget ()]
  -> ReplTargetProblem
matchesMultipleProblem decision targetSelector targetsExesBuildable =
  CustomTargetProblem $ TargetProblemMatchesMultiple decision targetSelector targetsExesBuildable

multipleTargetsProblem
  :: MultiReplDecision
  -> TargetsMap
  -> ReplTargetProblem
multipleTargetsProblem decision = CustomTargetProblem . TargetProblemMultipleTargets decision

reportTargetProblems :: Verbosity -> [TargetProblem ReplProblem] -> IO a
reportTargetProblems verbosity =
  dieWithException verbosity . RenderReplTargetProblem . map renderReplTargetProblem

renderReplTargetProblem :: TargetProblem ReplProblem -> String
renderReplTargetProblem = renderTargetProblem "open a repl for" renderReplProblem

renderReplProblem :: ReplProblem -> String
renderReplProblem (TargetProblemMatchesMultiple decision targetSelector targets) =
  "Cannot open a repl for multiple components at once. The target '"
    ++ showTargetSelector targetSelector
    ++ "' refers to "
    ++ renderTargetSelector targetSelector
    ++ " which "
    ++ (if targetSelectorRefersToPkgs targetSelector then "includes " else "are ")
    ++ renderListSemiAnd
      [ "the "
        ++ renderComponentKind Plural ckind
        ++ " "
        ++ renderListCommaAnd
          [ maybe (prettyShow pkgname) prettyShow (componentNameString cname)
          | t <- ts
          , let cname = availableTargetComponentName t
                pkgname = packageName (availableTargetPackageId t)
          ]
      | (ckind, ts) <- sortGroupOn availableTargetComponentKind targets
      ]
    ++ ".\n\n"
    ++ explainMultiReplDecision decision
  where
    availableTargetComponentKind =
      componentKind
        . availableTargetComponentName
renderReplProblem (TargetProblemMultipleTargets multi_decision selectorMap) =
  "Cannot open a repl for multiple components at once. The targets "
    ++ renderListCommaAnd
      [ "'" ++ showTargetSelector ts ++ "'"
      | ts <- uniqueTargetSelectors selectorMap
      ]
    ++ " refer to different components."
    ++ ".\n\n"
    ++ explainMultiReplDecision multi_decision

explainMultiReplDecision :: MultiReplDecision -> [Char]
explainMultiReplDecision MultiReplDecision{compilerVersion, enabledByFlag} =
  case (compilerVersion >= Just minMultipleHomeUnitsVersion, enabledByFlag) of
    -- Compiler not new enough, and not requested anyway.
    (False, False) -> explanationSingleComponentLimitation compilerVersion
    -- Compiler too old, but was requested
    (False, True) -> "Multiple component session requested but compiler version is too old.\n" ++ explanationSingleComponentLimitation compilerVersion
    -- Compiler new enough, but not requested
    (True, False) -> explanationNeedToEnableFlag
    _ -> error "explainMultiReplDecision"

explanationNeedToEnableFlag :: String
explanationNeedToEnableFlag =
  "Your compiler supports a multiple component repl but support is not enabled.\n"
    ++ "The experimental multi repl can be enabled by\n"
    ++ "  * Globally: Setting multi-repl: True in your .cabal/config\n"
    ++ "  * Project Wide: Setting multi-repl: True in your cabal.project file\n"
    ++ "  * Per Invocation: By passing --enable-multi-repl when starting the repl"

explanationSingleComponentLimitation :: Maybe Version -> String
explanationSingleComponentLimitation version =
  "The reason for this limitation is that your version "
    ++ versionString
    ++ "of ghci does not "
    ++ "support loading multiple components as source. Load just one component "
    ++ "and when you make changes to a dependent component then quit and reload.\n"
    ++ prettyShow minMultipleHomeUnitsVersion
    ++ " is needed to support multiple component sessions."
  where
    versionString = case version of
      Nothing -> ""
      Just ver -> "(" ++ prettyShow ver ++ ") "

-- Lenses
lElaboratedShared :: Lens' ProjectBuildContext ElaboratedSharedConfig
lElaboratedShared f s = fmap (\x -> s{elaboratedShared = x}) (f (elaboratedShared s))
{-# INLINE lElaboratedShared #-}

lPkgConfigReplOptions :: Lens' ElaboratedSharedConfig ReplOptions
lPkgConfigReplOptions f s = fmap (\x -> s{pkgConfigReplOptions = x}) (f (pkgConfigReplOptions s))
{-# INLINE lPkgConfigReplOptions #-}

lReplOptionsFlags :: Lens' ReplOptions [String]
lReplOptionsFlags f s = fmap (\x -> s{replOptionsFlags = x}) (f (replOptionsFlags s))
{-# INLINE lReplOptionsFlags #-}

lProjectConfig :: Lens' ProjectBaseContext ProjectConfig
lProjectConfig f s = fmap (\x -> s{projectConfig = x}) (f (projectConfig s))
{-# INLINE lProjectConfig #-}

lProjectConfigShared :: Lens' ProjectConfig ProjectConfigShared
lProjectConfigShared f s = fmap (\x -> s{projectConfigShared = x}) (f (projectConfigShared s))
{-# INLINE lProjectConfigShared #-}

lProjectConfigConstraints :: Lens' ProjectConfigShared [(UserConstraint, ConstraintSource)]
lProjectConfigConstraints f s = fmap (\x -> s{projectConfigConstraints = x}) (f (projectConfigConstraints s))
{-# INLINE lProjectConfigConstraints #-}
