{-# LANGUAGE DataKinds           #-}
{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications    #-}

module App.Commands.SyncToArchive
  ( cmdSyncToArchive
  ) where

import Antiope.Core                         (toText)
import Antiope.Env                          (LogLevel, mkEnv)
import App.Commands.Options.Parser          (optsSyncToArchive)
import App.Static                           (homeDirectory)
import Control.Lens                         hiding ((<.>))
import Control.Monad                        (unless, when)
import Control.Monad.Except
import Control.Monad.Trans.Resource         (runResourceT)
import Data.Generics.Product.Any            (the)
import Data.List                            (isSuffixOf)
import Data.Semigroup                       ((<>))
import HaskellWorks.Ci.Assist.Core          (PackageInfo (..), Presence (..), Tagged (..), getPackages, loadPlan, relativePaths, relativePaths2)
import HaskellWorks.Ci.Assist.Location      ((<.>), (</>))
import HaskellWorks.Ci.Assist.PackageConfig (templateConfig)
import HaskellWorks.Ci.Assist.Show
import HaskellWorks.Ci.Assist.Tar           (updateEntryWith)
import Options.Applicative                  hiding (columns)
import System.Directory                     (createDirectoryIfMissing, doesDirectoryExist)

import qualified App.Commands.Options.Types        as Z
import qualified Codec.Archive.Tar                 as F
import qualified Codec.Compression.GZip            as F
import qualified Data.ByteString.Lazy              as LBS
import qualified Data.Text                         as T
import qualified HaskellWorks.Ci.Assist.GhcPkg     as GhcPkg
import qualified HaskellWorks.Ci.Assist.IO.Console as CIO
import qualified HaskellWorks.Ci.Assist.IO.Error   as IO
import qualified HaskellWorks.Ci.Assist.IO.File    as IO
import qualified HaskellWorks.Ci.Assist.IO.Lazy    as IO
import qualified HaskellWorks.Ci.Assist.IO.Tar     as IO
import qualified HaskellWorks.Ci.Assist.Types      as Z
import qualified System.Directory                  as IO
import qualified System.IO                         as IO
import qualified System.IO.Temp                    as IO
import qualified UnliftIO.Async                    as IO

{-# ANN module ("HLint: ignore Reduce duplication"  :: String) #-}
{-# ANN module ("HLint: ignore Redundant do"        :: String) #-}

runSyncToArchive :: Z.SyncToArchiveOptions -> IO ()
runSyncToArchive opts = do
  let storePath   = opts ^. the @"storePath"
  let archiveUri  = opts ^. the @"archiveUri"
  let threads     = opts ^. the @"threads"

  CIO.putStrLn $ "Store path: "   <> toText storePath
  CIO.putStrLn $ "Archive URI: "  <> toText archiveUri
  CIO.putStrLn $ "Threads: "      <> tshow threads

  mbPlan <- loadPlan
  case mbPlan of
    Right planJson -> do
      let compilerId = planJson ^. the @"compilerId"
      envAws <- mkEnv (opts ^. the @"region") (\_ _ -> pure ())
      let archivePath = archiveUri </> compilerId
      IO.createLocalDirectoryIfMissing archivePath
      let baseDir = opts ^. the @"storePath"
      CIO.putStrLn "Extracting package list"

      packages <- getPackages baseDir planJson

      let storeCompilerPath           = baseDir </> T.unpack compilerId
      let storeCompilerPackageDbPath  = storeCompilerPath </> "package.db"

      storeCompilerPackageDbPathExists <- doesDirectoryExist storeCompilerPackageDbPath

      unless storeCompilerPackageDbPathExists $
        GhcPkg.init storeCompilerPackageDbPath

      CIO.putStrLn $ "Syncing " <> tshow (length packages) <> " packages"

      IO.withSystemTempDirectory "cabal-cache" $ \tempPath -> do
        CIO.putStrLn $ "Temp path: " <> tshow tempPath

        CIO.putStrLn "Copying package.db directory for transformation"
        let workingStoreCompilerPath = tempPath </> T.unpack compilerId
        let workingStoreCompilerPackageDbPath = tempPath </> T.unpack compilerId </> "package.db"

        runExceptT $ IO.exceptFatal "Fatal error" $ do
          liftIO $ IO.createDirectoryIfMissing True workingStoreCompilerPackageDbPath

        packageDbFiles <- IO.listDirectory storeCompilerPackageDbPath
        let confFiles = filter (isSuffixOf ".conf") packageDbFiles

        forM_ confFiles $ \confFile -> do
          stream <- LBS.readFile (storeCompilerPackageDbPath </> confFile)
          LBS.writeFile (workingStoreCompilerPackageDbPath </> confFile) (templateConfig baseDir stream)

        IO.pooledForConcurrentlyN_ (opts ^. the @"threads") packages $ \pInfo -> do
          let archiveFileBasename = packageDir pInfo <.> ".tar.gz"
          let archiveFile = archiveUri </> T.pack archiveFileBasename
          let packageStorePath = baseDir </> packageDir pInfo
          archiveFileExists <- runResourceT $ IO.resourceExists envAws archiveFile

          unless archiveFileExists $ do
            packageStorePathExists <- doesDirectoryExist packageStorePath

            when packageStorePathExists $ void $ runExceptT $ IO.exceptWarn "Warning" $ do
              let rp2 = relativePaths2 storePath tempPath pInfo
              CIO.putStrLn $ "Creating " <> toText archiveFile

              let tempArchiveFile = tempPath </> archiveFileBasename

              IO.createTar tempArchiveFile rp2

              liftIO (LBS.readFile tempArchiveFile >>= IO.writeResource envAws archiveFile)

              return ()

    Left errorMessage -> do
      CIO.hPutStrLn IO.stderr $ "ERROR: Unable to parse plan.json file: " <> T.pack errorMessage

  return ()

cmdSyncToArchive :: Mod CommandFields (IO ())
cmdSyncToArchive = command "sync-to-archive"  $ flip info idm $ runSyncToArchive <$> optsSyncToArchive