-- TODO: Consider intersecting version ranges for multiple deps on a
-- package.  This is why VersionRange is in the parent map.

pprintExceptions
    :: [ConstructPlanException]
    -> Path Abs File
    -> ParentMap
    -> Set PackageName
    -> AnsiDoc
pprintExceptions exceptions stackYaml parentMap wanted =
    mconcat $
      [ flow "While constructing the build plan, the following exceptions were encountered:"
      , line <> line
      , mconcat (intersperse (line <> line) (mapMaybe pprintException exceptions'))
      , line <> line
      , flow "Some potential ways to resolve this:"
      , line <> line
      ] ++
      (if Map.null extras then [] else
         [ "  *" <+> align
           (flow "Recommended action: try adding the following to your extra-deps in" <+>
            toAnsiDoc (display stackYaml) <> ":")
         , line <> line
         , vsep (map pprintExtra (Map.toList extras))
         , line <> line
         ]
      ) ++
      [ "  *" <+> align (flow "Set 'allow-newer: true' to ignore all version constraints and build anyway.")
      , line <> line
      , "  *" <+> align (flow "You may also want to try using the 'stack solver' command.")
      , line
      ]
  where
    exceptions' = nubOrd exceptions

    extras = Map.unions $ map getExtras exceptions'
    getExtras (DependencyCycleDetected _) = Map.empty
    getExtras (UnknownPackage _) = Map.empty
    getExtras (DependencyPlanFailures _ m) =
       Map.unions $ map go $ Map.toList m
     where
       -- TODO: Likely a good idea to distinguish these to the user.  In particular, for DependencyMismatch
       go (name, (_range, Just version, NotInBuildPlan)) =
           Map.singleton name version
       go (name, (_range, Just version, DependencyMismatch{})) =
           Map.singleton name version
       go _ = Map.empty
    pprintExtra (name, version) =
      fromString (concat ["- ", packageNameString name, "-", versionString version])

    allNotInBuildPlan = Set.fromList $ concatMap toNotInBuildPlan exceptions'
    toNotInBuildPlan (DependencyPlanFailures _ pDeps) =
      map fst $ filter (\(_, (_, _, badDep)) -> badDep == NotInBuildPlan) $ Map.toList pDeps
    toNotInBuildPlan _ = []

    pprintException (DependencyCycleDetected pNames) = Just $
        flow "Dependency cycle detected in packages:" <> line <>
        indent 4 (encloseSep "[" "]" "," (map (styleError . display) pNames))
    pprintException (DependencyPlanFailures pkg pDeps) =
        case mapMaybe pprintDep (Map.toList pDeps) of
            [] -> Nothing
            depErrors -> Just $
                flow "In the dependencies for" <+> pkgIdent <>
                pprintFlags (packageFlags pkg) <> ":" <> line <>
                indent 4 (vsep depErrors) <>
                case getShortestDepsPath parentMap wanted (packageName pkg) of
                    Nothing -> line <> flow "needed for unknown reason - stack invariant violated."
                    Just [] -> line <> flow "needed since" <+> pkgName <+> flow "is a build target."
                    Just (target:path) -> line <> flow "needed due to " <> encloseSep "" "" " -> " pathElems
                      where
                        pathElems =
                            [styleTarget . display $ target] ++
                            map display path ++
                            [pkgIdent]
              where
                pkgName = styleCurrent . display $ packageName pkg
                pkgIdent = styleCurrent . display $ packageIdentifier pkg
    -- Skip these when they are redundant with 'NotInBuildPlan' info.
    pprintException (UnknownPackage name)
        | name `Set.member` allNotInBuildPlan = Nothing
        | name `HashSet.member` wiredInPackages =
            Just $ flow "Can't build a package with same name as a wired-in-package:" <+> (styleCurrent . display $ name)
        | otherwise = Just $ flow "Unknown package:" <+> (styleCurrent . display $ name)

    pprintFlags flags
        | Map.null flags = ""
        | otherwise = parens $ sep $ map pprintFlag $ Map.toList flags
    pprintFlag (name, True) = "+" <> fromString (show name)
    pprintFlag (name, False) = "-" <> fromString (show name)

    pprintDep (name, (range, mlatestApplicable, badDep)) = case badDep of
        NotInBuildPlan -> Just $
            styleError (display name) <+>
            align (flow "must match" <+> goodRange <> "," <> softline <>
                   flow "but the stack configuration has no specified version" <>
                   latestApplicable Nothing)
        -- TODO: For local packages, suggest editing constraints
        DependencyMismatch version -> Just $
            (styleError . display) (PackageIdentifier name version) <+>
            align (flow "from stack configuration does not match" <+> goodRange <>
                   latestApplicable (Just version))
        -- I think the main useful info is these explain why missing
        -- packages are needed. Instead lets give the user the shortest
        -- path from a target to the package.
        Couldn'tResolveItsDependencies _version -> Nothing
      where
        goodRange = styleGood (fromString (Cabal.display range))
        latestApplicable mversion =
            case mlatestApplicable of
                Nothing -> ""
                Just la
                    | mlatestApplicable == mversion -> softline <>
                        flow "(latest matching version is specified)"
                    | otherwise -> softline <>
                        flow "(latest matching version is" <+> styleGood (display la) <> ")"

-- | Get the shortest reason for the package to be in the build plan. In
-- other words, trace the parent dependencies back to a 'wanted'
-- package.
pprintExceptions
    :: [ConstructPlanException]
    -> Path Abs File
    -> ParentMap
    -> Set PackageName
    -> AnsiDoc
pprintExceptions exceptions stackYaml parentMap wanted =
    mconcat $
      [ flow "While constructing the build plan, the following exceptions were encountered:"
      , line <> line
      , mconcat (intersperse (line <> line) (mapMaybe pprintException exceptions'))
      , line <> line
      , flow "Some potential ways to resolve this:"
      , line <> line
      ] ++
      (if Map.null extras then [] else
         [ "  *" <+> align
           (flow "Recommended action: try adding the following to your extra-deps in" <+>
            toAnsiDoc (display stackYaml) <> ":")
         , line <> line
         , vsep (map pprintExtra (Map.toList extras))
         , line <> line
         ]
      ) ++
      [ "  *" <+> align (flow "Set 'allow-newer: true' to ignore all version constraints and build anyway.")
      , line <> line
      , "  *" <+> align (flow "You may also want to try using the 'stack solver' command.")
      , line
      ]
  where
    exceptions' = nubOrd exceptions

    extras = Map.unions $ map getExtras exceptions'
    getExtras (DependencyCycleDetected _) = Map.empty
    getExtras (UnknownPackage _) = Map.empty
    getExtras (DependencyPlanFailures _ m) =
       Map.unions $ map go $ Map.toList m
     where
       -- TODO: Likely a good idea to distinguish these to the user.  In particular, for DependencyMismatch
       go (name, (_range, Just version, NotInBuildPlan)) =
           Map.singleton name version
       go (name, (_range, Just version, DependencyMismatch{})) =
           Map.singleton name version
       go _ = Map.empty
    pprintExtra (name, version) =
      fromString (concat ["- ", packageNameString name, "-", versionString version])

    allNotInBuildPlan = Set.fromList $ concatMap toNotInBuildPlan exceptions'
    toNotInBuildPlan (DependencyPlanFailures _ pDeps) =
      map fst $ filter (\(_, (_, _, badDep)) -> badDep == NotInBuildPlan) $ Map.toList pDeps
    toNotInBuildPlan _ = []

    pprintException (DependencyCycleDetected pNames) = Just $
        flow "Dependency cycle detected in packages:" <> line <>
        indent 4 (encloseSep "[" "]" "," (map (styleError . display) pNames))
    pprintException (DependencyPlanFailures pkg pDeps) =
        case mapMaybe pprintDep (Map.toList pDeps) of
            [] -> Nothing
            depErrors -> Just $
                flow "In the dependencies for" <+> pkgIdent <>
                pprintFlags (packageFlags pkg) <> ":" <> line <>
                indent 4 (vsep depErrors) <>
                case getShortestDepsPath parentMap wanted (packageName pkg) of
                    Nothing -> line <> flow "needed for unknown reason - stack invariant violated."
                    Just [] -> line <> flow "needed since" <+> pkgName <+> flow "is a build target."
                    Just (target:path) -> line <> flow "needed due to " <> encloseSep "" "" " -> " pathElems
                      where
                        pathElems =
                            [styleTarget . display $ target] ++
                            map display path ++
                            [pkgIdent]
              where
                pkgName = styleCurrent . display $ packageName pkg
                pkgIdent = styleCurrent . display $ packageIdentifier pkg
    -- Skip these when they are redundant with 'NotInBuildPlan' info.
    pprintException (UnknownPackage name)
        | name `Set.member` allNotInBuildPlan = Nothing
        | name `HashSet.member` wiredInPackages =
            Just $ flow "Can't build a package with same name as a wired-in-package:" <+> (styleCurrent . display $ name)
        | otherwise = Just $ flow "Unknown package:" <+> (styleCurrent . display $ name)

    pprintFlags flags
        | Map.null flags = ""
        | otherwise = parens $ sep $ map pprintFlag $ Map.toList flags
    pprintFlag (name, True) = "+" <> fromString (show name)
    pprintFlag (name, False) = "-" <> fromString (show name)

    pprintDep (name, (range, mlatestApplicable, badDep)) = case badDep of
        NotInBuildPlan -> Just $
            styleError (display name) <+>
            align (flow "must match" <+> goodRange <> "," <> softline <>
                   flow "but the stack configuration has no specified version" <>
                   latestApplicable Nothing)
        -- TODO: For local packages, suggest editing constraints
        DependencyMismatch version -> Just $
            (styleError . display) (PackageIdentifier name version) <+>
            align (flow "from stack configuration does not match" <+> goodRange <>
                   latestApplicable (Just version))
        -- I think the main useful info is these explain why missing
        -- packages are needed. Instead lets give the user the shortest
        -- path from a target to the package.
        Couldn'tResolveItsDependencies _version -> Nothing
      where
        goodRange = styleGood (fromString (Cabal.display range))
        latestApplicable mversion =
            case mlatestApplicable of
                Nothing -> ""
                Just la
                    | mlatestApplicable == mversion -> softline <>
                        flow "(latest matching version is specified)"
                    | otherwise -> softline <>
                        flow "(latest matching version is" <+> styleGood (display la) <> ")"

-- | Get the shortest reason for the package to be in the build plan. In
-- other words, trace the parent dependencies back to a 'wanted'
-- package.
pprintExceptions exceptions stackYaml parentMap wanted =
    mconcat $
      [ flow "While constructing the build plan, the following exceptions were encountered:"
      , line <> line
      , mconcat (intersperse (line <> line) (mapMaybe pprintException exceptions'))
      , line <> line
      , flow "Some potential ways to resolve this:"
      , line <> line
      ] ++
      (if Map.null extras then [] else
         [ "  *" <+> align
           (flow "Recommended action: try adding the following to your extra-deps in" <+>
            toAnsiDoc (display stackYaml) <> ":")
         , line <> line
         , vsep (map pprintExtra (Map.toList extras))
         , line <> line
         ]
      ) ++
      [ "  *" <+> align (flow "Set 'allow-newer: true' to ignore all version constraints and build anyway.")
      , line <> line
      , "  *" <+> align (flow "You may also want to try using the 'stack solver' command.")
      , line
      ]
  where
    exceptions' = nubOrd exceptions

    extras = Map.unions $ map getExtras exceptions'
    getExtras (DependencyCycleDetected _) = Map.empty
    getExtras (UnknownPackage _) = Map.empty
    getExtras (DependencyPlanFailures _ m) =
       Map.unions $ map go $ Map.toList m
     where
       -- TODO: Likely a good idea to distinguish these to the user.  In particular, for DependencyMismatch
       go (name, (_range, Just version, NotInBuildPlan)) =
           Map.singleton name version
       go (name, (_range, Just version, DependencyMismatch{})) =
           Map.singleton name version
       go _ = Map.empty
    pprintExtra (name, version) =
      fromString (concat ["- ", packageNameString name, "-", versionString version])

    allNotInBuildPlan = Set.fromList $ concatMap toNotInBuildPlan exceptions'
    toNotInBuildPlan (DependencyPlanFailures _ pDeps) =
      map fst $ filter (\(_, (_, _, badDep)) -> badDep == NotInBuildPlan) $ Map.toList pDeps
    toNotInBuildPlan _ = []

    pprintException (DependencyCycleDetected pNames) = Just $
        flow "Dependency cycle detected in packages:" <> line <>
        indent 4 (encloseSep "[" "]" "," (map (styleError . display) pNames))
    pprintException (DependencyPlanFailures pkg pDeps) =
        case mapMaybe pprintDep (Map.toList pDeps) of
            [] -> Nothing
            depErrors -> Just $
                flow "In the dependencies for" <+> pkgIdent <>
                pprintFlags (packageFlags pkg) <> ":" <> line <>
                indent 4 (vsep depErrors) <>
                case getShortestDepsPath parentMap wanted (packageName pkg) of
                    Nothing -> line <> flow "needed for unknown reason - stack invariant violated."
                    Just [] -> line <> flow "needed since" <+> pkgName <+> flow "is a build target."
                    Just (target:path) -> line <> flow "needed due to " <> encloseSep "" "" " -> " pathElems
                      where
                        pathElems =
                            [styleTarget . display $ target] ++
                            map display path ++
                            [pkgIdent]
              where
                pkgName = styleCurrent . display $ packageName pkg
                pkgIdent = styleCurrent . display $ packageIdentifier pkg
    -- Skip these when they are redundant with 'NotInBuildPlan' info.
    pprintException (UnknownPackage name)
        | name `Set.member` allNotInBuildPlan = Nothing
        | name `HashSet.member` wiredInPackages =
            Just $ flow "Can't build a package with same name as a wired-in-package:" <+> (styleCurrent . display $ name)
        | otherwise = Just $ flow "Unknown package:" <+> (styleCurrent . display $ name)

    pprintFlags flags
        | Map.null flags = ""
        | otherwise = parens $ sep $ map pprintFlag $ Map.toList flags
    pprintFlag (name, True) = "+" <> fromString (show name)
    pprintFlag (name, False) = "-" <> fromString (show name)

    pprintDep (name, (range, mlatestApplicable, badDep)) = case badDep of
        NotInBuildPlan -> Just $
            styleError (display name) <+>
            align (flow "must match" <+> goodRange <> "," <> softline <>
                   flow "but the stack configuration has no specified version" <>
                   latestApplicable Nothing)
        -- TODO: For local packages, suggest editing constraints
        DependencyMismatch version -> Just $
            (styleError . display) (PackageIdentifier name version) <+>
            align (flow "from stack configuration does not match" <+> goodRange <>
                   latestApplicable (Just version))
        -- I think the main useful info is these explain why missing
        -- packages are needed. Instead lets give the user the shortest
        -- path from a target to the package.
        Couldn'tResolveItsDependencies _version -> Nothing
      where
        goodRange = styleGood (fromString (Cabal.display range))
        latestApplicable mversion =
            case mlatestApplicable of
                Nothing -> ""
                Just la
                    | mlatestApplicable == mversion -> softline <>
                        flow "(latest matching version is specified)"
                    | otherwise -> softline <>
                        flow "(latest matching version is" <+> styleGood (display la) <> ")"

-- | Get the shortest reason for the package to be in the build plan. In
-- other words, trace the parent dependencies back to a 'wanted'
-- package.