using System.Collections.Generic;
using Microsoft.CodeAnalysis;

namespace AyCode.Core.Serializers.SourceGenerator;

/// <summary>
/// Build-time diagnostics for the AcBinary source generator.
///
/// <para><b>Registered diagnostics</b>:</para>
/// <list type="bullet">
///   <item><c>ACBIN001</c> — <see cref="CircularReferenceWarning"/>: detects circular type references
///   among <c>[AcBinarySerializable]</c> types and warns the developer to consider ref-handling mode.</item>
///   <item><c>ACBIN002</c> — <see cref="PolymorphicPropertyWithFeatureDisabledError"/>: ACCORE-BIN-I-T7K3
///   compile-time guard. Fires when a type opts out of <c>EnablePolymorphDetectFeature</c> AND still
///   declares an <c>object</c> property — the SGen-emitted writer would silently corrupt the wire.</item>
/// </list>
/// </summary>
public partial class AcBinarySourceGenerator
{
    private static readonly DiagnosticDescriptor CircularReferenceWarning = new(
        id: "ACBIN001",
        title: "Circular reference detected",
        messageFormat: "Type '{0}' participates in a circular reference chain: {1}. Consider using ReferenceHandling.OnlyId or .All to avoid exponential serialization size.",
        category: "AcBinarySerializer",
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true);

    /// <summary>
    /// ACCORE-BIN-I-T7K3 compile-time guard: a property declared as <c>System.Object</c> requires
    /// polymorphic-prefix emit (<c>ObjectWithTypeName</c>) so the deserializer can resolve the
    /// concrete runtime type. When the type opts out of the feature via
    /// <c>[AcBinarySerializable(enablePolymorphDetectFeature: false)]</c>, the prefix is suppressed
    /// and the wire silently corrupts on round-trip (FixObj slot byte against <c>typeof(object)</c>
    /// at read-time → 0-byte object wrapper → reader position drifts → downstream
    /// <c>DECIMAL_DRIFT</c> / <c>IndexOutOfRangeException</c>).
    ///
    /// Surface the misconfiguration at build time so the silent corruption is structurally
    /// impossible. Three escape hatches for the developer:
    ///   1. Enable the polymorph-detect feature on the type
    ///      (<c>[AcBinarySerializable(...enablePolymorphDetectFeature: true)]</c> — default true).
    ///   2. Change the property type to a concrete type (no polymorphism needed).
    ///   3. Mark the property with <c>[AcBinaryIgnore]</c> — ignored properties are filtered out
    ///      at property enumeration, so this diagnostic does not fire for them.
    /// </summary>
    private static readonly DiagnosticDescriptor PolymorphicPropertyWithFeatureDisabledError = new(
        id: "ACBIN002",
        title: "Polymorphic property requires EnablePolymorphDetectFeature",
        messageFormat: "Type '{0}' contains property '{1}' declared as System.Object, but EnablePolymorphDetectFeature is disabled on the type. " +
                       "The generated writer would silently corrupt the wire on round-trip. " +
                       "To fix: (1) enable EnablePolymorphDetectFeature on [AcBinarySerializable], (2) change '{1}' to a concrete type, or (3) exclude it with [AcBinaryIgnore].",
        category: "AcBinarySerializer",
        defaultSeverity: DiagnosticSeverity.Error,
        isEnabledByDefault: true);

    /// <summary>
    /// ACCORE-BIN-I-T7K3 guard: emits <see cref="PolymorphicPropertyWithFeatureDisabledError"/>
    /// (ACBIN002) for every <c>System.Object</c>-declared property on any
    /// <c>[AcBinarySerializable]</c> type whose <c>EnablePolymorphDetectFeature</c> is <c>false</c>.
    /// Per-class gating: types with the feature enabled (default) skip the check entirely; only
    /// opt-out types are scanned for misuse.
    /// </summary>
    private static void DetectAndReportPolymorphicMisuse(List<SerializableClassInfo> classes, SourceProductionContext spc)
    {
        foreach (var ci in classes)
        {
            if (ci.EnablePolymorphDetect) continue; // Feature enabled → polymorphic prefix is emitted, no misuse possible.

            foreach (var p in ci.Properties)
            {
                if (p.IsObjectDeclaredType)
                {
                    spc.ReportDiagnostic(Diagnostic.Create(
                        PolymorphicPropertyWithFeatureDisabledError, Location.None,
                        ci.ClassName, p.Name));
                }
            }
        }
    }

