namespace ZB.MOM.WW.OtOpcUa.Runtime.DeploymentCache;

/// <summary>
///     Node-local durable cache of the deployment artifact this node last applied.
/// </summary>
/// <remarks>
///     <para>
///         Exists so a driver node can boot into its last-known-good address space when the central
///         configuration database is unreachable. Without it, a control-plane outage that outlives a
///         node restart leaves that node with no address space at all — the plant loses visibility
///         for a reason that has nothing to do with the plant.
///     </para>
/// </remarks>
public interface IDeploymentArtifactCache
{
    /// <summary>
    ///     Persists <paramref name="artifact"/> as the cluster's current deployment.
    /// </summary>
    /// <remarks>
    ///     <para>
    ///         Idempotent per <paramref name="deploymentId"/>: re-storing the same deployment
    ///         replaces its chunks rather than accumulating orphans. Retention is bounded to the two
    ///         newest deployments per cluster, so a long-lived node cannot grow its local database
    ///         without limit.
    ///     </para>
    /// </remarks>
    Task StoreAsync(string clusterId, string deploymentId, string revisionHash,
                    byte[] artifact, CancellationToken ct = default);

    /// <summary>
    ///     Reads the cached artifact for <paramref name="clusterId"/>, or <see langword="null"/>
    ///     when nothing is cached or the cached bytes fail their integrity check.
    /// </summary>
    /// <remarks>
    ///     <para>
    ///         A failed integrity check is deliberately indistinguishable from a miss. Booting a
    ///         plant from a truncated address space is strictly worse than booting from none: the
    ///         missing half looks like a deliberate configuration rather than an error.
    ///     </para>
    /// </remarks>
    Task<CachedDeploymentArtifact?> GetCurrentAsync(string clusterId, CancellationToken ct = default);

    /// <summary>
    ///     Reads the single cached pointer without knowing the cluster id.
    /// </summary>
    /// <remarks>
    ///     <para>
    ///         The boot seam needs this. A node's cluster id is only derivable from an artifact it
    ///         has already loaded, or from the central database — which is precisely what is
    ///         unreachable in the scenario this cache exists to survive. So the cold path reads the
    ///         newest pointer unkeyed.
    ///     </para>
    ///     <para>
    ///         More than one pointer row means the node was re-homed between clusters. The newest
    ///         wins, but the choice is logged as a warning naming both clusters — a node silently
    ///         booting a neighbouring cluster's configuration is exactly the failure that must never
    ///         be quiet.
    ///     </para>
    /// </remarks>
    Task<CachedDeploymentArtifact?> GetCurrentUnkeyedAsync(CancellationToken ct = default);
}

/// <summary>
///     A deployment artifact recovered from the node-local cache, with the metadata needed to decide
///     whether it is still current once the control plane is reachable again.
/// </summary>
public sealed record CachedDeploymentArtifact(
    string DeploymentId, string RevisionHash, byte[] Artifact, DateTimeOffset AppliedAtUtc);
