using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace ZB.MOM.WW.OtOpcUa.Analyzers;

/// <summary>
///     Diagnostic analyzer that flags direct invocations of Phase 6.1-wrapped driver-capability
///     methods when the call is NOT already running inside a <c>CapabilityInvoker.ExecuteAsync</c>,
///     <c>CapabilityInvoker.ExecuteWriteAsync</c>, or <c>AlarmSurfaceInvoker.*Async</c> lambda.
///     The wrapping is what gives us per-host breaker isolation, retry semantics, bulkhead-depth
///     accounting, and alarm-ack idempotence guards — raw calls bypass all of that.
/// </summary>
/// <remarks>
///     The analyzer matches by receiver-interface identity using Roslyn's semantic model, not by
///     method name, so a driver with an unusually-named method implementing <c>IReadable.ReadAsync</c>
///     still trips the rule. Lambda-context detection walks up the syntax tree from the call site
///     + checks whether any enclosing <c>InvocationExpressionSyntax</c> targets a member whose
///     containing type is <c>CapabilityInvoker</c> or <c>AlarmSurfaceInvoker</c>. The rule is
///     intentionally narrow: it does NOT try to enforce the capability argument matches the
///     method (e.g. ReadAsync wrapped in <c>ExecuteAsync(DriverCapability.Write, ...)</c> still
///     passes) — that'd require flow analysis beyond single-expression scope.
/// </remarks>
[DiagnosticAnalyzer(Microsoft.CodeAnalysis.LanguageNames.CSharp)]
public sealed class UnwrappedCapabilityCallAnalyzer : DiagnosticAnalyzer
{
    public const string DiagnosticId = "OTOPCUA0001";

    /// <summary>Interfaces whose methods must be called through the capability invoker.</summary>
    private static readonly string[] GuardedInterfaces =
    [
        "ZB.MOM.WW.OtOpcUa.Core.Abstractions.IReadable",
        "ZB.MOM.WW.OtOpcUa.Core.Abstractions.IWritable",
        "ZB.MOM.WW.OtOpcUa.Core.Abstractions.ITagDiscovery",
        "ZB.MOM.WW.OtOpcUa.Core.Abstractions.ISubscribable",
        "ZB.MOM.WW.OtOpcUa.Core.Abstractions.IHostConnectivityProbe",
        "ZB.MOM.WW.OtOpcUa.Core.Abstractions.IAlarmSource",
        "ZB.MOM.WW.OtOpcUa.Core.Abstractions.IHistoryProvider",
    ];

    /// <summary>Wrapper types whose lambda arguments are the allowed home for guarded calls.</summary>
    private static readonly string[] WrapperTypes =
    [
        "ZB.MOM.WW.OtOpcUa.Core.Resilience.CapabilityInvoker",
        "ZB.MOM.WW.OtOpcUa.Core.Resilience.AlarmSurfaceInvoker",
    ];

    private static readonly DiagnosticDescriptor Rule = new(
        id: DiagnosticId,
        title: "Driver capability call must be wrapped in CapabilityInvoker",
        messageFormat: "Call to '{0}' is not wrapped in CapabilityInvoker.ExecuteAsync / ExecuteWriteAsync / AlarmSurfaceInvoker.*. Without the wrapping, Phase 6.1 resilience (retry, breaker, bulkhead, tracker telemetry) is bypassed for this call.",
        category: "OtOpcUa.Resilience",
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true,
        description: "Phase 6.1 Stream A requires every IReadable/IWritable/ITagDiscovery/ISubscribable/IHostConnectivityProbe/IAlarmSource/IHistoryProvider call to route through the shared Polly pipeline. Direct calls skip the pipeline + lose per-host isolation, retry semantics, and telemetry. If the caller is Core/Server/Driver dispatch code, wrap the call in CapabilityInvoker.ExecuteAsync. If the caller is a unit test invoking the driver directly to test its wire-level behavior, either suppress with a pragma or move the suppression into a NoWarn for the test project.");

    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Rule);

    public override void Initialize(AnalysisContext context)
    {
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
        context.EnableConcurrentExecution();
        context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Invocation);
    }

    private static void AnalyzeInvocation(OperationAnalysisContext context)
    {
        var invocation = (Microsoft.CodeAnalysis.Operations.IInvocationOperation)context.Operation;
        var method = invocation.TargetMethod;

        // Narrow the rule to async wire calls. Synchronous accessors like
        // IHostConnectivityProbe.GetHostStatuses() are pure in-memory snapshots + would never
        // benefit from the Polly pipeline; flagging them just creates false-positives.
        if (!IsAsyncReturningType(method.ReturnType)) return;
        if (!ImplementsGuardedInterface(method)) return;
        if (IsInsideWrapperLambda(invocation.Syntax, context.Operation.SemanticModel, context.CancellationToken)) return;

        var diag = Diagnostic.Create(Rule, invocation.Syntax.GetLocation(), $"{method.ContainingType.Name}.{method.Name}");
        context.ReportDiagnostic(diag);
    }

    private static bool IsAsyncReturningType(ITypeSymbol type)
    {
        var name = type.OriginalDefinition.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
        return name is "global::System.Threading.Tasks.Task"
            or "global::System.Threading.Tasks.Task<TResult>"
            or "global::System.Threading.Tasks.ValueTask"
            or "global::System.Threading.Tasks.ValueTask<TResult>";
    }

    private static bool ImplementsGuardedInterface(IMethodSymbol method)
    {
        foreach (var iface in method.ContainingType.AllInterfaces.Concat(new[] { method.ContainingType }))
        {
            var ifaceFqn = iface.OriginalDefinition.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
                .Replace("global::", string.Empty);
            if (!GuardedInterfaces.Contains(ifaceFqn)) continue;

            foreach (var member in iface.GetMembers().OfType<IMethodSymbol>())
            {
                var impl = method.ContainingType.FindImplementationForInterfaceMember(member);
                if (SymbolEqualityComparer.Default.Equals(impl, method) ||
                    SymbolEqualityComparer.Default.Equals(method.OriginalDefinition, member))
                    return true;
            }
        }
        return false;
    }

    private static bool IsInsideWrapperLambda(SyntaxNode startNode, SemanticModel? semanticModel, System.Threading.CancellationToken ct)
    {
        if (semanticModel is null) return false;

        for (var node = startNode.Parent; node is not null; node = node.Parent)
        {
            // We only care about an enclosing invocation — the call we're auditing must literally
            // live inside a lambda (ParenthesizedLambda / SimpleLambda / AnonymousMethod) that is
            // an argument of a CapabilityInvoker.Execute* / AlarmSurfaceInvoker.* call.
            if (node is not InvocationExpressionSyntax outer) continue;

            var sym = semanticModel.GetSymbolInfo(outer, ct).Symbol as IMethodSymbol;
            if (sym is null) continue;

            var outerTypeFqn = sym.ContainingType.OriginalDefinition.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
                .Replace("global::", string.Empty);
            if (!WrapperTypes.Contains(outerTypeFqn)) continue;

            // The call is wrapped IFF our startNode is transitively inside one of the outer
            // call's argument lambdas. Walk the outer invocation's argument list + check whether
            // any lambda body contains the startNode's position.
            foreach (var arg in outer.ArgumentList.Arguments)
            {
                if (arg.Expression is not AnonymousFunctionExpressionSyntax lambda) continue;
                if (lambda.Span.Contains(startNode.Span)) return true;
            }
        }
        return false;
    }
}
