using ZB.MOM.WW.ScadaBridge.Commons.Types;

namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;

/// <summary>
/// Interface for invoking external system HTTP APIs.
/// Implemented by ExternalSystemGateway, consumed by ScriptRuntimeContext.
/// </summary>
public interface IExternalSystemClient
{
    /// <summary>
    /// Synchronous call to an external system. All failures returned to caller.
    /// </summary>
    /// <param name="systemName">The name of the external system.</param>
    /// <param name="methodName">The name of the method to invoke.</param>
    /// <param name="parameters">Method parameters as a dictionary, or null if none.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The result of the external call.</returns>
    Task<ExternalCallResult> CallAsync(
        string systemName,
        string methodName,
        IReadOnlyDictionary<string, object?>? parameters = null,
        CancellationToken cancellationToken = default);

    /// <summary>
    /// Attempt immediate delivery; on transient failure, hand to S&amp;F engine.
    /// Permanent failures returned to caller.
    /// </summary>
    /// <param name="systemName">The name of the external system.</param>
    /// <param name="methodName">The name of the method to invoke.</param>
    /// <param name="parameters">Method parameters as a dictionary, or null if none.</param>
    /// <param name="originInstanceName">The instance name originating the call, or null.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <param name="trackedOperationId">
    /// Caller-supplied tracking id used as the
    /// store-and-forward message id so the S&amp;F retry loop can read it
    /// back via <c>StoreAndForwardMessage.Id</c> and emit per-attempt /
    /// terminal cached-call telemetry under the same id. Defaults to
    /// <c>null</c> — when omitted the S&amp;F engine mints a fresh GUID and no
    /// telemetry is correlated (the legacy behaviour older callers rely
    /// on).
    /// </param>
    /// <param name="executionId">
    /// The originating script execution's
    /// per-run correlation id. When the call is buffered on a transient
    /// failure this is threaded onto the S&amp;F message so the retry-loop
    /// cached-call audit rows carry it. <c>null</c> when not threaded.
    /// </param>
    /// <param name="sourceScript">
    /// The originating script identifier,
    /// threaded onto the buffered S&amp;F message alongside
    /// <paramref name="executionId"/>. <c>null</c> when not known.
    /// </param>
    /// <param name="parentExecutionId">
    /// The <c>ExecutionId</c> of the
    /// inbound-API request that spawned the originating script execution.
    /// When the call is buffered on a transient failure this is threaded onto
    /// the S&amp;F message alongside <paramref name="executionId"/> so the
    /// retry-loop cached-call audit rows carry it. <c>null</c> for a non-routed
    /// run.
    /// </param>
    /// <returns>The result of the external call.</returns>
    Task<ExternalCallResult> CachedCallAsync(
        string systemName,
        string methodName,
        IReadOnlyDictionary<string, object?>? parameters = null,
        string? originInstanceName = null,
        CancellationToken cancellationToken = default,
        TrackedOperationId? trackedOperationId = null,
        Guid? executionId = null,
        string? sourceScript = null,
        Guid? parentExecutionId = null);
}

/// <summary>
/// Result of an external system call.
/// </summary>
public record ExternalCallResult(
    bool Success,
    string? ResponseJson,
    string? ErrorMessage,
    bool WasBuffered = false)
{
    // Thread-safe lazy parse — `Lazy<T>` with the default
    // `LazyThreadSafetyMode.ExecutionAndPublication` guarantees that two
    // concurrent readers see the same `DynamicJsonElement` instance, the
    // `JsonDocument.Parse` runs at most once, and the published value is
    // safe under .NET's memory model. The closure captures `ResponseJson`
    // by reference to the property — the record's positional property is
    // an init-only field set in the constructor, so the snapshot read at
    // first-access time is stable for the lifetime of the result.
    private readonly Lazy<dynamic?> _response = new(() =>
        string.IsNullOrEmpty(ResponseJson)
            ? null
            : new DynamicJsonElement(System.Text.Json.JsonDocument.Parse(ResponseJson).RootElement));

    /// <summary>
    /// Parsed response as a dynamic object. Returns null if ResponseJson is null or empty.
    /// Access properties directly: result.Response.result, result.Response.items[0].name, etc.
    /// Thread-safe: concurrent readers share a single parsed instance.
    /// </summary>
    public dynamic? Response => _response.Value;
}
