{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -Wno-deriving-defaults #-}

module Yore.DB
  ( DB
  , Error (..)
  , initDB
  , withConn
  , withTransaction
  , DayIndex (..)
  , DayFile (..)
  , createDayFile
  , createDayIndex
  , readDayIndex
  , readDayPaths
  , getDayFileByIssue
  , getNumberOfIssues
  , lift
  ) where

import Control.Concurrent (getNumCapabilities)
import Control.Monad.Trans.Except (ExceptT (..), catchE, runExceptT, throwE)
import Data.Functor.Identity (Identity (..))
import Data.Pool (Pool, defaultPoolConfig, newPool, withResource)
import Data.Text (Text)
import Data.Time (Day)
import GHC.Generics (Generic)

import qualified Database.PostgreSQL.Opium as Opium

import Data.Bifunctor (Bifunctor (..))
import Yore.Error (Error (..))

newtype DB = DB (Pool (Either Opium.ConnectionError Opium.Connection))

initDB :: Text -> IO DB
initDB connString = do
  capabilities <- getNumCapabilities
  let maxResources = capabilities
  DB
    <$> newPool
      ( defaultPoolConfig
          (Opium.connect connString)
          (either (const $ pure ()) Opium.close)
          10
          maxResources
      )

-- TODO: This should probably also do something like bracket...
-- For now let's assume no exceptions are thrown in f.
withConn :: DB -> (Opium.Connection -> ExceptT Error IO a) -> ExceptT Error IO a
withConn (DB connPool) f =
  ExceptT $ withResource connPool $ \case
    Left connectionError ->
      pure $ Left $ ConnectionError connectionError
    Right conn ->
      runExceptT $ f conn

withTransaction :: DB -> (Opium.Connection -> ExceptT Error IO a) -> ExceptT Error IO a
withTransaction db f =
  withConn db $ \conn ->
    ( do
        lift $ Opium.execute_ "BEGIN" conn
        x <- f conn
        lift $ Opium.execute_ "COMMIT" conn
        pure x
    )
      `catchE` ( \err -> do
                  -- rollback open transactions and release transaction level locks...
                  lift $ Opium.execute_ "ROLLBACK" conn
                  -- ...then rethrow the error so the app can handle it too.
                  throwE err
               )

lift :: IO (Either Opium.Error a) -> ExceptT Error IO a
lift f =
  ExceptT $ first DBError <$> f

data DayIndex = DayIndex
  { day_index_id :: Int
  , day :: Day
  }
  deriving (Show, Generic, Opium.FromRow)

data DayFile = DayFile
  { day_file_id :: Int
  , day_index_id :: Int
  , label :: Text
  , relative_path :: FilePath
  }
  deriving (Show, Generic, Opium.FromRow)

newtype CountResult = CountResult
  { count :: Int
  }
  deriving (Show, Generic, Opium.FromRow)

createDayIndex :: Day -> Opium.Connection -> ExceptT Error IO DayIndex
createDayIndex date =
  ex runIdentity
    . Opium.fetch
      "INSERT INTO yore.day_index (day) VALUES ($1) RETURNING *"
      (Identity date)

createDayFile :: Int -> Text -> FilePath -> Opium.Connection -> ExceptT Error IO ()
createDayFile dayId label path =
  lift
    . Opium.execute
      "INSERT INTO yore.day_file (day_index_id, label, relative_path) VALUES ($1, $2, $3)"
      (dayId, label, path)

readDayIndex :: Day -> Opium.Connection -> ExceptT Error IO (Maybe DayIndex)
readDayIndex date =
  lift
    . Opium.fetch
      "SELECT * FROM yore.day_index WHERE day = $1"
      (Identity date)

readDayPaths :: Day -> Opium.Connection -> ExceptT Error IO [(DayIndex, DayFile)]
readDayPaths date =
  lift
    . Opium.fetch
      "SELECT * FROM yore.day_index LEFT JOIN yore.day_file USING (day_index_id) WHERE day = $1"
      (Identity date)

getDayFileByIssue :: Day -> Int -> Opium.Connection -> ExceptT Error IO DayFile
getDayFileByIssue date issue =
  ex runIdentity
    . Opium.fetch
      "SELECT yore.day_file.* FROM yore.day_index LEFT JOIN yore.day_file USING (day_index_id) WHERE day = $1 ORDER BY day_file_id LIMIT 1 OFFSET $2"
      (date, issue)

getNumberOfIssues :: Day -> Opium.Connection -> ExceptT Error IO Int
getNumberOfIssues date =
  ex (count . runIdentity)
    . Opium.fetch
      "SELECT count(*) FROM yore.day_index LEFT JOIN yore.day_file USING (day_index_id) WHERE day = $1"
      (Identity date)

ex :: (a -> b) -> IO (Either Opium.Error a) -> ExceptT Error IO b
ex f = fmap f . lift
