module Database.PostgreSQL.Simple.Migration
    ( runMigration
    , MigrationContext(..)
    , MigrationCommand(..)
    , MigrationResult(..)
    , ScriptName
    ) where
import           Control.Monad                    (liftM, void, when)
import qualified Crypto.Hash.MD5                  as MD5 (hash)
import qualified Data.ByteString                  as BS (ByteString, readFile)
import qualified Data.ByteString.Base64           as B64 (encode)
import           Data.List                        (isPrefixOf, sort)
import           Data.Monoid                      (mconcat)
import           Database.PostgreSQL.Simple       (Connection, Only (..),
                                                   execute, execute_, query)
import           Database.PostgreSQL.Simple.Types (Query (..))
import           System.Directory                 (getDirectoryContents)
runMigration :: MigrationContext -> IO (MigrationResult String)
runMigration (MigrationContext cmd verbose con) = case cmd of
    MigrationInitialization ->
        initializeSchema con verbose >> return MigrationSuccess
    MigrationDirectory path ->
       executeDirectoryMigration con verbose path
    MigrationScript name contents ->
        executeMigration con verbose name contents
    MigrationFile name path ->
        executeMigration con verbose name =<< BS.readFile path
executeDirectoryMigration :: Connection -> Bool -> FilePath -> IO (MigrationResult String)
executeDirectoryMigration con verbose dir =
    liftM (filter (\x -> not $ "." `isPrefixOf` x))
        (getDirectoryContents dir) >>= go . sort
    where
        go [] = return MigrationSuccess
        go (f:fs) = do
            r <- executeMigration con verbose f =<< BS.readFile (dir ++ "/" ++ f)
            case r of
                MigrationError _ ->
                    return r
                MigrationSuccess ->
                    go fs
executeMigration :: Connection -> Bool -> ScriptName -> BS.ByteString -> IO (MigrationResult String)
executeMigration con verbose name contents = do
    let checksum = md5Hash contents
    checkScript con name checksum >>= \r -> case r of
        ScriptOk -> do
            when verbose $ putStrLn $ "Ok:\t" ++ name
            return MigrationSuccess
        ScriptNotExecuted -> do
            void $ execute_ con (Query contents)
            void $ execute con q (name, checksum)
            when verbose $ putStrLn $ "Execute:\t" ++ name
            return MigrationSuccess
        ScriptModified _ -> do
            when verbose $ putStrLn $ "Fail:\t" ++ name
            return (MigrationError name)
    where
        q = "insert into schema_migrations(filename, checksum) values(?, ?) "
initializeSchema :: Connection -> Bool -> IO ()
initializeSchema con verbose = do
    when verbose $ putStrLn "Initializing schema"
    void $ execute_ con $ mconcat
        [ "create table if not exists schema_migrations "
        , "( filename varchar(512) not null"
        , ", checksum varchar(32) not null"
        , ", executed_at timestamp without time zone not null default now() "
        , ");"
        ]
checkScript :: Connection -> ScriptName -> Checksum -> IO CheckScriptResult
checkScript con name checksum =
    query con q (Only name) >>= \r -> case r of
        [] ->
            return ScriptNotExecuted
        Only actualChecksum:_ | checksum == actualChecksum ->
            return ScriptOk
        Only actualChecksum:_ ->
            return (ScriptModified actualChecksum)
    where
        q = mconcat
            [ "select checksum from schema_migrations "
            , "where filename = ? limit 1"
            ]
md5Hash :: BS.ByteString -> Checksum
md5Hash = B64.encode . MD5.hash
type Checksum = BS.ByteString
type ScriptName = String
data MigrationCommand
    = MigrationInitialization
    
    
    | MigrationDirectory FilePath
    
    
    | MigrationFile ScriptName FilePath
    
    
    | MigrationScript ScriptName BS.ByteString
    
    deriving (Show, Eq, Read, Ord)
data CheckScriptResult
    = ScriptOk
    
    
    | ScriptModified Checksum
    
    
    | ScriptNotExecuted
    
    deriving (Show, Eq, Read, Ord)
data MigrationResult a
    = MigrationError a
    
    | MigrationSuccess
    
    deriving (Show, Eq, Read, Ord)
data MigrationContext = MigrationContext
    { migrationContextCommand    :: MigrationCommand
    
    , migrationContextVerbose    :: Bool
    
    , migrationContextConnection :: Connection
    
    }