using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace NatsNet.PortTracker.Audit;

/// <summary>
/// Parses .cs files using Roslyn syntax trees and builds a lookup index
/// of (className, memberName) -> list of MethodInfo.
/// </summary>
public sealed class SourceIndexer
{
    public record MethodInfo(
        string FilePath,
        int LineNumber,
        int BodyLineCount,
        bool IsStub,
        bool IsPartial,
        int StatementCount);

    // Key: (className lowercase, memberName lowercase)
    private readonly Dictionary<(string, string), List<MethodInfo>> _index = new();

    public int FilesIndexed { get; private set; }
    public int MethodsIndexed { get; private set; }

    /// <summary>
    /// Recursively parses all .cs files under <paramref name="sourceDir"/>
    /// (skipping obj/ and bin/) and populates the index.
    /// </summary>
    public void IndexDirectory(string sourceDir)
    {
        var files = Directory.EnumerateFiles(sourceDir, "*.cs", SearchOption.AllDirectories)
            .Where(f =>
            {
                var rel = Path.GetRelativePath(sourceDir, f);
                return !rel.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}")
                    && !rel.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}")
                    && !rel.StartsWith($"obj{Path.DirectorySeparatorChar}")
                    && !rel.StartsWith($"bin{Path.DirectorySeparatorChar}");
            });

        foreach (var file in files)
        {
            IndexFile(file);
            FilesIndexed++;
        }
    }

    /// <summary>
    /// Looks up all method declarations for a given class and member name.
    /// Case-insensitive. Returns empty list if not found.
    /// </summary>
    public List<MethodInfo> Lookup(string className, string memberName)
    {
        var key = (className.ToLowerInvariant(), memberName.ToLowerInvariant());
        return _index.TryGetValue(key, out var list) ? list : [];
    }

    /// <summary>
    /// Returns true if the class exists anywhere in the index (any member).
    /// </summary>
    public bool HasClass(string className)
    {
        var lower = className.ToLowerInvariant();
        return _index.Keys.Any(k => k.Item1 == lower);
    }

    private void IndexFile(string filePath)
    {
        var source = File.ReadAllText(filePath);
        var tree = CSharpSyntaxTree.ParseText(source, path: filePath);
        var root = tree.GetCompilationUnitRoot();

        foreach (var typeDecl in root.DescendantNodes().OfType<TypeDeclarationSyntax>())
        {
            var className = typeDecl.Identifier.Text.ToLowerInvariant();

            // Methods
            foreach (var method in typeDecl.Members.OfType<MethodDeclarationSyntax>())
            {
                var info = AnalyzeMethod(filePath, method.Body, method.ExpressionBody, method.GetLocation());
                AddToIndex(className, method.Identifier.Text.ToLowerInvariant(), info);
            }

            // Properties (get/set are like methods)
            foreach (var prop in typeDecl.Members.OfType<PropertyDeclarationSyntax>())
            {
                var info = AnalyzeProperty(filePath, prop);
                AddToIndex(className, prop.Identifier.Text.ToLowerInvariant(), info);
            }

            // Constructors — index as class name
            foreach (var ctor in typeDecl.Members.OfType<ConstructorDeclarationSyntax>())
            {
                var info = AnalyzeMethod(filePath, ctor.Body, ctor.ExpressionBody, ctor.GetLocation());
                AddToIndex(className, ctor.Identifier.Text.ToLowerInvariant(), info);
            }
        }
    }

    private MethodInfo AnalyzeMethod(string filePath, BlockSyntax? body, ArrowExpressionClauseSyntax? expressionBody, Location location)
    {
        var lineSpan = location.GetLineSpan();
        var lineNumber = lineSpan.StartLinePosition.Line + 1;

        if (expressionBody is not null)
        {
            // Expression-bodied: => expr;
            var isStub = IsNotImplementedExpression(expressionBody.Expression);
            return new MethodInfo(filePath, lineNumber, 1, IsStub: isStub, IsPartial: false, StatementCount: isStub ? 0 : 1);
        }

        if (body is null || body.Statements.Count == 0)
        {
            // No body or empty body
            return new MethodInfo(filePath, lineNumber, 0, IsStub: true, IsPartial: false, StatementCount: 0);
        }

        var bodyLines = body.GetLocation().GetLineSpan();
        var bodyLineCount = bodyLines.EndLinePosition.Line - bodyLines.StartLinePosition.Line - 1; // exclude braces

        var statements = body.Statements;
        var hasNotImplemented = statements.Any(s => IsNotImplementedStatement(s));
        var meaningfulCount = statements.Count(s => !IsNotImplementedStatement(s));

        // Pure stub: single throw NotImplementedException
        if (statements.Count == 1 && hasNotImplemented)
            return new MethodInfo(filePath, lineNumber, bodyLineCount, IsStub: true, IsPartial: false, StatementCount: 0);

        // Partial: has some logic AND a NotImplementedException
        if (hasNotImplemented && meaningfulCount > 0)
            return new MethodInfo(filePath, lineNumber, bodyLineCount, IsStub: false, IsPartial: true, StatementCount: meaningfulCount);

        // Real logic
        return new MethodInfo(filePath, lineNumber, bodyLineCount, IsStub: false, IsPartial: false, StatementCount: meaningfulCount);
    }

    private MethodInfo AnalyzeProperty(string filePath, PropertyDeclarationSyntax prop)
    {
        var lineSpan = prop.GetLocation().GetLineSpan();
        var lineNumber = lineSpan.StartLinePosition.Line + 1;

        // Expression-bodied property: int Foo => expr;
        if (prop.ExpressionBody is not null)
        {
            var isStub = IsNotImplementedExpression(prop.ExpressionBody.Expression);
            return new MethodInfo(filePath, lineNumber, 1, IsStub: isStub, IsPartial: false, StatementCount: isStub ? 0 : 1);
        }

        // Auto-property: int Foo { get; set; } — this is valid, not a stub
        if (prop.AccessorList is not null && prop.AccessorList.Accessors.All(a => a.Body is null && a.ExpressionBody is null))
            return new MethodInfo(filePath, lineNumber, 0, IsStub: false, IsPartial: false, StatementCount: 1);

        // Property with accessor bodies — check if any are stubs
        if (prop.AccessorList is not null)
        {
            var hasStub = prop.AccessorList.Accessors.Any(a =>
                (a.ExpressionBody is not null && IsNotImplementedExpression(a.ExpressionBody.Expression)) ||
                (a.Body is not null && a.Body.Statements.Count == 1 && IsNotImplementedStatement(a.Body.Statements[0])));
            return new MethodInfo(filePath, lineNumber, 0, IsStub: hasStub, IsPartial: false, StatementCount: hasStub ? 0 : 1);
        }

        return new MethodInfo(filePath, lineNumber, 0, IsStub: false, IsPartial: false, StatementCount: 1);
    }

    private static bool IsNotImplementedExpression(ExpressionSyntax expr)
    {
        // throw new NotImplementedException(...)
        if (expr is ThrowExpressionSyntax throwExpr)
            return throwExpr.Expression is ObjectCreationExpressionSyntax oc
                && oc.Type.ToString().Contains("NotImplementedException");
        // new NotImplementedException() — shouldn't normally be standalone but handle it
        return expr is ObjectCreationExpressionSyntax oc2
            && oc2.Type.ToString().Contains("NotImplementedException");
    }

    private static bool IsNotImplementedStatement(StatementSyntax stmt)
    {
        // throw new NotImplementedException(...);
        if (stmt is ThrowStatementSyntax throwStmt && throwStmt.Expression is not null)
            return throwStmt.Expression is ObjectCreationExpressionSyntax oc
                && oc.Type.ToString().Contains("NotImplementedException");
        // Expression statement containing throw expression
        if (stmt is ExpressionStatementSyntax exprStmt)
            return IsNotImplementedExpression(exprStmt.Expression);
        return false;
    }

    private void AddToIndex(string className, string memberName, MethodInfo info)
    {
        var key = (className, memberName);
        if (!_index.TryGetValue(key, out var list))
        {
            list = [];
            _index[key] = list;
        }
        list.Add(info);
        MethodsIndexed++;
    }
}
