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>
///     or <c>CapabilityInvoker.ExecuteWriteAsync</c> lambda. The wrapping is what gives us
///     per-host breaker isolation, retry semantics, in-flight-depth accounting, and alarm-ack
///     idempotence guards — raw calls bypass all of that.
/// </summary>
/// <remarks>
///     <para>
///         Guarded-call detection uses Roslyn's semantic model: the analyzer compares the invoked
///         method's containing-type symbol (and every implemented interface symbol) against the
///         <c>GuardedInterfaceTypes</c> set resolved once per compilation, then verifies the
///         method either is the interface member or is the concrete implementation discovered via
///         <see cref="ITypeSymbol.FindImplementationForInterfaceMember"/>. Matching by symbol
///         identity means a driver with an unusually-named method implementing
///         <c>IReadable.ReadAsync</c> still trips the rule.
///     </para>
///     <para>
///         <b>Default-interface-method handling:</b> <c>IHistoryProvider.ReadAtTimeAsync</c> and
///         <c>IHistoryProvider.ReadEventsAsync</c> ship as DIM bodies. When a driver inherits the
///         DIM (no override), <c>FindImplementationForInterfaceMember</c> returns the interface's
///         own method symbol, which still equals <paramref name="method"/> for an interface-typed
///         receiver. When a driver overrides the DIM, the override symbol equals
///         <paramref name="method"/> directly. Both paths are covered.
///     </para>
///     <para>
///         <b>Wrapper-lambda detection:</b> the analyzer walks up the syntax tree from the call
///         site and looks for an enclosing <c>InvocationExpressionSyntax</c> bound to
///         <c>CapabilityInvoker.ExecuteAsync</c> / <c>CapabilityInvoker.ExecuteWriteAsync</c>
///         whose argument list contains a lambda whose span contains the call. The match keys on
///         both the containing-type symbol AND the method name — a future overload on
///         <c>CapabilityInvoker</c> that took a non-<c>callSite</c> lambda (predicate, selector,
///         etc.) would still suppress the diagnostic; matching the method name is the strongest
///         containment check the analyzer can make without doing parameter-position binding on
///         every invocation in the IDE hot path. The known limitation is that any lambda
///         argument to <c>ExecuteAsync</c> / <c>ExecuteWriteAsync</c> suppresses the diagnostic
///         even if it is not bound to the <c>callSite</c> parameter — today both overload pairs
///         have exactly one lambda parameter (<c>callSite</c>), so the limitation is theoretical.
///     </para>
///     <para>
///         <b>Why <c>AlarmSurfaceInvoker</c> is NOT a wrapper home:</b> its public methods take
///         <c>IReadOnlyList&lt;...&gt;</c> / <c>IAlarmSubscriptionHandle</c> — no lambda
///         arguments — so no call to it can ever satisfy the lambda-containment check. Calls
///         inside <c>AlarmSurfaceInvoker</c>'s own implementation are covered transitively
///         because the surface routes through the inner <c>CapabilityInvoker.ExecuteAsync</c>
///         lambda.
///     </para>
///     <para>
///         The rule does NOT enforce that the capability argument matches the method (e.g.
///         <c>ReadAsync</c> wrapped in <c>ExecuteAsync(DriverCapability.Write, ...)</c> still
///         passes) — that would require flow analysis beyond single-expression scope.
///     </para>
/// </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[] GuardedInterfaceMetadataNames =
    [
        "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-method (containing-type metadata name, method name) pairs. Only a lambda bound
    ///     to one of these specific methods is treated as a valid wrapper home; other lambdas on
    ///     the same type (e.g. future predicate/selector overloads) do not suppress the diagnostic
    ///     unless they take a lambda in an argument position. <c>AlarmSurfaceInvoker</c> is not in
    ///     this list because none of its public methods accept lambda arguments — calls inside its
    ///     own implementation are covered transitively by the <c>CapabilityInvoker.ExecuteAsync</c>
    ///     match.
    /// </summary>
    private static readonly (string TypeMetadataName, string MethodName)[] WrapperMethodKeys =
    [
        ("ZB.MOM.WW.OtOpcUa.Core.Resilience.CapabilityInvoker", "ExecuteAsync"),
        ("ZB.MOM.WW.OtOpcUa.Core.Resilience.CapabilityInvoker", "ExecuteWriteAsync"),
        // The Polly-free Runtime dispatch layer wraps calls through the IDriverCapabilityInvoker
        // seam (it cannot reference the concrete CapabilityInvoker in Core). An interface-typed
        // invoker call's containing type is the interface, so it must be a wrapper home too — else
        // correctly-wrapped dispatch-layer calls would still trip OTOPCUA0001.
        ("ZB.MOM.WW.OtOpcUa.Core.Abstractions.IDriverCapabilityInvoker", "ExecuteAsync"),
        ("ZB.MOM.WW.OtOpcUa.Core.Abstractions.IDriverCapabilityInvoker", "ExecuteWriteAsync"),
    ];

    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. Without the wrapping, Phase 6.1 resilience (retry, breaker, 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.");

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

    /// <inheritdoc />
    public override void Initialize(AnalysisContext context)
    {
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
        context.EnableConcurrentExecution();
        context.RegisterCompilationStartAction(OnCompilationStart);
    }

    private static void OnCompilationStart(CompilationStartAnalysisContext context)
    {
        // Resolve the guarded interfaces and wrapper types ONCE per compilation. If none of the
        // guarded interfaces are referenced (e.g. an unrelated project building against this
        // analyzer package), there's nothing to flag — register no further callbacks so the
        // analyzer is a true no-op on cold compilations.
        var guardedSet = new HashSet<INamedTypeSymbol>(SymbolEqualityComparer.Default);
        foreach (var metadataName in GuardedInterfaceMetadataNames)
        {
            var sym = context.Compilation.GetTypeByMetadataName(metadataName);
            if (sym is not null) guardedSet.Add(sym);
        }
        if (guardedSet.Count == 0) return;

        // Wrapper methods: build a lookup keyed by (containing-type-symbol, method-name). Matching
        // both means a future predicate/selector overload on CapabilityInvoker doesn't silently
        // widen the suppression scope.
        var wrapperMethodsByType = new Dictionary<INamedTypeSymbol, HashSet<string>>(SymbolEqualityComparer.Default);
        foreach (var (typeMetadataName, methodName) in WrapperMethodKeys)
        {
            var typeSym = context.Compilation.GetTypeByMetadataName(typeMetadataName);
            if (typeSym is null) continue;
            if (!wrapperMethodsByType.TryGetValue(typeSym, out var names))
            {
                names = new HashSet<string>();
                wrapperMethodsByType[typeSym] = names;
            }
            names.Add(methodName);
        }

        var state = new AnalyzerState(guardedSet, wrapperMethodsByType);
        context.RegisterOperationAction(ctx => AnalyzeInvocation(ctx, state), OperationKind.Invocation);
    }

    private static void AnalyzeInvocation(OperationAnalysisContext context, AnalyzerState state)
    {
        var invocation = (IInvocationOperation)context.Operation;

        // Defensive: a null SemanticModel means the IDE asked for analysis on a half-bound tree.
        // We can't tell whether the call is inside a wrapper lambda without it, so SKIP rather
        // than report — emitting a diagnostic in that state would be a false positive on code
        // that is in fact correctly wrapped.
        if (context.Operation.SemanticModel is null) return;

        var method = invocation.TargetMethod;
        if (method?.ContainingType is null) return;
        if (method.ReturnType is null) return;

        // 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, state.GuardedInterfaces)) return;
        if (IsInsideWrapperLambda(invocation.Syntax, context.Operation.SemanticModel, state.WrapperMethodsByType, 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, HashSet<INamedTypeSymbol> guarded)
    {
        // The method may be defined directly on a guarded interface (interface-typed receiver) or
        // be a concrete implementation. Walk every interface the containing type implements (plus
        // the containing type itself, to catch the interface-typed-receiver case) and look for a
        // member whose implementation in the containing type equals `method`, OR a member whose
        // original definition equals `method.OriginalDefinition` (covers the DIM inherit case
        // where FindImplementationForInterfaceMember returns the interface method itself).
        var containingType = method.ContainingType;
        foreach (var iface in containingType.AllInterfaces)
        {
            if (!guarded.Contains(iface.OriginalDefinition)) continue;
            if (MethodImplementsInterfaceMember(method, containingType, iface)) return true;
        }
        // Interface-typed receiver: containingType *is* the guarded interface.
        if (guarded.Contains(containingType.OriginalDefinition))
        {
            if (MethodImplementsInterfaceMember(method, containingType, containingType)) return true;
        }
        return false;
    }

    private static bool MethodImplementsInterfaceMember(IMethodSymbol method, INamedTypeSymbol containingType, INamedTypeSymbol iface)
    {
        foreach (var member in iface.GetMembers())
        {
            if (member is not IMethodSymbol memberMethod) continue;
            var impl = containingType.FindImplementationForInterfaceMember(memberMethod);
            if (SymbolEqualityComparer.Default.Equals(impl, method)) return true;
            if (SymbolEqualityComparer.Default.Equals(method.OriginalDefinition, memberMethod)) return true;
        }
        return false;
    }

    private static bool IsInsideWrapperLambda(
        SyntaxNode startNode,
        SemanticModel semanticModel,
        Dictionary<INamedTypeSymbol, HashSet<string>> wrapperMethodsByType,
        System.Threading.CancellationToken ct)
    {
        for (var node = startNode.Parent; node is not null; node = node.Parent)
        {
            // The call must literally live inside a lambda (ParenthesizedLambda / SimpleLambda /
            // AnonymousMethod) that is an argument of one of the wrapper methods. Match both the
            // containing-type symbol AND the method name — a future overload that took a
            // predicate/selector lambda on the same type would still suppress the diagnostic IF it
            // had the same method name, but a brand-new method (e.g. a hypothetical
            // CapabilityInvoker.WithFilterAsync) would not. Today both wrapper method pairs have
            // exactly one lambda parameter (the callSite), so the limitation is theoretical.
            if (node is not InvocationExpressionSyntax outer) continue;

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

            var containingDef = sym.ContainingType.OriginalDefinition;
            if (!wrapperMethodsByType.TryGetValue(containingDef, out var methodNames)) continue;
            if (!methodNames.Contains(sym.Name)) continue;

            // The call is wrapped IFF 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;
    }

    /// <summary>Holds analyzer state for a compilation, including resolved guarded interfaces and wrapper methods.</summary>
    private sealed class AnalyzerState
    {
        /// <summary>Gets the set of guarded interface types resolved from the compilation.</summary>
        public HashSet<INamedTypeSymbol> GuardedInterfaces { get; }
        /// <summary>Gets the mapping of wrapper-method types to method names.</summary>
        public Dictionary<INamedTypeSymbol, HashSet<string>> WrapperMethodsByType { get; }

        /// <summary>Initializes a new analyzer state with guarded interfaces and wrapper methods.</summary>
        /// <param name="guardedInterfaces">The set of guarded interface symbols.</param>
        /// <param name="wrapperMethodsByType">The wrapper method lookup dictionary.</param>
        public AnalyzerState(
            HashSet<INamedTypeSymbol> guardedInterfaces,
            Dictionary<INamedTypeSymbol, HashSet<string>> wrapperMethodsByType)
        {
            GuardedInterfaces = guardedInterfaces;
            WrapperMethodsByType = wrapperMethodsByType;
        }
    }
}
