{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE NoImplicitPrelude #-}

module Main
  ( main,
  )
where

import CLI.Commands
  ( App (..),
    Command (..),
    GlobalOptions (..),
    parseCLI,
  )
import CLI.Config
  ( Config (..),
    Service (..),
    ServiceOptions (..),
    Source (..),
    readConfig,
  )
import CLI.File (getModuleNameByPath, processDocument, processFileName)
import CLI.Generator
  ( BuildConfig (..),
    processClientDocument,
    processServerDocument,
  )
import qualified Data.ByteString.Lazy as L
  ( readFile,
  )
import Data.Morpheus.Client (readSchemaSource)
import Data.Morpheus.Internal.Ext (resultOr)
import qualified Data.Text.IO as TIO
import Data.Version (showVersion)
import qualified Paths_morpheus_graphql_code_gen as CLI
import Relude hiding (ByteString)
import System.Exit (ExitCode (..))
import System.FilePath (normalise, (</>))
import System.FilePath.Glob (glob)

currentVersion :: String
currentVersion = showVersion CLI.version

main :: IO ()
main = parseCLI >>= runApp

runApp :: App -> IO ()
runApp App {..}
  | version options = putStrLn currentVersion
  | otherwise = runOperation operations
  where
    runOperation About = putStrLn $ "Morpheus GraphQL CLI, version " <> currentVersion
    runOperation (Build source) = processAll (scan . Context False) source
    runOperation (Check source) = processAll (scan . Context True) source

data Context = Context
  { isCheck :: Bool,
    configDir :: FilePath
  }

type CommandResult = Bool

processAll :: (Traversable t, MonadIO m) => (a1 -> m Bool) -> t a1 -> m b
processAll f xs = do
  res <- traverse f xs
  if and res
    then putStr "\x1b[32mOK\x1b[0m\n" >> exitSuccess
    else exitWith (ExitFailure 1)

scan :: Context -> IO CommandResult
scan ctx = do
  Config {server, client} <- readConfig (configDir ctx)
  servers <- traverse (handleServerService ctx) (concat $ maybeToList server)
  clients <- traverse (handleClientService ctx) (concat $ maybeToList client)
  pure $ and (servers <> clients)

getImports :: Maybe ServiceOptions -> [Text]
getImports = concatMap optionImports . maybeToList

expandSource :: FilePath -> Source -> IO [Source]
expandSource root (Source p o) = do
  files <- glob $ normalise (root </> p)
  pure $ map (`Source` o) files

parseServiceData :: Context -> Service -> IO (FilePath, [Source], ServiceOptions)
parseServiceData ctx Service {source, includes, options} = do
  let root = normalise (configDir ctx </> source)
  let namespaces = maybe False optionNamespace options
  let externals = maybe mempty optionExternals options
  files <- concat <$> traverse (expandSource root) includes
  pure
    ( root,
      files,
      ServiceOptions namespaces (getImports options) externals
    )

getSchemaPath :: FilePath -> String -> Maybe Source -> IO Source
getSchemaPath root _ (Just Source {..}) = do
  pure Source {sourcePath = normalise $ root </> sourcePath, ..}
getSchemaPath _ name _ = fail $ "client service " <> name <> " should provide schema!"

handleClientService :: Context -> Service -> IO CommandResult
handleClientService ctx s@Service {name, schema} = do
  (root, files, buildOptions) <- parseServiceData ctx s
  putStrLn ("\n build:" <> name)
  schemaPath <- getSchemaPath root name schema
  let config = BuildConfig {..}
  (imports, globals) <- buildClientGlobals ctx config schemaPath
  let newConfig = config {buildOptions = buildOptions {optionImports = imports <> optionImports buildOptions}}
  and . (globals :) <$> traverse (buildClientQuery ctx newConfig schemaPath) files

buildClientGlobals :: Context -> BuildConfig -> Source -> IO ([Text], CommandResult)
buildClientGlobals ctx config src@Source {sourcePath} = do
  putStr ("  - " <> sourcePath <> "\n")
  schemaDoc <- readSchemaSource sourcePath
  let hsPath = processFileName sourcePath
  let moduleName = getModuleNameByPath (root config) hsPath
  let result = processClientDocument (localConfig config src) schemaDoc Nothing moduleName
  res <- processDocument (isCheck ctx) hsPath result
  pure ([moduleName | resultOr (const False) isJust result], res)

buildClientQuery :: Context -> BuildConfig -> Source -> Source -> IO CommandResult
buildClientQuery ctx config schemaPath querySrc = do
  let queryPath = sourcePath querySrc
  putStr ("  - " <> queryPath <> "\n")
  file <- TIO.readFile queryPath
  schemaDoc <- readSchemaSource (sourcePath schemaPath)
  let hsPath = processFileName queryPath
  let moduleName = getModuleNameByPath (root config) hsPath
  let result = processClientDocument (localConfig config querySrc) schemaDoc (Just file) moduleName
  processDocument (isCheck ctx) hsPath result

handleServerService :: Context -> Service -> IO CommandResult
handleServerService ctx s@Service {name} = do
  (root, files, buildOptions) <- parseServiceData ctx s
  putStrLn ("\n build:" <> name)
  and <$> traverse (buildServer ctx BuildConfig {..}) files

buildServer :: Context -> BuildConfig -> Source -> IO CommandResult
buildServer ctx config src = do
  let path = sourcePath src
  let hsPath = processFileName path
  putStr ("  - " <> path <> "\n")
  file <- L.readFile path
  let moduleName = getModuleNameByPath (root config) hsPath
  let result = processServerDocument (localConfig config src) moduleName file
  processDocument (isCheck ctx) hsPath (Just <$> result)

localConfig :: BuildConfig -> Source -> BuildConfig
localConfig (BuildConfig root ops) src =
  let options = sconcat (ops :| maybeToList (sourceOptions src))
   in BuildConfig root options
