{-# LANGUAGE DeriveGeneric, NamedFieldPuns, TypeOperators, DataKinds, RecordWildCards, DeriveAnyClass #-}
module OddJobs.Web where

import OddJobs.Types
import OddJobs.Job as Job
import Data.Time
import Data.Aeson as Aeson
import qualified Data.Text as T
import Data.Text (Text)
import GHC.Generics hiding (from, to)
import Database.PostgreSQL.Simple as PGS
import Database.PostgreSQL.Simple.ToRow as PGS
import Database.PostgreSQL.Simple.ToField as PGS
import Data.Pool as Pool
import UnliftIO
import Data.Maybe
import Data.String (fromString)
import Control.Applicative ((<|>))
import Data.List (nub)
import Servant
import Servant.API.Generic
import Servant.HTML.Lucid
import Lucid
import Lucid.Html5
import Lucid.Base
import Data.String.Conv
import qualified Data.HashMap.Strict as HM
import Data.List as DL hiding (filter, and)
import Control.Monad
import Data.Time.Format.Human (humanReadableTime')
import Data.Time.Convenience (timeSince, Unit(..), Direction(..))
import Data.Text.Conversions (fromText, toText)
import Prelude hiding (filter, and)

data OrderDirection = Asc | Desc deriving (Eq, Show, Generic, Enum)

data OrderByField = OrdCreatedAt
                  | OrdUpdatedAt
                  | OrdLockedAt
                  | OrdStatus
                  | OrdJobType
                  deriving (Eq, Show, Generic, Enum)

data Filter = Filter
  { filterStatuses :: [Status]
  , filterCreatedAfter :: Maybe UTCTime
  , filterCreatedBefore :: Maybe UTCTime
  , filterUpdatedAfter :: Maybe UTCTime
  , filterUpdatedBefore :: Maybe UTCTime
  , filterJobTypes :: [Text]
  , filterOrder :: Maybe (OrderByField, OrderDirection)
  , filterPage :: Maybe (Int, Int)
  , filterRunAfter :: Maybe UTCTime
  , filterJobRunner :: [JobRunnerName]
  } deriving (Eq, Show, Generic)

instance Semigroup Filter where
  (<>) a b = Filter
    { filterStatuses = nub (filterStatuses b <> filterStatuses a)
    , filterCreatedAfter = filterCreatedAfter b <|> filterCreatedAfter a
    , filterCreatedBefore = filterCreatedBefore b <|> filterCreatedBefore a
    , filterUpdatedAfter = filterUpdatedAfter b <|> filterUpdatedBefore a
    , filterUpdatedBefore = filterUpdatedBefore b <|> filterUpdatedBefore a
    , filterJobTypes = nub (filterJobTypes b <> filterJobTypes a)
    , filterOrder = filterOrder b <|> filterOrder a
    , filterPage = filterPage b <|> filterPage a
    , filterRunAfter = filterRunAfter b <|> filterRunAfter a
    , filterJobRunner = nub (filterJobRunner b <> filterJobRunner a)
    }

instance Monoid Filter where
  mempty = blankFilter

blankFilter :: Filter
blankFilter = Filter
  { filterStatuses = []
  , filterCreatedAfter = Nothing
  , filterCreatedBefore = Nothing
  , filterUpdatedAfter = Nothing
  , filterUpdatedBefore = Nothing
  , filterJobTypes = []
  , filterOrder = Nothing
  , filterPage = Just (10, 0)
  , filterRunAfter = Nothing
  , filterJobRunner = []
  }

instance ToJSON OrderDirection
instance FromJSON OrderDirection
instance ToJSON OrderByField
instance FromJSON OrderByField

instance ToJSON Filter where
  toJSON = Aeson.genericToJSON  Aeson.defaultOptions{omitNothingFields = True}
instance FromJSON Filter where
  parseJSON = Aeson.genericParseJSON Aeson.defaultOptions{omitNothingFields = True}


instance FromHttpApiData Filter where
  parseQueryParam x = case eitherDecode (toS x) of
    Left e -> Left $ toS e
    Right r -> Right r

instance ToHttpApiData Filter where
  toQueryParam x = toS $ Aeson.encode x

-- data Routes route = Routes
--   { rFilterResults :: route :- QueryParam "filters" Filter :> Get '[HTML] (Html ())
--   , rStaticAssets :: route :- "assets" :> Raw
--   , rEnqueue :: route :- "enqueue" :> Capture "jobId" JobId :> Post '[HTML] NoContent
--   , rRunNow :: route :- "run" :> Capture "jobId" JobId :> Post '[HTML] NoContent
--   , rCancel :: route :- "cancel" :> Capture "jobId" JobId :> Post '[HTML] NoContent
--   , rRefreshJobTypes :: route :- "refresh-job-types" :> Post '[HTML] NoContent
--   , rRefreshJobRunners :: route :- "refresh-job-runners" :> Post '[HTML] NoContent
--   } deriving (Generic)


data Routes = Routes
  { rFilterResults :: Maybe Filter -> Text
  , rEnqueue :: JobId -> Text
  , rRunNow :: JobId -> Text
  , rCancel :: JobId -> Text
  , rRefreshJobTypes :: Text
  , rRefreshJobRunners :: Text
  , rStaticAsset :: Text -> Text
  }


filterJobsQuery :: Config -> Filter -> (PGS.Query, [Action])
filterJobsQuery Config{cfgTableName, cfgJobTypeSql} Filter{..} =
  ( "SELECT " <> Job.concatJobDbColumns <> " FROM " <> cfgTableName <> whereClause <> " " <> (orderClause $ fromMaybe (OrdUpdatedAt, Desc) filterOrder) <> " " <> limitOffsetClause
  , whereActions
  )
  where
    orderClause (flt, dir) =
      let fname = case flt of
            OrdCreatedAt -> "created_at"
            OrdUpdatedAt -> "updated_at"
            OrdLockedAt -> "locked_at"
            OrdStatus -> "status"
            OrdJobType -> "payload->>'tag'"
          dname = case dir of
            Asc -> "asc nulls first"
            Desc -> "desc nulls last"
      in "ORDER BY " <> fname <> " " <> dname <> ", id desc"

    limitOffsetClause :: Query
    limitOffsetClause = case filterPage of
      Nothing -> mempty
      Just (l, o) -> "LIMIT " <> fromString (show l) <> " OFFSET " <> fromString (show o)

    (whereClause, whereActions) =
      let finalClause = statusClause `and` createdAfterClause `and`
                       createdBeforeClause `and` updatedBeforeClause `and`
                       updatedAfterClause `and` jobTypeClause `and`
                       runAfterClause `and` jobRunnerClause
      in case finalClause  of
        Nothing -> (mempty, toRow ())
        Just (q, as) -> (" WHERE " <> q, as)

    statusClause = if Prelude.null filterStatuses
                   then Nothing
                   else Just ("status IN ?", toRow $ (Only (In filterStatuses)))

    createdAfterClause = Prelude.fmap (\x -> ("created_at >= ?", toRow $ Only x)) filterCreatedAfter
    createdBeforeClause = Prelude.fmap (\x -> ("created_at < ?", toRow $ Only x)) filterCreatedBefore
    updatedAfterClause = Prelude.fmap (\x -> ("updated_at >= ?", toRow $ Only x)) filterUpdatedAfter
    updatedBeforeClause = Prelude.fmap (\x -> ("updated_at < ?", toRow $ Only x)) filterUpdatedBefore
    runAfterClause = Prelude.fmap (\x -> ("run_at > ?", toRow $ Only x)) filterRunAfter

    jobTypeClause :: Maybe (Query, [Action])
    jobTypeClause = case filterJobTypes of
      [] -> Nothing
      xs ->
        let qFragment = "(" <> cfgJobTypeSql <> ")=?"
            build ys (q, vs) = case ys of
              [] -> (q, vs)
              (y:[]) -> (qFragment <> q, (toField y):vs)
              (y:ys_) -> build ys_ (" OR " <> qFragment <> q, (toField y):vs)
        in Just $ build xs (mempty, [])

    jobRunnerClause :: Maybe (Query, [Action])
    jobRunnerClause = case filterJobRunner of
      [] -> Nothing
      xs -> Just ("locked_by in ?", toRow $ Only $ In xs)

    and :: Maybe (Query, [PGS.Action]) -> Maybe (Query, [PGS.Action]) -> Maybe (Query, [PGS.Action])
    and Nothing Nothing = Nothing
    and Nothing (Just (q, as)) = Just (q, as)
    and (Just (q, as)) Nothing = Just (q, as)
    and (Just (qa, as)) (Just (qb, bs)) = Just ("(" <> qa <> ") AND (" <> qb <> ")", as <> bs)

    -- orderClause = _

filterJobs :: Config -> Connection -> Filter -> IO [Job]
filterJobs cfg conn f = do
  let (q, queryArgs) = filterJobsQuery cfg f
  PGS.query conn q queryArgs

countJobs :: Config -> Connection -> Filter -> IO Int
countJobs cfg conn f = do
  let (q, queryArgs) = filterJobsQuery cfg f
      finalqry = "SELECT count(*) FROM (" <> q <> ") a"
  [Only r] <- PGS.query conn finalqry queryArgs
  pure r


-- f = encode [FTStatuses [Job.Success, Queued], FTJobType "QueuedMail"]

-- f = blankFilter
--   { filterStatuses = [Job.Success, Queued]
--   , filterJobTypes = ["QueuedMail", "ConfirmBooking"]
--   }



pageNav :: Routes -> Html ()
pageNav Routes{..} = do
  div_ $ nav_ [ class_ "navbar navbar-default navigation-clean" ] $ div_ [ class_ "container-fluid" ] $ do
    div_ [ class_ "navbar-header" ] $ do
      a_ [ class_ "navbar-brand navbar-link", href_ "#", style_ "margin-left: 2px; padding: 0px;" ] $ img_ [ src_ $ rStaticAsset "assets/odd-jobs-color-logo.png", title_ "Odd Jobs Logo" ]
      button_ [ class_ "navbar-toggle collapsed", data_ "toggle" "collapse", data_ "target" "#navcol-1" ] $ do
        span_ [ class_ "sr-only" ] $ "Toggle navigation"
        span_ [ class_ "icon-bar" ] $ ""
        span_ [ class_ "icon-bar" ] $ ""
        span_ [ class_ "icon-bar" ] $ ""
    -- div_ [ class_ "collapse navbar-collapse", id_ "navcol-1" ] $ ul_ [ class_ "nav navbar-nav navbar-right" ] $ do
    --   li_ [ class_ "active", role_ "presentation" ] $ a_ [ href_ "#" ] $ "First Item"
    --   li_ [ role_ "presentation" ] $ a_ [ href_ "#" ] $ "Second Item"
    --   li_ [ role_ "presentation" ] $ a_ [ href_ "#" ] $ "Third Item"
    --   li_ [ class_ "dropdown" ] $ do
    --     a_ [ class_ "dropdown-toggle", data_ "toggle" "dropdown", ariaExpanded_ "false", href_ "#" ] $ do
    --       "Dropdown"
    --       span_ [ class_ "caret" ] $ ""
    --     ul_ [ class_ "dropdown-menu", role_ "menu" ] $ do
    --       li_ [ role_ "presentation" ] $ a_ [ href_ "#" ] $ "First Item"
    --       li_ [ role_ "presentation" ] $ a_ [ href_ "#" ] $ "Second Item"
    --       li_ [ role_ "presentation" ] $ a_ [ href_ "#" ] $ "Third Item"

pageLayout :: Routes -> Html() -> Html () -> Html ()
pageLayout routes@Routes{..} navHtml bodyHtml = do
  doctype_
  html_ $ do
    head_ $ do
      meta_ [ charset_ "utf-8" ]
      meta_ [ name_ "viewport", content_ "width=device-width, initial-scale=1.0" ]
      title_ "haskell-pg-queue"
      link_ [ rel_ "stylesheet", href_ $ rStaticAsset "assets/bootstrap/css/bootstrap.min.css" ]
      link_ [ rel_ "stylesheet", href_ $ rStaticAsset "https://fonts.googleapis.com/css?family=Lato:100i,300,300i,400,700,900" ]
      link_ [ rel_ "stylesheet", href_ $ rStaticAsset "assets/css/styles.css" ]
    body_ $ do
      pageNav routes
      div_ $ div_ [ class_ "container-fluid", style_ "/*background-color:#f2f2f2;*/" ] $ div_ [ class_ "row" ] $ do
        div_ [ class_ "d-none d-md-block col-md-2" ] navHtml
        div_ [ class_ "col-12 col-md-10" ] bodyHtml
      script_ [ src_ $ rStaticAsset "assets/js/jquery.min.js" ] $ ("" :: Text)
      script_ [ src_ $ rStaticAsset "assets/bootstrap/js/bootstrap.min.js" ] $ ("" :: Text)
      script_ [ src_ $ rStaticAsset "assets/js/custom.js" ] $ ("" :: Text)

sideNav :: Routes -> [Text] -> [JobRunnerName] -> UTCTime -> Filter -> Html ()
sideNav Routes{..} jobTypes jobRunnerNames t filter@Filter{..} = do
  div_ [ class_ "filters mt-3" ] $ do
    jobStatusFilters
    jobTypeFilters
    jobRunnerFilters
  where
    jobStatusFilters = do
      h6_ "Filter by job status"
      div_ [ class_ "card" ] $ do
        ul_ [ class_ "list-group list-group-flush" ] $ do
          li_ [ class_ ("list-group-item " <> if filterStatuses == [] then "active-nav" else "") ] $ do
            let lnk = (rFilterResults $ Just filter{filterStatuses = [], filterPage = (OddJobs.Web.filterPage blankFilter)})
            a_ [ href_ lnk ] $ do
              "all"
              -- span_ [ class_ "badge badge-pill badge-secondary float-right" ] "12"
          forM_ ((\\) (enumFrom minBound) [Job.Success]) $ \st -> do
            li_ [ class_ ("list-group-item " <> if (st `elem` filterStatuses) then "active-nav" else "") ] $ do
              let lnk = (rFilterResults $ Just filter{filterStatuses = [st], filterPage = Nothing})
              a_ [ href_ lnk ] $ do
                toHtml $ toText st
                -- span_ [ class_ "badge badge-pill badge-secondary float-right" ] "12"

    jobRunnerFilters = do
      h6_ [ class_ "mt-3" ] $ do
        "Filter by job-runner"
        form_ [ method_ "post", action_ rRefreshJobRunners, class_ "d-inline"] $ do
          button_ [ type_ "submit", class_ "btn btn-link m-0 p-0 ml-1 float-right"] $ do
            small_ "refresh"

      div_ [ class_ "card" ] $ do
        ul_ [ class_ "list-group list-group-flush" ] $ do
          li_ [ class_ ("list-group-item " <> if filterJobRunner == [] then "active-nav" else "") ] $ do
            let lnk = (rFilterResults $ Just filter{filterJobRunner = [], filterPage = (OddJobs.Web.filterPage blankFilter)})
            a_ [ href_ lnk ] "all"
          forM_ jobRunnerNames $ \jr -> do
            li_ [ class_ ("list-group-item" <> if (jr `elem` filterJobRunner) then " active-nav" else "")] $ do
              a_ [ href_ "#" ] $ toHtml $ unJobRunnerName jr

    jobTypeFilters = do
      h6_ [ class_ "mt-3" ] $ do
        "Filter by job-type"
        form_ [ method_ "post", action_ rRefreshJobTypes, class_ "d-inline"] $ do
          button_ [ type_ "submit", class_ "btn btn-link m-0 p-0 ml-1 float-right"] $ do
            small_ "refresh"

      div_ [ class_ "card" ] $ do
        ul_ [ class_ "list-group list-group-flush" ] $ do
          li_ [ class_ ("list-group-item " <> if filterJobTypes == [] then "active-nav" else "") ] $ do
            let lnk = (rFilterResults $ Just filter{filterJobTypes = [], filterPage = (OddJobs.Web.filterPage blankFilter)})
            a_ [ href_ lnk ] "all"
          forM_ jobTypes $ \jt -> do
            li_ [ class_ ("list-group-item" <> if (jt `elem` filterJobTypes) then " active-nav" else "")] $ do
              a_ [ href_ (rFilterResults $ Just filter{filterJobTypes=[jt]}) ] $ toHtml jt

searchBar :: Routes -> UTCTime -> Filter -> Html ()
searchBar Routes{..} t filter@Filter{filterStatuses, filterCreatedAfter, filterCreatedBefore, filterUpdatedAfter, filterUpdatedBefore, filterJobTypes, filterRunAfter} = do
  form_ [ style_ "padding-top: 2em;" ] $ do
    div_ [ class_ "form-group" ] $ do
      div_ [ class_ "search-container" ] $ do
        ul_ [ class_ "list-inline search-bar" ] $ do
          forM_ filterStatuses $ \s -> renderFilter "Status" (toText s) (rFilterResults $ Just filter{filterStatuses = filterStatuses \\ [s]})
          maybe mempty (\x -> renderFilter "Created after" (showText x) (rFilterResults $ Just filter{filterCreatedAfter = Nothing})) filterCreatedAfter
          maybe mempty (\x -> renderFilter "Created before" (showText x) (rFilterResults $ Just filter{filterCreatedBefore = Nothing})) filterCreatedBefore
          maybe mempty (\x -> renderFilter "Updated after" (showText x) (rFilterResults $ Just filter{filterUpdatedAfter = Nothing})) filterUpdatedAfter
          maybe mempty (\x -> renderFilter "Updated before" (showText x) (rFilterResults $ Just filter{filterUpdatedBefore = Nothing})) filterUpdatedBefore
          maybe mempty (\x -> renderFilter "Run after" (showText x) (rFilterResults $ Just filter{filterRunAfter = Nothing})) filterRunAfter
          forM_ filterJobTypes $ \x -> renderFilter "Job type" x (rFilterResults $ Just filter{filterJobTypes = filterJobTypes \\ [x]})

        button_ [ class_ "btn btn-default search-button", type_ "button" ] $ "Search"
      -- ul_ [ class_ "list-inline" ] $ do
      --   li_ $ span_ $ strong_ "Common searches:"
      --   li_ $ a_ [ href_ (rFilterResults $ Just mempty) ] $ "All jobs"
      --   li_ $ a_ [ href_ (rFilterResults $ Just $ filter{ filterStatuses = [Job.Locked] }) ] $ "Currently running"
      --   li_ $ a_ [ href_ (rFilterResults $ Just $ filter{ filterStatuses = [Job.Success] }) ] $ "Successful"
      --   li_ $ a_ [ href_ (rFilterResults $ Just $ filter{ filterStatuses = [Job.Failed] }) ] $ "Failed"
      --   li_ $ a_ [ href_ (rFilterResults $ Just $ filter{ filterRunAfter = Just t }) ] $ "Future"
      --   -- li_ $ a_ [ href_ "#" ] $ "Retried"
      --   li_ $ a_ [ href_ (rFilterResults $ Just $ filter{ filterStatuses = [Job.Queued] }) ] $ "Queued"
      --   li_ $ a_ [ href_ (rFilterResults $ Just $ filter{ filterUpdatedAfter = Just $ timeSince t 10 Minutes Ago }) ] $ "Last 10 mins"
      --   li_ $ a_ [ href_ (rFilterResults $ Just $ filter{ filterCreatedAfter = Just $ timeSince t 10 Minutes Ago }) ] $ "Recently created"
  where
    renderFilter :: Text -> Text -> Text -> Html ()
    renderFilter k v u = do
      li_ [ class_ "search-filter" ] $ do
        span_ [ class_ "filter-name" ] $ toHtml k
        span_ [ class_ "filter-value" ] $ do
          toHtml v
          a_ [ href_ u, class_ "text-danger" ] $ i_ [ class_ "glyphicon glyphicon-remove" ] $ ""


timeDuration :: UTCTime -> UTCTime -> (Int, String)
timeDuration from to = (diff, str)
  where
    str = if diff <= 0
          then "under 1s"
          else (if d>0 then (show d) <> "d" else "") <>
               (if m>0 then (show m) <> "m" else "") <>
               (if s>0 then (show s) <> "s" else "")
    diff = (abs $ round $ diffUTCTime from to)
    (m', s) = diff `divMod` 60
    (h', m) = m' `divMod` 60
    (d, h) = h' `divMod` 24

showText :: (Show a) => a -> Text
showText a = toS $ show a

jobContent :: Value -> Value
jobContent v = case v of
  Aeson.Object o -> case HM.lookup "contents" o of
    Nothing -> v
    Just c -> c
  _ -> v

jobRow :: Routes -> UTCTime -> (Job, Html ()) -> Html ()
jobRow routes t (job@Job{..}, jobHtml) = do
  tr_ $ do
    td_ [ class_ "job-type" ] $ do
      let statusFn = case jobStatus of
            Job.Success -> statusSuccess
            Job.Failed -> statusFailed
            Job.Queued -> if jobRunAt > t
                          then statusFuture
                          else statusWaiting
            Job.Retry -> statusRetry
            Job.Locked -> statusLocked
      statusFn t job

    td_ jobHtml
    td_ $ do
      let actionsFn = case jobStatus of
            Job.Success -> (const mempty)
            Job.Failed -> actionsFailed
            Job.Queued -> if jobRunAt > t
                          then actionsFuture
                          else actionsWaiting
            Job.Retry -> actionsRetry
            Job.Locked -> (const mempty)
      actionsFn routes job


actionsFailed :: Routes -> Job -> Html ()
actionsFailed Routes{..} Job{..} = do
  form_ [ action_ (rEnqueue jobId), method_ "post" ] $ do
    button_ [ class_ "btn btn-secondary", type_ "submit" ] $ "Enqueue again"

actionsRetry :: Routes -> Job -> Html ()
actionsRetry Routes{..} Job{..} = do
  form_ [ action_ (rRunNow jobId), method_ "post" ] $ do
    button_ [ class_ "btn btn-secondary", type_ "submit" ] $ "Run now"

actionsFuture :: Routes -> Job -> Html ()
actionsFuture Routes{..} Job{..} = do
  form_ [ action_ (rRunNow jobId), method_ "post" ] $ do
    button_ [ class_ "btn btn-secondary", type_ "submit" ] $ "Run now"

actionsWaiting :: Routes -> Job -> Html ()
actionsWaiting Routes{..} Job{..} = do
  form_ [ action_ (rCancel jobId), method_ "post" ] $ do
    button_ [ class_ "btn btn-danger", type_ "submit" ] $ "Cancel"

statusSuccess :: UTCTime -> Job -> Html ()
statusSuccess t Job{..} = do
  span_ [ class_ "badge badge-success" ] $ "Success"
  span_ [ class_ "job-run-time" ] $ do
    let (d, s) = timeDuration jobCreatedAt jobUpdatedAt
    abbr_ [ title_ (showText jobUpdatedAt) ] $ toHtml $ "Completed " <> humanReadableTime' t jobUpdatedAt <> ". "
    abbr_ [ title_ (showText d <> " seconds")] $ toHtml $ "Took " <> s

statusFailed :: UTCTime -> Job -> Html ()
statusFailed t Job{..} = do
  span_ [ class_ "badge badge-danger" ] $ "Failed"
  span_ [ class_ "job-run-time" ] $ do
    abbr_ [ title_ (showText jobUpdatedAt) ] $ toHtml $ "Failed " <> humanReadableTime' t jobUpdatedAt <> " after " <> show jobAttempts <> " attempts"

statusFuture :: UTCTime -> Job -> Html ()
statusFuture t Job{..} = do
  span_ [ class_ "badge badge-secondary" ] $ "Future"
  span_ [ class_ "job-run-time" ] $ do
    abbr_ [ title_ (showText jobRunAt) ] $ toHtml $ humanReadableTime' t jobRunAt

statusWaiting :: UTCTime -> Job -> Html ()
statusWaiting t Job{..} = do
      span_ [ class_ "badge badge-warning" ] $ "Waiting"
      -- span_ [ class_ "job-run-time" ] ("Waiting to be picked up" :: Text)

statusRetry :: UTCTime -> Job -> Html ()
statusRetry t Job{..} = do
  span_ [ class_ "badge badge-warning" ] $ toHtml $ "Retries (" <> show jobAttempts <> ")"
  span_ [ class_ "job-run-time" ] $ do
    abbr_ [ title_ (showText jobUpdatedAt) ] $ toHtml $ "Retried " <> humanReadableTime' t jobUpdatedAt <> ". "
    abbr_ [ title_ (showText jobRunAt)] $ toHtml $ "Next retry in " <> humanReadableTime' t jobRunAt

statusLocked :: UTCTime -> Job -> Html ()
statusLocked t Job{..} = do
  span_ [ class_ "badge badge-info" ] $ toHtml ("Locked"  :: Text)
  -- span_ [ class_ "job-run-time" ] $ do
  --   abbr_ [ title_ (showText jobUpdatedAt) ] $ toHtml $ "Retried " <> humanReadableTime' t jobUpdatedAt <> ". "
  --   abbr_ [ title_ (showText jobRunAt)] $ toHtml $ "Next retry in " <> humanReadableTime' t jobRunAt

resultsPanel :: Routes -> UTCTime -> Filter -> [(Job, Html ())] -> Int -> Html ()
resultsPanel routes@Routes{..} t filter@Filter{filterPage} js runningCount = do
  div_ [ class_ "card mt-3" ] $ do
    div_ [ class_ "card-header bg-secondary text-white" ] $ do
      "Currently running "
      span_ [ class_ "badge badge-primary badge-primary" ] $ toHtml (show runningCount)
    div_ [ class_ "currently-running" ] $ div_ [ class_ "" ] $ table_ [ class_ "table table-striped table-hover" ] $ do
      thead_ [ class_ "thead-dark"] $ do
        tr_ $ do
          th_ "Job status"
          th_ "Job"
          th_ [ style_ "min-width: 12em;" ] "Actions"
      tbody_ $ do
        forM_ js (jobRow routes t)
    div_ [ class_ "card-footer" ] $ do
      nav_ $ do
        ul_ [ class_ "pagination" ] $ do
          prevLink
          nextLink
  where
    prevLink = do
      let (extraClass, lnk) = case filterPage of
            Nothing -> ("disabled", "")
            Just (l, 0) -> ("disabled", "")
            Just (l, o) -> ("", rFilterResults $ Just $ filter {filterPage = Just (l, max 0 $ o - l)})
      li_ [ class_ ("page-item previous " <> extraClass) ] $ do
        a_ [ class_ "page-link", href_ lnk ] $ "Prev"

    nextLink = do
      let (extraClass, lnk) = case filterPage of
            Nothing ->
              if (DL.length js) < 10
              then ("disabled", "")
              else ("", (rFilterResults $ Just $ filter {filterPage = Just (10, 10)}))
            Just (l, o) ->
              if (DL.length js) < l
              then ("disabled", "")
              else ("", (rFilterResults $ Just $ filter {filterPage = Just (l, o + l)}))
      li_ [ class_ ("page-item next " <> extraClass) ] $ do
        a_ [ class_ "page-link", href_ lnk ] $ "Next"

ariaExpanded_ :: Text -> Attribute
ariaExpanded_ v = makeAttribute "aria-expanded" v