using Serilog;  // resolves Serilog.ILogger explicitly in signatures
using Serilog.Core;
using Serilog.Events;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Scripting;

namespace ZB.MOM.WW.OtOpcUa.Admin.Services;

/// <summary>
///     Dry-run harness for the Phase 7 scripting UI. Takes a script + a synthetic input
///     map + evaluates once, returns the output (or rejection / exception) plus any
///     logger emissions the script produced. Per Phase 7 plan decision #22: only inputs
///     the <see cref="DependencyExtractor"/> identified can be supplied, so a dependency
///     the harness can't prove statically surfaces as a harness error, not a runtime
///     surprise later.
/// </summary>
public sealed class ScriptTestHarnessService
{
    /// <summary>
    ///     Evaluate <paramref name="source"/> as a virtual-tag script (return value is the
    ///     tag's new value). <paramref name="inputs"/> supplies synthetic
    ///     <see cref="DataValueSnapshot"/>s for every path the extractor found.
    /// </summary>
    public async Task<ScriptTestResult> RunVirtualTagAsync(
        string source, IDictionary<string, DataValueSnapshot> inputs, CancellationToken ct)
    {
        var deps = DependencyExtractor.Extract(source);
        if (!deps.IsValid)
            return ScriptTestResult.DependencyRejections(deps.Rejections);

        var missing = deps.Reads.Where(r => !inputs.ContainsKey(r)).ToArray();
        if (missing.Length > 0)
            return ScriptTestResult.MissingInputs(missing);

        var extra = inputs.Keys.Where(k => !deps.Reads.Contains(k)).ToArray();
        if (extra.Length > 0)
            return ScriptTestResult.UnknownInputs(extra);

        ScriptEvaluator<HarnessVirtualTagContext, object?> evaluator;
        try
        {
            evaluator = ScriptEvaluator<HarnessVirtualTagContext, object?>.Compile(source);
        }
        catch (Exception compileEx)
        {
            return ScriptTestResult.Threw(compileEx.Message, []);
        }
        var capturing = new CapturingSink();
        var logger = new LoggerConfiguration().MinimumLevel.Verbose().WriteTo.Sink(capturing).CreateLogger();
        var ctx = new HarnessVirtualTagContext(inputs, logger);

        try
        {
            var result = await evaluator.RunAsync(ctx, ct);
            return ScriptTestResult.Ok(result, ctx.Writes, capturing.Events);
        }
        catch (Exception ex)
        {
            return ScriptTestResult.Threw(ex.Message, capturing.Events);
        }
    }

    // Public so Roslyn's script compilation can reference the context type through the
    // ScriptGlobals<T> surface. The harness instantiates this directly; operators never see it.
    public sealed class HarnessVirtualTagContext(
        IDictionary<string, DataValueSnapshot> inputs, Serilog.ILogger logger) : ScriptContext
    {
        public Dictionary<string, object?> Writes { get; } = [];
        public override DataValueSnapshot GetTag(string path) =>
            inputs.TryGetValue(path, out var v)
                ? v
                : new DataValueSnapshot(null, Ua.StatusCodes.BadNotFound, null, DateTime.UtcNow);
        public override void SetVirtualTag(string path, object? value) => Writes[path] = value;
        public override DateTime Now => DateTime.UtcNow;
        public override Serilog.ILogger Logger => logger;
    }

    private sealed class CapturingSink : ILogEventSink
    {
        public List<LogEvent> Events { get; } = [];
        public void Emit(LogEvent e) => Events.Add(e);
    }
}

/// <summary>Harness outcome: outputs, write-set, logger events, or a rejection/throw reason.</summary>
public sealed record ScriptTestResult(
    ScriptTestOutcome Outcome,
    object? Output,
    IReadOnlyDictionary<string, object?> Writes,
    IReadOnlyList<LogEvent> LogEvents,
    IReadOnlyList<string> Errors)
{
    public static ScriptTestResult Ok(object? output, IReadOnlyDictionary<string, object?> writes, IReadOnlyList<LogEvent> logs) =>
        new(ScriptTestOutcome.Success, output, writes, logs, []);
    public static ScriptTestResult Threw(string reason, IReadOnlyList<LogEvent> logs) =>
        new(ScriptTestOutcome.Threw, null, new Dictionary<string, object?>(), logs, [reason]);
    public static ScriptTestResult DependencyRejections(IReadOnlyList<DependencyRejection> rejs) =>
        new(ScriptTestOutcome.DependencyRejected, null, new Dictionary<string, object?>(), [],
            rejs.Select(r => r.Message).ToArray());
    public static ScriptTestResult MissingInputs(string[] paths) =>
        new(ScriptTestOutcome.MissingInputs, null, new Dictionary<string, object?>(), [],
            paths.Select(p => $"Missing synthetic input: {p}").ToArray());
    public static ScriptTestResult UnknownInputs(string[] paths) =>
        new(ScriptTestOutcome.UnknownInputs, null, new Dictionary<string, object?>(), [],
            paths.Select(p => $"Input '{p}' is not referenced by the script — remove it").ToArray());
}

public enum ScriptTestOutcome
{
    Success,
    Threw,
    DependencyRejected,
    MissingInputs,
    UnknownInputs,
}

file static class Ua
{
    // Mirrors OPC UA StatusCodes.BadNotFound without pulling the OPC stack into Admin.
    public static class StatusCodes { public const uint BadNotFound = 0x803E0000; }
}
