module Database.Trek.Run where
import qualified Database.Trek.Db as Db
import qualified Database.PostgreSQL.Simple.Options as P
import qualified Database.PostgreSQL.Simple.PartialOptions as Partial
import Data.Time.Format
import Data.Time
import System.IO
import Database.Trek.Parser
import System.FilePath.Posix
import Data.String
import qualified Data.ByteString.Lazy.Char8 as BSL
import qualified Data.ByteString.Char8 as BSC
import Crypto.Hash.SHA256
import Control.Exception
import Data.Typeable
import Database.PostgreSQL.Simple
import Data.Aeson
import System.Exit
import System.IO.Error
import Control.Monad
import Data.List.NonEmpty (nonEmpty)
import System.Directory
import Data.Foldable
import qualified Database.PostgreSQL.Transact as T
import Database.PostgreSQL.Simple.Transaction
import qualified Data.ByteString.Base64 as Base64
import qualified Database.PostgreSQL.Simple as Psql
import Data.List.Split
import Data.Maybe
import Data.Aeson.Encode.Pretty

data Errors = CouldNotParseMigration FilePath
            | DirectoryDoesNotExist FilePath
  deriving (Show, Eq, Typeable)

instance Exception Errors

eval :: Command -> IO ()
eval cmd = do
  let e = case cmd of
        Create name -> putStrLn =<< create name
        Apply partialOptions filePath -> do
          let options = either (const $ error "Partial db options. Not possible") id
                      $ Partial.completeOptions partialOptions

          traverse_ (BSL.putStrLn . encodePretty) =<< apply options filePath

  let action = try e >>= \case
        Left err -> case err of
          CouldNotParseMigration filePath -> do
            hPutStrLn stderr $ "Could not parse migration: " <> filePath
            exitWith $ ExitFailure 2
          DirectoryDoesNotExist filePath -> do
            hPutStrLn stderr $ "Directory does not exist: " <> filePath
            exitWith $ ExitFailure 4
        Right () -> pure ()

  try action >>= \case
    Left (finalErr :: SomeException) -> do
      hPutStrLn stderr $ "Unknown error: " <> show finalErr
      exitWith $ ExitFailure 1
    Right () -> pure ()

create :: String -> IO String
create name = do
  now <- getCurrentTime
  let (dir, theFileName) = splitFileName name
      outputFile = formatTime defaultTimeLocale "%Y-%m-%dT%H-%M-%S" now <> "_" <> theFileName
      outputFilePath = dir </> outputFile
  try (withFile outputFilePath WriteMode (const $ pure ())) >>= \case
    Left i
      | isDoesNotExistError i -> throwIO $ DirectoryDoesNotExist outputFilePath
      | otherwise             -> throwIO i
    Right () -> pure ()

  pure outputFilePath

withOptions :: P.Options -> (Connection -> IO a) -> IO a
withOptions options f =
  bracket (Psql.connectPostgreSQL $ P.toConnectionString options) Psql.close f

parseVersion :: FilePath -> Maybe UTCTime
parseVersion filePath = do
  date <- listToMaybe $ splitOn "_" $ takeFileName filePath
  parseTimeM True defaultTimeLocale "%Y-%m-%dT%H-%M-%S" date

computeHash :: String -> Binary Db.Hash
computeHash = Binary . hash . BSC.pack

makeInputMigration :: FilePath -> IO Db.InputMigration
makeInputMigration filePath = do
  inputVersion <- maybe (throwIO $ CouldNotParseMigration filePath) pure $ parseVersion filePath

  theQuery <- readFile filePath
  let inputAction  = void $ T.execute_ $ fromString theQuery
      inputHash    = computeHash theQuery

  pure Db.InputMigration {..}

newtype OutputMigration = OutputMigration Db.OutputMigration

instance ToJSON OutputMigration where
  toJSON (OutputMigration (Db.OutputMigration {..})) = object
    [ "version" .= omVersion
    , "hash"    .= binaryToJSON omHash
    ]

newtype OutputGroup = OutputGroup Db.OutputGroup
  deriving (Show, Eq)

instance ToJSON OutputGroup where
  toJSON (OutputGroup (Db.OutputGroup {..})) = do
    object
      [ "id"         .= groupIdToJSON ogId
      , "created_at" .= ogCreatedAt
      , "migrations" .= fmap OutputMigration ogMigrations
      ]

binaryToJSON :: Binary BSC.ByteString -> Value
binaryToJSON (Binary x) = toJSON $ BSC.unpack $ Base64.encode x

groupIdToJSON :: Db.GroupId -> Value
groupIdToJSON (Db.GroupId x) = binaryToJSON x

apply :: P.Options -> FilePath -> IO (Maybe OutputGroup)
apply options dirPath = do
  xs <- mapM (makeInputMigration . (dirPath </>)) . filter ((==".sql") . takeExtension)
    =<< listDirectory dirPath
  withOptions options $ \conn -> fmap (fmap OutputGroup . join) $ forM (nonEmpty xs) $ \theNonEmpty ->
    T.runDBT (Db.apply =<< Db.inputGroup theNonEmpty) ReadCommitted conn