module Utils.Metrics
  ( withHealthReportMetrics
  , registerGHCMetrics
  , observeHTTPRequestLatency
  , registerReadyMetric
  , withJobWorkerStateLbls
  , observeYesodCacheSize
  , observeFavouritesQuickActionsDuration
  , LoginOutcome(..), observeLoginOutcome
  ) where

import Import.NoModel hiding (Vector, Info)
import Model
import Prometheus
import Prometheus.Metric.GHC

import qualified Data.List as List

import System.Clock
import Data.Time.Clock.POSIX

import Network.Wai (Middleware)
import qualified Network.Wai as Wai
import qualified Network.HTTP.Types as HTTP

import Yesod.Core.Types (HandlerData(..), GHState(..))

{-# ANN module ("HLint: ignore Use even" :: String) #-}


histogramBuckets :: Rational -- ^ min
                 -> Rational -- ^ max
                 -> [Double]
histogramBuckets bMin bMax = map fromRational . takeWhile (<= bMax) . go bMin $ List.cycle factors
  where
    go n []     = [n]
    go n (f:fs) = n : go (f * n) fs

    factors
      | bMin' `mod` 2 == 0 = [2.5, 2, 2]
      | bMin' `mod` 5 == 0 = [2, 2, 2.5]
      | otherwise          = [2, 2.5, 2]
      where
        bMin' :: Integer
        bMin' = floor . List.head . dropWhile (< 1) $ List.iterate (* 10) bMin


{-# NOINLINE healthReportTime #-}
healthReportTime :: Vector Label2 Gauge
healthReportTime = unsafeRegister . vector ("check", "status") $ gauge info
  where
    info = Info "uni2work_health_check_time"
                "POSIXTime of last health check performed by this Uni2work-instance"

{-# NOINLINE healthReportDuration #-}
healthReportDuration :: Vector Label2 Histogram
healthReportDuration = unsafeRegister . vector ("check", "status") $ histogram info buckets
  where
    info = Info "uni2work_health_check_duration_seconds"
                "Duration of last health check performed by this Uni2work-instance"
    buckets = histogramBuckets 5e-6 100e-3

{-# NOINLINE httpRequestLatency #-}
httpRequestLatency :: Vector Label3 Histogram
httpRequestLatency = unsafeRegister . vector ("handler", "method", "status") $ histogram info buckets
  where info = Info "http_request_duration_seconds"
                    "HTTP request latency"
        buckets = histogramBuckets 50e-6 500

data ReadySince = MkReadySince

readyMetric :: POSIXTime -> Metric ReadySince
readyMetric ts = Metric $ return (MkReadySince, collectReadySince)
  where
    collectReadySince = return [SampleGroup info GaugeType [Sample "ready_time" [] sample]]
    info = Info "ready_time"
                "POSIXTime this Uni2work-instance became ready"
    sample = encodeUtf8 $ tshow (realToFrac ts :: Nano)

{-# NOINLINE jobWorkerStateDuration #-}
jobWorkerStateDuration :: Vector Label4 Histogram
jobWorkerStateDuration = unsafeRegister . vector ("worker", "state", "jobctl", "task") $ histogram info buckets
  where info = Info "uni2work_job_worker_state_duration_seconds"
                    "Duration of time a Uni2work job executor spent in a certain state"
        buckets = histogramBuckets 1e-6 500

{-# NOINLINE jobWorkerStateTransitions #-}
jobWorkerStateTransitions :: Vector Label4 Counter
jobWorkerStateTransitions = unsafeRegister . vector ("worker", "state", "jobctl", "task") $ counter info
  where info = Info "uni2work_job_worker_state_transitions_total"
                    "Number of times a Uni2work job executor entered a certain state"

{-# NOINLINE yesodCacheSize #-}
yesodCacheSize :: Histogram
yesodCacheSize = unsafeRegister $ histogram info buckets
  where info = Info "yesod_ghs_cache_items"
                    "Number of items in Yesod's ghsCache and ghsCacheBy"
        buckets = 0 : histogramBuckets 1 1e6

{-# NOINLINE favouritesQuickActionsDuration #-}
favouritesQuickActionsDuration :: Histogram
favouritesQuickActionsDuration = unsafeRegister $ histogram info buckets
  where info = Info "uni2work_favourites_quick_actions_seconds"
                    "Duration of time needed to calculate a set of favourite quick actions"
        buckets = histogramBuckets 500e-6 50

{-# NOINLINE loginOutcomes #-}
loginOutcomes :: Vector Label2 Counter
loginOutcomes = unsafeRegister . vector ("plugin", "outcome") $ counter info
  where info = Info "uni2work_login_attempts_total"
                    "Number of login attempts"


withHealthReportMetrics :: MonadIO m => m HealthReport -> m HealthReport
withHealthReportMetrics act = do
  before <- liftIO $ getTime Monotonic
  report <- act
  after <- liftIO $ getTime Monotonic

  let checkVal = toPathPiece $ classifyHealthReport report
      statusVal = toPathPiece $ healthReportStatus report
      timeSample, durationSample :: Double
      timeSample = realToFrac (realToFrac after :: Nano)
      durationSample = realToFrac (realToFrac $ after - before :: Nano)
  liftIO $ withLabel healthReportTime (checkVal, statusVal) $ flip setGauge timeSample
  liftIO $ withLabel healthReportDuration (checkVal, statusVal) $ flip observe durationSample

  return report

registerGHCMetrics :: MonadIO m => m ()
registerGHCMetrics = void $ register ghcMetrics

observeHTTPRequestLatency :: forall site. ParseRoute site => (Route site -> String) -> Middleware
observeHTTPRequestLatency classifyHandler app req respond' = do
  start <- getTime Monotonic
  app req $ \res -> do
    end <- getTime Monotonic
    let method  = decodeUtf8 $ Wai.requestMethod req
        status  = tshow . HTTP.statusCode $ Wai.responseStatus res
        route :: Maybe (Route site)
        route   = parseRoute ( Wai.pathInfo req
                             , over (mapped . _2) (fromMaybe "") . HTTP.queryToQueryText $ Wai.queryString req
                             )
        handler' = pack . classifyHandler <$> route

        labels :: Label3
        labels = (fromMaybe "n/a" handler', method, status)
    withLabel httpRequestLatency labels . flip observe . realToFrac $ end - start

    respond' res

registerReadyMetric :: MonadIO m => m ()
registerReadyMetric = liftIO $ void . register . readyMetric =<< getPOSIXTime

withJobWorkerStateLbls :: (MonadIO m, MonadMask m) => Label4 -> m a -> m a
withJobWorkerStateLbls newLbls act = do
  liftIO $ withLabel jobWorkerStateTransitions newLbls incCounter

  start <- liftIO $ getTime Monotonic
  res <- handleAll (return . Left) $ Right <$> act
  end <- liftIO $ getTime Monotonic

  liftIO . withLabel jobWorkerStateDuration newLbls . flip observe . realToFrac $ end - start

  either throwM return res

observeYesodCacheSize :: MonadHandler m => m ()
observeYesodCacheSize = do
  HandlerData{handlerState} <- liftHandler ask
  liftIO $ do
    GHState{..} <- readIORef handlerState
    let size = fromIntegral $ length ghsCache + length ghsCacheBy
    observe yesodCacheSize size

observeFavouritesQuickActionsDuration :: (MonadIO m, MonadMask m) => m a -> m a
observeFavouritesQuickActionsDuration act = do
  start <- liftIO $ getTime Monotonic
  res <- handleAll (return . Left) $ Right <$> act
  end <- liftIO $ getTime Monotonic

  liftIO . observe favouritesQuickActionsDuration . realToFrac $ end - start

  either throwM return res

data LoginOutcome
  = LoginSuccessful
  | LoginInvalidCredentials
  | LoginError
  deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
  deriving anyclass (Universe, Finite)
nullaryPathPiece ''LoginOutcome $ camelToPathPiece' 1

observeLoginOutcome :: MonadHandler m
                    => Text -- ^ Plugin
                    -> LoginOutcome
                    -> m ()
observeLoginOutcome plugin outcome
  = liftIO $ withLabel loginOutcomes (plugin, toPathPiece outcome) incCounter
