{-# LANGUAGE OverloadedStrings #-}

import Control.Monad.Extra
import Data.Either
import Data.Ini.Config
import Data.List.Extra
import Data.Maybe
import Data.Tuple (swap)
import Text.Read (readMaybe)
import qualified Data.Text.IO as T
import SimpleCmd
import SimpleCmdArgs
import System.Directory
import System.Exit
import System.FilePath
import System.IO
import System.Process

import Paths_stack_all (version)

-- FIXME allow specific snapshots?
-- FIXME lts latest
data Snapshot = LTS Int | Nightly
  deriving (Eq, Ord)

-- readCompactSnap "lts16"
readCompactSnap :: String -> Maybe Snapshot
readCompactSnap "nightly" = Just Nightly
readCompactSnap snap =
  if "lts" `isPrefixOf` snap then
    case readMaybe (dropPrefix "lts" snap) of
      Just major -> Just (LTS major)
      Nothing -> error' $ "couldn't parse compact " ++ snap ++  " (expected ltsXX)"
  else Nothing

eitherReadSnap :: String -> Either String Snapshot
eitherReadSnap cs =
  case maybeReadSnap cs of
    Just s -> Right s
    _ -> Left cs

maybeReadSnap :: String -> Maybe Snapshot
maybeReadSnap "nightly" = Just Nightly
maybeReadSnap snap =
  if "lts" `isPrefixOf` snap then
    case readMaybe (dropPrefix "lts-" snap) <|> readMaybe (dropPrefix "lts" snap) of
      Just major -> Just (LTS major)
      Nothing -> Nothing
  else Nothing

-- readSnap "lts-16"
readSnap :: String -> Snapshot
readSnap "nightly" = Nightly
readSnap snap =
  case maybeReadSnap snap of
    Just s -> s
    Nothing ->
      error' $ "couldn't parse " ++ snap ++ " (expected lts-XX or ltsXX)"

showSnap :: Snapshot -> String
showSnap Nightly = "nightly"
showSnap (LTS ver) = "lts-" ++ show ver

defaultOldest :: Snapshot
defaultOldest = LTS 11

allSnaps :: [Snapshot]
allSnaps = [Nightly, LTS 17, LTS 16, LTS 14, LTS 13, LTS 12, LTS 11,
            LTS 10, LTS 9, LTS 8, LTS 6, LTS 5, LTS 4, LTS 2, LTS 1]

data VersionLimit = DefaultLimit | Oldest Snapshot | AllVersions

main :: IO ()
main = do
  hSetBuffering stdout NoBuffering
  simpleCmdArgs' (Just version) "Build over Stackage versions"
    "stack-all builds projects easily across different Stackage versions" $
    run <$>
    switchWith 'c' "create-config" "Create a project .stack-all file" <*>
    switchWith 'd' "debug" "Verbose stack build output on error" <*>
    optional (readSnap <$> strOptionWith 'n' "newest" "lts-MAJOR" "Newest LTS release to build from") <*>
    (Oldest . readSnap <$> strOptionWith 'o' "oldest" "lts-MAJOR" "Oldest compatible LTS release" <|>
     flagWith DefaultLimit AllVersions 'a' "all-lts" "Try to build back to LTS 1 even") <*>
    many (strArg "SNAPSHOT... [COMMAND...]")

run :: Bool -> Bool -> Maybe Snapshot -> VersionLimit -> [String] -> IO ()
run createconfig debug mnewest verlimit verscmd = do
  haveSYL <- doesFileExist "stack.yaml"
  if not haveSYL
    then do
    cwdir <- getCurrentDirectory
    if cwdir == "/"
      then error' "No stack project found"
      else setCurrentDirectory ".." >>
           run createconfig debug mnewest verlimit verscmd
    else
    if createconfig then
      case verlimit of
        Oldest oldest -> createStackAll oldest
        _ -> error' "creating .stack-all requires --oldest LTS"
      else do
      (versions, cs) <- getVersionsCmd
      configs <- mapMaybe readStackConf <$> listDirectory "."
      let newestFilter = maybe id (filter . (>=)) mnewest
      mapM_ (stackBuild configs debug cs) (newestFilter versions)
  where
    readStackConf :: FilePath -> Maybe Snapshot
    readStackConf "stack-lts.yaml" = error' "unversioned stack-lts.yaml is unsupported"
    readStackConf f =
      stripPrefix "stack-" f >>= stripSuffix ".yaml" >>= readCompactSnap

    getVersionsCmd :: IO ([Snapshot],[String])
    getVersionsCmd = do
      let partitionSnaps = swap . partitionEithers . map eitherReadSnap
          (verlist,cmds) = partitionSnaps verscmd
      versions <-
        if null verlist then
          case verlimit of
            DefaultLimit -> do
              oldest <- fromMaybeM (return defaultOldest) readOldestLTS
              return $ case mnewest of
                         Just newest ->
                           if newest < oldest
                           then filter (newest >=) allSnaps
                           else filter (\ s ->  s >= oldest && newest >= s) allSnaps
                         Nothing -> filter (>= oldest) allSnaps
            AllVersions -> return allSnaps
            Oldest ver -> return $ filter (>= ver) allSnaps
        else return verlist
      return (versions,if null cmds then ["build"] else cmds)

stackAllFile :: FilePath
stackAllFile = ".stack-all"

createStackAll :: Snapshot -> IO ()
createStackAll snap = do
  exists <- doesFileExist stackAllFile
  if exists then error' $ stackAllFile ++ " already exists"
    else do
    let older =
          let molder = listToMaybe $ dropWhile (>= snap) allSnaps
          in maybe "" (\s -> showSnap s ++ " too old") molder
    writeFile stackAllFile $
      "[versions]\n# " ++ older ++ "\noldest = " ++ showSnap snap ++ "\n"

readOldestLTS :: IO (Maybe Snapshot)
readOldestLTS = do
  haveConfig <- doesFileExist stackAllFile
  if haveConfig then
    Just . readSnap <$> readIniConfig stackAllFile rcParser id
    else return Nothing
  where
    rcParser :: IniParser String
    rcParser =
      section "versions" $
      fieldOf "oldest" string

    readIniConfig :: FilePath -> IniParser a -> (a -> b) -> IO b
    readIniConfig inifile iniparser fn = do
      ini <- T.readFile inifile
      return $ either error fn $ parseIniFile ini iniparser

stackBuild :: [Snapshot] -> Bool -> [String] -> Snapshot -> IO ()
stackBuild configs debug command snap = do
  let config =
        case sort (filter (snap <=) configs) of
          [] -> []
          (cfg:_) -> ["--stack-yaml", showConfig cfg]
      opts = ["-v" | debug] ++ ["--resolver", showSnap snap] ++
             config
  if debug
    then debugBuild $ opts ++ command
    else do
    ok <- cmdBool "stack" $ opts ++ command
    unless ok $ do
      putStr "\nsnapshot-pkg-db: "
      cmd_ "stack" $ "--silent" : opts ++ ["path", "--snapshot-pkg-db"]
      error' $ "failed for " ++ showSnap snap
  putStrLn ""
  where
    showConfig :: Snapshot -> FilePath
    showConfig sn = "stack-" ++ compactSnap sn <.> "yaml"
      where
        compactSnap :: Snapshot -> String
        compactSnap Nightly = "nightly"
        compactSnap (LTS ver) = "lts" ++ show ver

    debugBuild :: [String] -> IO ()
    debugBuild args = do
      putStr $ "stack " ++ unwords args
      (ret,out,err) <- readProcessWithExitCode "stack" args ""
      putStrLn "\n"
      unless (null out) $ putStrLn out
      unless (ret == ExitSuccess) $ do
        -- stack verbose includes info line with all stackages (> 500kbytes)
        mapM_ putStrLn $ filter ((<10000) . length) . lines $ err
        error' $ showSnap snap ++ " build failed"