    /// <summary>
    /// Detects circular reference chains among [AcBinarySerializable] types at compile time
    /// and reports ACBIN001 warnings. Uses DFS with 3-color marking to find back-edges.
    /// </summary>
    private static void DetectAndReportCycles(List<SerializableClassInfo> classes, SourceProductionContext spc)
    {
        // Build lookup: WriterClassName → FullTypeName
        var writerToFull = new Dictionary<string, string>(classes.Count);
        foreach (var ci in classes)
        {
            var writerName = string.IsNullOrEmpty(ci.Namespace)
                ? $"{ci.ClassName}_GeneratedWriter"
                : $"{ci.Namespace}.{ci.ClassName}_GeneratedWriter";
            writerToFull[writerName] = ci.FullTypeName;
        }

        // Build adjacency list: FullTypeName → set of referenced FullTypeNames
        var adjacency = new Dictionary<string, HashSet<string>>(classes.Count);
        foreach (var ci in classes)
        {
            var edges = new HashSet<string>();
            foreach (var p in ci.Properties)
            {
                if (p.TypeKind == PropertyTypeKind.Complex && p.HasGeneratedWriter && p.WriterClassName != null)
                {
                    if (writerToFull.TryGetValue(p.WriterClassName, out var target))
                        edges.Add(target);
                }
                if (p.ElementKind == PropertyTypeKind.Complex && p.ElementHasGeneratedWriter && p.ElementWriterClassName != null)
                {
                    if (writerToFull.TryGetValue(p.ElementWriterClassName, out var target))
                        edges.Add(target);
                }
                if (p.DictValueKind == PropertyTypeKind.Complex && p.DictValueHasGeneratedWriter && p.DictValueWriterClassName != null)
                {
                    if (writerToFull.TryGetValue(p.DictValueWriterClassName, out var target))
                        edges.Add(target);
                }
            }
            adjacency[ci.FullTypeName] = edges;
        }

        // DFS with 3-color marking: White=0, Gray=1, Black=2
        var color = new Dictionary<string, int>(classes.Count);
        foreach (var ci in classes)
            color[ci.FullTypeName] = 0;

        var stack = new List<string>();
        var reported = new HashSet<string>();

        void Dfs(string node)
        {
            color[node] = 1; // Gray
            stack.Add(node);

            if (adjacency.TryGetValue(node, out var neighbors))
            {
                foreach (var next in neighbors)
                {
                    if (!color.TryGetValue(next, out var c)) continue;
                    if (c == 1) // Gray → back-edge = cycle
                    {
                        var cycleStart = stack.IndexOf(next);
                        var parts = new List<string>();
                        for (var i = cycleStart; i < stack.Count; i++)
                            parts.Add(ShortTypeName(stack[i]));
                        parts.Add(ShortTypeName(next)); // close the cycle

                        var cycleDesc = string.Join(" → ", parts);
                        for (var i = cycleStart; i < stack.Count; i++)
                        {
                            if (reported.Add(stack[i]))
                            {
                                spc.ReportDiagnostic(Diagnostic.Create(
                                    CircularReferenceWarning, Location.None,
                                    ShortTypeName(stack[i]), cycleDesc));
                            }
                        }
                    }
                    else if (c == 0) // White → unvisited
                    {
                        Dfs(next);
                    }
                }
            }

            stack.RemoveAt(stack.Count - 1);
            color[node] = 2; // Black
        }

        foreach (var ci in classes)
        {
            if (color[ci.FullTypeName] == 0)
                Dfs(ci.FullTypeName);
        }
    }

    private static string ShortTypeName(string fullTypeName)
    {
        var dot = fullTypeName.LastIndexOf('.');
        return dot >= 0 ? fullTypeName.Substring(dot + 1) : fullTypeName;
    }
}
