module Hadolint.Rule.DL3009 (rule) where

import qualified Data.Map.Strict as Map
import qualified Data.Text as Text
import Hadolint.Rule
import qualified Hadolint.Shell as Shell
import Language.Docker.Syntax

data Acc
  = Acc
      { lastFrom :: BaseImage,
        stages :: Map.Map Text.Text Linenumber,
        forgets :: Map.Map Linenumber BaseImage
      }
  | Empty
  deriving (Show)

rule :: Rule Shell.ParsedShell
rule = veryCustomRule check (emptyState Empty) markFailures
  where
    code = "DL3009"
    severity = DLInfoC
    message = "Delete the apt-get lists after installing something"

    check line st (From from) = st |> modify (rememberStage line from)
    check line st (Run (RunArgs args flags))
      | hasNoCacheMount flags && foldArguments forgotToCleanup args =
          st |> modify (rememberLine line)
      | otherwise = st
    check _ st _ = st

    -- Convert the final state into failures
    markFailures (State fails Empty) = fails
    markFailures (State _ Acc {..}) = forgets |> Map.foldMapWithKey mapFail
      where
        mapFail line from
          | from == lastFrom = pure CheckFailure {..}
          | BaseImage {alias = Just (ImageAlias als)} <- from, -- Check if this stage is used later
            Just _ <- Map.lookup als stages =
            -- If the same alias is used in another stage, fail
            pure CheckFailure {..}
          | otherwise = mempty
{-# INLINEABLE rule #-}

rememberStage :: Linenumber -> BaseImage -> Acc -> Acc
rememberStage line from@BaseImage {image = Image _ als} (Acc _ stages o) = Acc from (Map.insert als line stages) o
rememberStage line from@BaseImage {image = Image _ als} Empty = Acc from (Map.singleton als line) Map.empty

rememberLine :: Linenumber -> Acc -> Acc
rememberLine line (Acc from stages o) = Acc from stages (Map.insert line from o)
rememberLine line Empty = Acc emptyImage mempty (Map.singleton line emptyImage)

forgotToCleanup :: Shell.ParsedShell -> Bool
forgotToCleanup args
  | hasUpdate && not hasCleanup = True
  | otherwise = False
  where
    hasCleanup =
      any (Shell.cmdHasArgs "rm" ["-rf", "/var/lib/apt/lists/*"]) (Shell.presentCommands args)

    hasUpdate = any (Shell.cmdHasArgs "apt-get" ["update"]) (Shell.presentCommands args)

hasNoCacheMount :: RunFlags -> Bool
hasNoCacheMount RunFlags
  { mount =
      Just (CacheMount CacheOpts {cTarget = TargetPath {unTargetPath = p}})
  } = Text.dropWhileEnd (=='/') p /= "/var/lib/apt/lists"
hasNoCacheMount RunFlags {} = True

-- | Even though dockerfiles without a FROM are not valid, we still want to provide some feedback for this rule
-- so we pretend there is a base image at the start of the file if there is none
emptyImage :: BaseImage
emptyImage = (BaseImage {image = "scratch", tag = Nothing, digest = Nothing, alias = Nothing, platform = Nothing})
