{-# LANGUAGE TypeFamilies #-}

module Development.Iridium.Checks
  ( packageCheck
  , hlint
  , changelog
  , lowerBounds
  , upperBounds
  , remoteVersion
  , compile
  , documentation
  , compileVersions
  , upperBoundsStackage
  , packageSDist
  )
where



import qualified Data.Text            as Text
import qualified Data.Text.IO         as Text.IO
import qualified Turtle               as Turtle
import qualified Control.Foldl        as Foldl
import qualified Network.HTTP.Conduit as HTTP
import qualified Data.ByteString      as ByteString
import qualified Data.ByteString.Lazy as ByteStringL

import           Control.Exception.Lifted
import           Control.Monad
import           Control.Monad.IO.Class
import           Control.Monad.Trans.Class
import           Control.Monad.Trans.Control
import           Control.Monad.Trans.Maybe
import           Control.Monad.Trans.MultiRWS

import           Data.Text ( Text )
import           Data.Text.Encoding
import           Distribution.PackageDescription
import           Data.Maybe ( maybeToList )
import           Data.ByteString ( ByteString )
import qualified Data.ByteString as ByteString
import           Filesystem.Path.CurrentOS hiding ( null )

import qualified Distribution.Package
import           Distribution.Version

-- no way to retrieve stdout, stderr and exitcode with turtle.
-- the most generic case, not supported? psshhh.
import           System.Process hiding ( cwd )
import           Data.List ( nub )
import           Data.Version ( showVersion )

import           Development.Iridium.CheckState
import           Development.Iridium.Config
import           Development.Iridium.Types
import           Development.Iridium.UI.Console
import           Development.Iridium.UI.Prompt
import           Development.Iridium.Utils
import           Development.Iridium.ExternalProgWrappers




packageCheck
  :: ( MonadIO m
     , MonadMultiReader Config m
     , MonadMultiState LogState m
     , MonadMultiState CheckState m
     )
  => m ()
packageCheck = do
  buildtool <- configReadStringM ["setup", "buildtool"]
  case buildtool of
    "cabal" -> boolToError $ runCheck "Checking package validity" $ do
      mzeroToFalse $
        runCommandSuccessCabal ["check"]
    "stack" -> do
      -- stack has no "check".
      -- and no "upload --dry-run either."
      pushLog LogLevelWarn "stack has no `check` command!"
      pushLog LogLevelWarn "package validity could not be determined."
      return ()
    _ -> error "bad config setup.buildtool"

hlint
  :: ( MonadIO m
     , MonadMultiReader Config m
     , MonadMultiReader Infos m
     , MonadMultiState LogState m
     , MonadMultiState CheckState m
     )
  => m ()
hlint = boolToWarning
      $ runCheck "Running hlint on hsSourceDirs"
      $ do
  buildInfos <- askAllBuildInfo
  -- pushLog LogLevelDebug $ show buildInfos
  let sourceDirs = nub $ buildInfos >>= hsSourceDirs
  pushLog LogLevelInfoVerboser $ "hsSourceDirs: " ++ show sourceDirs
  liftM and $ sourceDirs `forM` \path -> do
    mzeroToFalse $
      runCommandSuccessHLint [path]

changelog
  :: ( MonadIO m
     , MonadMultiState LogState m
     , MonadMultiState CheckState m
     , MonadMultiReader Infos m
     , MonadMultiReader Config m
     )
  => m ()
changelog = boolToWarning
          $ runCheck "Testing if the changelog mentions the latest version"
          $ do
  pathRaw <- configReadStringM ["checks", "changelog", "location"]
  cwd <- liftM _i_cwd mAsk
  let path = cwd </> decodeString pathRaw
  exists <- Turtle.testfile path
  if (not exists)
    then do
      pushLog LogLevelPrint $ "changelog file (" ++ show path ++ ") does not exist!"
      return False
    else do
      changelogContentLines <- Turtle.fold (Turtle.input path) Foldl.list
      currentVersionStr <- liftM showVersion askPackageVersion
      if any (Text.pack currentVersionStr `Text.isInfixOf`)
             (Turtle.lineToText `fmap` changelogContentLines)
        then return True
        else do
          pushLog LogLevelError $ "changelog does not contain " ++ currentVersionStr
          return False

lowerBounds
  :: ( MonadIO m
     , MonadMultiState LogState m
     , MonadMultiState CheckState m
     , MonadMultiReader Infos m
     )
  => m ()
lowerBounds = boolToWarning
            $ runCheck "Checking that all dependencies have a lower bound"
            $ do
  buildInfos <- askAllBuildInfo
  pName <- askPackageName
  let missingBounds
        = [ name
          | info <- buildInfos
          , Distribution.Package.Dependency name range <- targetBuildDepends info
          , name /= pName -- ignore dependencies on the package's library
          , let intervals = asVersionIntervals range
          , let badLowerBound = LowerBound (Version [0] []) InclusiveBound
          , case intervals of
              []                                              -> True
              xs | any (\(lwr, _) -> lwr == badLowerBound) xs -> True
              _                                               -> False
          ]
  if null missingBounds
    then return True
    else do
      pushLog LogLevelError $ "Found dependencies without a lower bound:"
      missingBounds `forM_` \(Distribution.Package.PackageName n) ->
        pushLog LogLevelError $ "    " ++ n
      return False

upperBounds
  :: ( MonadIO m
     , MonadMultiState LogState m
     , MonadMultiState CheckState m
     , MonadMultiReader Infos m
     )
  => m ()
upperBounds = boolToWarning
            $ runCheck "Checking that all dependencies have an upper bound"
            $ do
  buildInfos <- askAllBuildInfo
  pName <- askPackageName
  let missingBounds
        = [ name
          | info <- buildInfos
          , Distribution.Package.Dependency name range <- targetBuildDepends info
          , name /= pName -- ignore dependencies on the package's library
          , let intervals = asVersionIntervals range
          , case intervals of
              []                                             -> True
              xs | any (\(_, upr) -> upr == NoUpperBound) xs -> True
              _                                              -> False
          ]
  if null missingBounds
    then return True
    else do
      pushLog LogLevelError $ "Found dependencies without an upper bound:"
      missingBounds `forM_` \(Distribution.Package.PackageName n) ->
        pushLog LogLevelError $ "    " ++ n
      return False

remoteVersion
  :: ( MonadIO m
     , MonadMultiState LogState m
     , MonadMultiState CheckState m
     , MonadMultiReader Infos m
     )
  => m ()
remoteVersion = boolToError
              $ runCheck "Comparing local version to hackage version"
              $ do
  infos <- mAsk
  localVersion <- askPackageVersion
  -- pushLog LogLevelDebug $ show $ _i_remote_version infos
  case _i_remote_version infos of
    Nothing -> do
      pushLog LogLevelWarn $ "no remote version detected. This is harmless if the package is not on hackage yet."
      return True
    Just remoteVers ->
      if localVersion == remoteVers
        then do
          pushLog LogLevelError $ "This package version (" ++ showVersion localVersion ++ ") is already on hackage; needs bump?"
          return False
        else if localVersion < remoteVers
          then do
            pushLog LogLevelWarn $ "The version on hackage ("
                                ++ showVersion remoteVers
                                ++ ") is greater than the local version ("
                                ++ showVersion localVersion
                                ++ ")."
            return False
          else return True

compile
  :: forall m
   . ( MonadIO m
     , MonadPlus m
     , MonadMultiState LogState m
     , MonadMultiState CheckState m
     , MonadMultiReader Config m
     )
  => m ()
compile = withStack "basic compilation" $ boolToError $ do

  warningsEnabled <- configIsEnabledM ["checks", "compiler-warnings"]
  if warningsEnabled
    then fallbackCheck
      (do
        b <- runCheck "Checking basic compilation" (checks True)
        unless b $ do
          incWarningCounter
          addNotWallClean "<default>"
        return b
      )
      (do
        pushLog LogLevelPrint "Falling back on compilation without warnings."
        runCheck "Checking basic compilation -w" (checks False)
      )
    else
      runCheck "Checking basic compilation" (checks False)

 where
  checks :: Bool -> m Bool
  checks werror = do
    buildtool <- configReadStringM ["setup", "buildtool"]
    testsEnabled <- configIsEnabledM ["checks", "testsuites"]
    case buildtool of
      "cabal" ->
        mzeroToFalse $ do
          let testsArg = ["--enable-tests" | testsEnabled]
          let werrorArg = ["--ghc-options=\"-Werror\"" | werror]
                       ++ ["--ghc-options=\"-w\"" | not werror]
          withDefaultCompiler <- createDefaultCompilerFlag
          runCommandSuccessCabal ["clean"]
          runCommandSuccessCabal $ ["install"] ++ withDefaultCompiler ++ ["--dep"] ++ testsArg
          runCommandSuccessCabal $ ["configure"] ++ withDefaultCompiler ++ testsArg ++ werrorArg
          runCommandSuccessCabal ["build"]
          when testsEnabled $
            runCommandSuccessCabal ["test"]
          return True
      "stack" -> do
        pushLog LogLevelError "TODO: stack build"
        mzero
      _ -> mzero

documentation
  :: ( MonadIO m
     , MonadMultiState LogState m
     , MonadMultiState CheckState m
     , MonadMultiReader Config m
     )
  => m ()
documentation = boolToError
              $ runCheck "Checking documentation"
              $ withStack "documentation check"
              $ do
  buildtool <- configReadStringM ["setup", "buildtool"]
  case buildtool of
    "cabal" ->
      mzeroToFalse $ do
        withDefaultCompiler <- createDefaultCompilerFlag
        runCommandSuccessCabal $ ["clean"]
        runCommandSuccessCabal $ ["install", "--dep"] ++ withDefaultCompiler
        runCommandSuccessCabal $ ["configure"] ++ withDefaultCompiler
        runCommandSuccessCabal $ ["haddock"]
    "stack" -> do
      pushLog LogLevelError "TODO: stack build"
      return False
    _ -> error "lkajsdlkjasd"

compileVersions
  :: forall m
   . ( MonadIO m
     , MonadPlus m
     , MonadMultiState LogState m
     , MonadMultiState CheckState m
     , MonadMultiReader Config m
     )
  => m ()
compileVersions = withStack "compiler checks" $ do

  buildtool       <- configReadStringM ["setup", "buildtool"]
  testsEnabled    <- configIsEnabledM ["checks", "testsuites"]
  warningsEnabled <- configIsEnabledM ["checks", "compiler-warnings"]

  case () of {
    () -> do
      if testsEnabled
        then pushLog LogLevelPrint "Checking compilation and tests with different compiler versions"
        else pushLog LogLevelPrint "Checking compilation with different compiler versions"
      withIndentation $ do
        rawList <- configReadListM ["checks", "compiler-versions", "compilers"]
        rawList `forM_` \val -> boolToError $ do
          let compilerStr = configReadString ["compiler"] val
                         ++ "-"
                         ++ configReadString ["version"] val
          let checkBaseName = "Checking with compiler " ++ compilerStr
          withStack compilerStr $ if warningsEnabled
            then
              fallbackCheck
                (do
                  b <- runCheck checkBaseName $ checks compilerStr True
                  unless b $ do
                    incWarningCounter
                    addNotWallClean compilerStr
                  return b
                )
                (do
                  pushLog LogLevelPrint "Falling back on compilation without warnings."
                  runCheck (checkBaseName ++ " -w") $ checks compilerStr False
                )
            else
              runCheck checkBaseName $ checks compilerStr False

    where
      checks :: String -> Bool -> m Bool
      checks compilerStr werror = case buildtool of
        "cabal" -> do
          let confList = ["setup", "compiler-paths", compilerStr]
          compilerPathMaybe <- configReadStringMaybeM confList
          compilerPath <- case compilerPathMaybe of
            Nothing -> do
              pushLog LogLevelError $ "Expected string in config for " ++ show confList
              mzero
            Just x -> return x
          mzeroToFalse $ do
            let testsArg = ["--enable-tests" | testsEnabled]
            let werrorArg = ["--ghc-options=\"-Werror\"" | werror]
                         ++ ["--ghc-options=\"-w\"" | not werror]
            runCommandSuccessCabal ["clean"]
            runCommandSuccessCabal $ ["install", "--dep", "-w" ++ compilerPath]
                                     ++ testsArg
            runCommandSuccessCabal $ ["configure", "-w" ++ compilerPath]
                                     ++ testsArg
                                     ++ werrorArg
            runCommandSuccessCabal ["build"]
            when testsEnabled $
              runCommandSuccessCabal ["test"]
        "stack" -> do
          pushLog LogLevelError "TODO: stack build"
          mzero
        _ -> mzero
  }

upperBoundsStackage
  :: forall m
   . ( MonadIO m
     , MonadPlus m
     , MonadMultiState LogState m
     , MonadMultiState CheckState m
     , MonadMultiReader Infos m
     , MonadMultiReader Config m
     )
  => m ()
upperBoundsStackage = withStack "stackage upper bound" $ boolToError $ do

  runCheck "Checking upper bounds using stackage" $ do
    buildtool    <- configReadStringM ["setup", "buildtool"]
    testsEnabled <- configIsEnabledM  ["checks", "testsuites"]
    case buildtool of
      "cabal" -> do
          cabalConfigPath       <- getLocalFilePath "cabal.config"
          cabalConfigBackupPath <- getLocalFilePath "cabal.config.backup"
          alreadyExists <- Turtle.testfile cabalConfigPath
          -- TODO: make this safe against ctrl-c again.
          -- TODO: make sure the backup does not exist yet. (!)
          pushLog LogLevelInfo $ "Preparing cabal.config"
          when alreadyExists $ do
            pushLog LogLevelInfoVerbose $ "Renaming existing cabal.config to cabal.config.backup"
            Turtle.mv cabalConfigPath cabalConfigBackupPath
          result <- mzeroToFalse $ do
            useNightly <- configIsTrueM ["checks", "upper-bounds-stackage", "use-nightly"]
            let urlStr = if useNightly
                  then "http://www.stackage.org/nightly/cabal.config"
                  else "http://www.stackage.org/lts/cabal.config"
            cabalConfigContents <- fetchCabalConfig urlStr
            pName <- liftM (Text.pack . (\(Distribution.Package.PackageName n) -> n)) askPackageName
            let filteredLines = filter (not . (pName `Text.isInfixOf`))
                              $ Text.lines
                              $ decodeUtf8 cabalConfigContents
            -- pushLog LogLevelDebug $ "Writing n lines to cabal.config: " ++ show (length filteredLines)
            liftIO $ Text.IO.writeFile (encodeString cabalConfigPath) (Text.unlines filteredLines)
            let testsArg = ["--enable-tests" | testsEnabled]
            runCommandSuccessCabal ["clean"]
            withDefaultCompiler <- createDefaultCompilerFlag
            runCommandSuccessCabal $ ["--no-require-sandbox", "--ignore-sandbox", "install", "--dep", "--global", "--dry-run"] ++ withDefaultCompiler ++ testsArg
          pushLog LogLevelInfo $ "Cleanup (cabal.config)"
          unless alreadyExists $ Turtle.rm cabalConfigPath
          when alreadyExists $ Turtle.mv cabalConfigBackupPath cabalConfigPath
          -- let
          --   act :: MaybeT m () = do
          --     pushLog LogLevelInfo $ "Preparing cabal.config"
          --     when alreadyExists $ do
          --       pushLog LogLevelInfoVerbose $ "Renaming existing cabal.config to cabal.config.backup"
          --       Turtle.mv cabalConfigPath cabalConfigBackupPath
          --     useNightly <- configIsTrueM ["checks", "upper-bounds-stackage", "use-nightly"]
          --     let urlStr = if useNightly
          --           then "http://www.stackage.org/nightly/cabal.config"
          --           else "http://www.stackage.org/lts/cabal.config"
          --     cabalConfigContents <- fetchCabalConfig urlStr
          --     pName <- liftM (Text.pack . (\(Distribution.Package.PackageName n) -> n)) askPackageName
          --     let filteredLines = filter (pName `Text.isInfixOf`)
          --                       $ Text.lines
          --                       $ decodeUtf8 cabalConfigContents
          --     liftIO $ Text.IO.writeFile (encodeString cabalConfigPath) (Text.unlines filteredLines)
          --     let testsArg = ["--enable-tests" | testsEnabled]
          --     runCommandSuccessCabal ["clean"]
          --     runCommandSuccessCabal $ ["install", "--dep"] ++ testsArg
          --   fin = do
          --     pushLog LogLevelInfo $ "Cleanup (cabal.config)"
          --     when alreadyExists $ Turtle.mv cabalConfigBackupPath cabalConfigPath
          -- act `finally` fin
          return result
      "stack" -> do
        pushLog LogLevelError "TODO: stack upper bound check"
        mzero
      _ -> mzero
 
 where
  fetchCabalConfig
    :: forall m0
     . ( MonadIO m0
       , MonadMultiState LogState m0
       )
    => String
    -> m0 ByteString
  fetchCabalConfig urlStr = do
    pushLog LogLevelInfoVerbose $ "Fetching up-to-date cabal.config from " ++ urlStr
    -- TODO: exception handling
    r <- HTTP.simpleHttp urlStr
    return $ ByteString.concat $ ByteStringL.toChunks $ r
    -- url <- case URI.parseURI urlStr of
    --   Nothing -> do
    --     pushLog LogLevelError "bad URI"
    --     mzero
    --   Just u -> return u
    -- result <- liftIO $ HTTP.simpleHTTP (HTTP.mkRequest HTTP.GET url)
    -- case result of
    --   Left _ -> do
    --     pushLog LogLevelError "Error: Could not retrieve hackage version"
    --     mzero
    --   Right x -> do
    --     pushLog LogLevelInfoVerboser $ show x
    --     let body = HTTP.rspBody x
    --     pushLog LogLevelInfoVerbose $ "Retrieved " ++ show (ByteString.length body) ++ " bytes."
    --     return $ body

packageSDist
  :: forall m
   . ( MonadIO m
     , MonadPlus m
     , MonadMultiState LogState m
     , MonadMultiState CheckState m
     , MonadMultiReader Infos m
     , MonadMultiReader Config m
     )
  => m ()
packageSDist = withStack "package sdist" $ boolToError $ do

  runCheck "Testing the source distribution package" $ do

    Distribution.Package.PackageName pName <- askPackageName
    currentVersionStr <- liftM showVersion askPackageVersion
      
    buildtool    <- configReadStringM ["setup", "buildtool"]
    case buildtool of
      "cabal" -> mzeroToFalse $ do
        runCommandSuccessCabal ["sdist"]
        let sdistName = pName ++ "-" ++ currentVersionStr ++ ".tar.gz"
        withDefaultCompiler <- createDefaultCompilerFlag
        runCommandSuccessCabal $ ["install", "dist/" ++ sdistName] ++ withDefaultCompiler
      "stack" -> do
        pushLog LogLevelError "TODO: stack upper bound check"
        mzero
      _ -> mzero