{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE OverloadedStrings #-}

module Hercules.Secrets
  ( SecretContext (..),
    evalCondition,
    evalConditionTrace,
  )
where

import Control.Monad.Writer qualified
import Data.Binary (Binary)
import Data.Tagged
import Data.Text qualified as T
import Hercules.Formats.Secret
import Protolude

data SecretContext = SecretContext
  { ownerName :: Text,
    repoName :: Text,
    isDefaultBranch :: Bool,
    ref :: Text
  }
  deriving (Generic, Binary, Show, Eq)

evalCondition' :: (Monad m, MonadMiniWriter [Text] m) => SecretContext -> Condition -> m Bool
evalCondition' ctx = eval
  where
    eval (Or cs) = do
      tell ["or: Entering"]
      let go [] = do
            tell ["or: Leaving (false)"]
            pure False
          go (a : as) = do
            b <- eval a
            if b
              then do
                tell ["or: Leaving (true)"]
                pure True
              else do
                unless (null as) (tell ["or: Backtracking"])
                go as
      go cs
    eval (And cs) = do
      tell ["and: Entering"]
      let go [] = do
            tell ["and: Leaving (true)"]
            pure True
          go (a : as) = do
            b <- eval a
            if b
              then go as
              else do
                tell ["and: Leaving (false)"]
                pure False
      go cs
    eval IsDefaultBranch =
      if isDefaultBranch ctx
        then pure True
        else False <$ tell ["isDefaultBranch: ref " <> show (ref ctx) <> " is not the default branch"]
    eval IsTag =
      if "refs/tags/" `T.isPrefixOf` ref ctx
        then pure True
        else False <$ tell ["isTag: ref " <> show (ref ctx) <> " is not a tag"]
    eval (IsBranch b) = do
      let expect = "refs/heads/" <> b
          actual = ref ctx
      if expect == actual
        then pure True
        else False <$ tell ["isBranch: ref " <> show actual <> " is not the desired " <> show expect]
    eval (IsRepo expect) = do
      let actual = repoName ctx
      if actual == expect
        then pure True
        else False <$ tell ["isRepo: repo " <> show actual <> " is not the desired " <> show expect]
    eval (IsOwner expect) = do
      let actual = ownerName ctx
      if actual == expect
        then pure True
        else False <$ tell ["isOwner: owner " <> show actual <> " is not the desired " <> show expect]
    eval (Hercules.Formats.Secret.Const b) = do
      tell ["const: " <> show b]
      pure b

-- This uses tagless final to derive both an efficient and a tracing function.

evalCondition :: SecretContext -> Condition -> Bool
evalCondition ctx c = unTagged (evalCondition' ctx c :: Tagged [Text] Bool)

evalConditionTrace :: SecretContext -> Condition -> ([Text], Bool)
evalConditionTrace = evalCondition'

-- | Like 'Control.Monad.Class.Writer.MonadWriter' but simpler.
class MonadMiniWriter w m | m -> w where
  tell :: w -> m ()

instance (Monoid w) => MonadMiniWriter w ((,) w) where
  tell = Control.Monad.Writer.tell

instance MonadMiniWriter w (Tagged w) where
  tell _ = pure ()
