-- SPDX-FileCopyrightText: 2026 Alexandra de Wit
--
-- SPDX-License-Identifier: MIT

{- | What each half of the demand-driven mirror pipeline needs of the process running it, the
mirror-write credential it mints, and the boot refusal a role earns against the resolved mirror
runtime.

Mirroring has a producer half (the serve path enqueues a job for every artifact it admits) and
a consumer half (the worker drains the queue and publishes). One process runs both, or a
split deployment runs each in its own fleet so the front door and the worker scale apart.
The split only works over a durable queue, which is what 'mirrorRoleRefusal' decides.
-}
module Ecluse.Composition.MirrorRole (
    spawnsWorker,
    enqueuesJobs,
    MirrorMintPlan (..),
    mirrorMintPlan,
    mirrorRoleRefusal,
) where

import Ecluse.Composition.BootError (BootError (MirrorRoleWithoutMirroring, SplitRoleNeedsDurableQueue))
import Ecluse.Composition.MirrorQueue (
    MirrorQueuePlan (MemoryBackend, SqsBackend),
    MirrorRuntimePlan (MirrorWith, NoMirroring),
 )
import Ecluse.Composition.Types (MirrorRole (MirrorOnly, ServeAndMirror, ServeOnly), roleInvocation)

-- Whether this role would run the mirror worker, given a runtime that has one to run.
runsWorker :: MirrorRole -> Bool
runsWorker :: MirrorRole -> Bool
runsWorker = \case
    MirrorRole
ServeAndMirror -> Bool
True
    MirrorRole
ServeOnly -> Bool
False
    MirrorRole
MirrorOnly -> Bool
True

{- | Whether this process runs the mirror worker: the role wants one and a mount declares a
mirror target. Under 'NoMirroring' a spawned loop would poll an inert queue with nothing to pace it.
-}
spawnsWorker :: MirrorRole -> MirrorRuntimePlan -> Bool
spawnsWorker :: MirrorRole -> MirrorRuntimePlan -> Bool
spawnsWorker MirrorRole
role = \case
    MirrorRuntimePlan
NoMirroring -> Bool
False
    MirrorWith MirrorQueuePlan
_ -> MirrorRole -> Bool
runsWorker MirrorRole
role

{- | Whether this role serves requests, and so enqueues a mirror job for each version it
admits. The composition root reads it to decide whether to build the enqueue buffer.
-}
enqueuesJobs :: MirrorRole -> Bool
enqueuesJobs :: MirrorRole -> Bool
enqueuesJobs = \case
    MirrorRole
ServeAndMirror -> Bool
True
    MirrorRole
ServeOnly -> Bool
True
    MirrorRole
MirrorOnly -> Bool
False

{- | Whether a role's boot mints each mirrored mount's write credential. Only a role that writes
to the mirror store holds that identity.
-}
data MirrorMintPlan
    = -- | @ecluse proxy@ and @ecluse mirror@: mint at boot, so a bad identity refuses there.
      MintMirrorWrite
    | -- | @ecluse proxy --no-worker@: the front door writes nothing, so it needs no write identity.
      SkipMirrorWrite
    deriving stock (MirrorMintPlan -> MirrorMintPlan -> Bool
(MirrorMintPlan -> MirrorMintPlan -> Bool)
-> (MirrorMintPlan -> MirrorMintPlan -> Bool) -> Eq MirrorMintPlan
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: MirrorMintPlan -> MirrorMintPlan -> Bool
== :: MirrorMintPlan -> MirrorMintPlan -> Bool
$c/= :: MirrorMintPlan -> MirrorMintPlan -> Bool
/= :: MirrorMintPlan -> MirrorMintPlan -> Bool
Eq, Int -> MirrorMintPlan -> ShowS
[MirrorMintPlan] -> ShowS
MirrorMintPlan -> String
(Int -> MirrorMintPlan -> ShowS)
-> (MirrorMintPlan -> String)
-> ([MirrorMintPlan] -> ShowS)
-> Show MirrorMintPlan
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> MirrorMintPlan -> ShowS
showsPrec :: Int -> MirrorMintPlan -> ShowS
$cshow :: MirrorMintPlan -> String
show :: MirrorMintPlan -> String
$cshowList :: [MirrorMintPlan] -> ShowS
showList :: [MirrorMintPlan] -> ShowS
Show)

-- | The worker is the only writer, so the roles that run one are the roles that mint.
mirrorMintPlan :: MirrorRole -> MirrorMintPlan
mirrorMintPlan :: MirrorRole -> MirrorMintPlan
mirrorMintPlan MirrorRole
role
    | MirrorRole -> Bool
runsWorker MirrorRole
role = MirrorMintPlan
MintMirrorWrite
    | Bool
otherwise = MirrorMintPlan
SkipMirrorWrite

{- | Refuse a role the resolved mirror runtime cannot serve. A split role over the bounded
in-memory queue would strand every job, because that queue lives inside one process.
-}
mirrorRoleRefusal :: MirrorRole -> MirrorRuntimePlan -> Either [BootError] ()
mirrorRoleRefusal :: MirrorRole -> MirrorRuntimePlan -> Either [BootError] ()
mirrorRoleRefusal MirrorRole
role MirrorRuntimePlan
plan = case (MirrorRole
role, MirrorRuntimePlan
plan) of
    (MirrorRole
ServeAndMirror, MirrorRuntimePlan
_) -> () -> Either [BootError] ()
forall a b. b -> Either a b
Right ()
    (MirrorRole
ServeOnly, MirrorRuntimePlan
NoMirroring) -> () -> Either [BootError] ()
forall a b. b -> Either a b
Right ()
    (MirrorRole
ServeOnly, MirrorWith SqsBackend{}) -> () -> Either [BootError] ()
forall a b. b -> Either a b
Right ()
    (MirrorRole
ServeOnly, MirrorWith MirrorQueuePlan
MemoryBackend) -> [BootError] -> Either [BootError] ()
forall a b. a -> Either a b
Left [Text -> BootError
SplitRoleNeedsDurableQueue (MirrorRole -> Text
roleInvocation MirrorRole
role)]
    (MirrorRole
MirrorOnly, MirrorWith SqsBackend{}) -> () -> Either [BootError] ()
forall a b. b -> Either a b
Right ()
    (MirrorRole
MirrorOnly, MirrorWith MirrorQueuePlan
MemoryBackend) -> [BootError] -> Either [BootError] ()
forall a b. a -> Either a b
Left [Text -> BootError
SplitRoleNeedsDurableQueue (MirrorRole -> Text
roleInvocation MirrorRole
role)]
    (MirrorRole
MirrorOnly, MirrorRuntimePlan
NoMirroring) -> [BootError] -> Either [BootError] ()
forall a b. a -> Either a b
Left [BootError
MirrorRoleWithoutMirroring]