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

{- | @ecluse check-config@: run the boot's config-decidable tier and print the resolved posture,
without starting anything. It runs 'Ecluse.Composition.Plan.resolveBootPlan' once for its own pass
and once per other boot role, and prints the plan's lines, applying none of it: no socket opens, no
capability count changes, no re-exec, no cloud call. It predicts the posture from
'appliedRuntimePlan', because the checker's own process posture is not the boot's. It exits @0@ on
a valid configuration and @2@ where its own pass refuses, and a refusal only some roles earn prints
as a warning naming the command that earns it.
-}
module Ecluse.CheckConfig (runCheckConfig) where

import Data.Text.IO qualified as TIO

import Ecluse.Boot (loadBootConfig, refuseBoot, runtimeOverridesOf)
import Ecluse.Composition.BootError (renderAdvisory, renderBootErrors)
import Ecluse.Composition.Plan (
    BootInputs (BootInputs, biConfig, biDocument, biEnvVars, biFdLimit, biRuntimePlan),
    BootPlan (bpLines, bpWarnings),
    BootReport (brAdvisories, brOutcome, brProvenance),
    resolveBootPlan,
    roleRefusalWarnings,
 )
import Ecluse.Composition.Sizing (openFileSoftLimit)
import Ecluse.Composition.Types (BootRole (BootWithoutPipeline))
import Ecluse.Config (AppConfig (cfgRuntime), Config (configApp))
import Ecluse.Rts (
    appliedRuntimePlan,
    currentRtsPosture,
    readCgroupLimits,
    renderEffectivePosture,
    renderPostureWarnings,
    resolveRuntimePlan,
 )

{- | Validate the configuration and print the resolved posture. A valid configuration
returns (exit @0@) and a refused one aborts (exit @2@).
-}
runCheckConfig :: IO ()
runCheckConfig :: IO ()
runCheckConfig = do
    {- The refusal suffix carries the verdict into the boot's own typed abort, which
    'Ecluse.Internal.superviseProcess' maps to exit 2 and 'Ecluse.Startup.runWith' reports. -}
    (envVars, docBlob, config) <- (Text -> Text) -> IO ([(String, String)], Maybe ByteString, Config)
loadBootConfig (Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
"\nconfiguration: refused")
    rts <- currentRtsPosture
    cgroup <- readCgroupLimits
    fdLimit <- openFileSoftLimit
    let overrides = RuntimeSettings -> RuntimeOverrides
runtimeOverridesOf (AppConfig -> RuntimeSettings
cfgRuntime (Config -> AppConfig
configApp Config
config))
        runtimePlan = RuntimeOverrides -> CgroupLimits -> RtsPosture -> RuntimePlan
resolveRuntimePlan RuntimeOverrides
overrides CgroupLimits
cgroup RtsPosture
rts
        effective = CgroupLimits -> RuntimePlan -> RtsPosture -> EffectiveRuntimePlan
appliedRuntimePlan CgroupLimits
cgroup RuntimePlan
runtimePlan RtsPosture
rts
    -- The checker runs no mirror pipeline and prunes no store, so its own pass vets under the
    -- writing roles' severities. Every other role's verdict follows below.
    let inputs =
            BootInputs
                { biEnvVars :: [(String, String)]
biEnvVars = [(String, String)]
envVars
                , biDocument :: Maybe ByteString
biDocument = Maybe ByteString
docBlob
                , biConfig :: Config
biConfig = Config
config
                , biRuntimePlan :: EffectiveRuntimePlan
biRuntimePlan = EffectiveRuntimePlan
effective
                , biFdLimit :: Int
biFdLimit = Int
fdLimit
                }
        report = BootRole -> BootInputs -> BootReport
resolveBootPlan BootRole
BootWithoutPipeline BootInputs
inputs
    -- The boot logs these posture lines and warnings from 'Ecluse.Rts.applyRuntimePosture',
    -- which the checker never runs. They stand in that position here.
    traverse_ TIO.putStrLn (renderEffectivePosture effective)
    traverse_ warn (renderPostureWarnings effective)
    -- Printed ahead of every refusable phase, exactly where the boot logs it.
    traverse_ TIO.putStrLn (brProvenance report)
    bootPlan <- case brOutcome report of
        Left [BootError]
errs -> do
            (Advisory -> IO ()) -> [Advisory] -> IO ()
forall (t :: * -> *) (f :: * -> *) a b.
(Foldable t, Applicative f) =>
(a -> f b) -> t a -> f ()
traverse_ (Text -> IO ()
warn (Text -> IO ()) -> (Advisory -> Text) -> Advisory -> IO ()
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Advisory -> Text
renderAdvisory) (BootReport -> [Advisory]
brAdvisories BootReport
report)
            Text -> IO BootPlan
forall a. Text -> IO a
refuseBoot ([BootError] -> Text
renderBootErrors [BootError]
errs Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
"\nconfiguration: refused")
        Right BootPlan
plan -> BootPlan -> IO BootPlan
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure BootPlan
plan
    traverse_ TIO.putStrLn (bpLines bootPlan)
    traverse_ warn (bpWarnings bootPlan)
    traverse_ (warn . renderAdvisory) (brAdvisories report)
    -- A configuration one role refuses and another boots is a normal deployment, so the other
    -- roles' refusals report as warnings rather than deciding the exit status.
    traverse_ warn (roleRefusalWarnings BootWithoutPipeline inputs)
    TIO.putStrLn "configuration: valid"
  where
    -- Standard output carries no severity field, so the prefix stands in for the boot's
    -- katip WarningS.
    warn :: Text -> IO ()
    warn :: Text -> IO ()
warn = Text -> IO ()
TIO.putStrLn (Text -> IO ()) -> (Text -> Text) -> Text -> IO ()
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Text
"warning: " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<>)