using System.CommandLine;
using System.Text;
using NatsNet.PortTracker.Audit;
using NatsNet.PortTracker.Data;

namespace NatsNet.PortTracker.Commands;

public static class AuditCommand
{
    private record AuditTarget(string Table, string Label, string DefaultSource, string DefaultOutput);

    private static readonly AuditTarget FeaturesTarget = new(
        "features", "features",
        Path.Combine(Directory.GetCurrentDirectory(), "dotnet", "src", "ZB.MOM.NatsNet.Server"),
        Path.Combine(Directory.GetCurrentDirectory(), "reports", "audit-results.csv"));

    private static readonly AuditTarget TestsTarget = new(
        "unit_tests", "unit tests",
        Path.Combine(Directory.GetCurrentDirectory(), "dotnet", "tests", "ZB.MOM.NatsNet.Server.Tests"),
        Path.Combine(Directory.GetCurrentDirectory(), "reports", "audit-results-tests.csv"));

    public static Command Create(Option<string> dbOption)
    {
        var sourceOpt = new Option<string?>("--source")
        {
            Description = "Path to the .NET source directory (defaults based on --type)"
        };

        var outputOpt = new Option<string?>("--output")
        {
            Description = "CSV report output path (defaults based on --type)"
        };

        var moduleOpt = new Option<int?>("--module")
        {
            Description = "Restrict to a specific module ID"
        };

        var executeOpt = new Option<bool>("--execute")
        {
            Description = "Apply DB updates (default: dry-run preview)",
            DefaultValueFactory = _ => false
        };

        var typeOpt = new Option<string>("--type")
        {
            Description = "What to audit: features, tests, or all",
            DefaultValueFactory = _ => "features"
        };

        var cmd = new Command("audit", "Classify unknown features/tests by inspecting .NET source code");
        cmd.Add(sourceOpt);
        cmd.Add(outputOpt);
        cmd.Add(moduleOpt);
        cmd.Add(executeOpt);
        cmd.Add(typeOpt);

        cmd.SetAction(parseResult =>
        {
            var dbPath = parseResult.GetValue(dbOption)!;
            var sourceOverride = parseResult.GetValue(sourceOpt);
            var outputOverride = parseResult.GetValue(outputOpt);
            var moduleId = parseResult.GetValue(moduleOpt);
            var execute = parseResult.GetValue(executeOpt);
            var type = parseResult.GetValue(typeOpt)!;

            AuditTarget[] targets = type switch
            {
                "features" => [FeaturesTarget],
                "tests" => [TestsTarget],
                "all" => [FeaturesTarget, TestsTarget],
                _ => throw new ArgumentException($"Unknown audit type: {type}. Use features, tests, or all.")
            };

            foreach (var target in targets)
            {
                var sourcePath = sourceOverride ?? target.DefaultSource;
                var outputPath = outputOverride ?? target.DefaultOutput;
                RunAudit(dbPath, sourcePath, outputPath, moduleId, execute, target);
            }
        });

        return cmd;
    }

    private static void RunAudit(string dbPath, string sourcePath, string outputPath, int? moduleId, bool execute, AuditTarget target)
    {
        if (!Directory.Exists(sourcePath))
        {
            Console.WriteLine($"Error: source directory not found: {sourcePath}");
            return;
        }

        Console.WriteLine($"Parsing .NET source files in {sourcePath}...");
        var indexer = new SourceIndexer();
        indexer.IndexDirectory(sourcePath);
        Console.WriteLine($"Indexed {indexer.FilesIndexed} files, {indexer.MethodsIndexed} methods/properties.");

        using var db = new Database(dbPath);
        var sql = $"SELECT id, dotnet_class, dotnet_method, go_file, go_method FROM {target.Table} WHERE status = 'unknown'";
        var parameters = new List<(string, object?)>();
        if (moduleId is not null)
        {
            sql += " AND module_id = @module";
            parameters.Add(("@module", moduleId));
        }
        sql += " ORDER BY id";

        var rows = db.Query(sql, parameters.ToArray());
        if (rows.Count == 0)
        {
            Console.WriteLine($"No unknown {target.Label} found.");
            return;
        }
        Console.WriteLine($"Found {rows.Count} unknown {target.Label} to classify.\n");

        var classifier = new FeatureClassifier(indexer);
        var results = new List<(FeatureClassifier.FeatureRecord Feature, FeatureClassifier.ClassificationResult Result)>();

        foreach (var row in rows)
        {
            var feature = new FeatureClassifier.FeatureRecord(
                Id: Convert.ToInt64(row["id"]),
                DotnetClass: row["dotnet_class"]?.ToString() ?? "",
                DotnetMethod: row["dotnet_method"]?.ToString() ?? "",
                GoFile: row["go_file"]?.ToString() ?? "",
                GoMethod: row["go_method"]?.ToString() ?? "");

            var result = classifier.Classify(feature);
            results.Add((feature, result));
        }

        WriteCsvReport(outputPath, results);

        var grouped = results.GroupBy(r => r.Result.Status)
            .ToDictionary(g => g.Key, g => g.Count());

        var label = char.ToUpper(target.Label[0]) + target.Label[1..];
        Console.WriteLine($"{label} Status Audit Results");
        Console.WriteLine(new string('=', $"{label} Status Audit Results".Length));
        Console.WriteLine($"Source: {sourcePath} ({indexer.FilesIndexed} files, {indexer.MethodsIndexed} methods indexed)");
        Console.WriteLine($"{label} audited: {results.Count}");
        Console.WriteLine();
        Console.WriteLine($"  verified:  {grouped.GetValueOrDefault("verified", 0)}");
        Console.WriteLine($"  stub:      {grouped.GetValueOrDefault("stub", 0)}");
        Console.WriteLine($"  n_a:       {grouped.GetValueOrDefault("n_a", 0)}");
        Console.WriteLine($"  deferred:  {grouped.GetValueOrDefault("deferred", 0)}");
        Console.WriteLine();

        if (!execute)
        {
            Console.WriteLine("Dry-run mode. Add --execute to apply changes.");
            Console.WriteLine($"Report: {outputPath}");
            return;
        }

        ApplyUpdates(db, results, target);
        Console.WriteLine($"Report: {outputPath}");
    }

    private static void WriteCsvReport(
        string outputPath,
        List<(FeatureClassifier.FeatureRecord Feature, FeatureClassifier.ClassificationResult Result)> results)
    {
        var dir = Path.GetDirectoryName(outputPath);
        if (!string.IsNullOrEmpty(dir))
            Directory.CreateDirectory(dir);

        var sb = new StringBuilder();
        sb.AppendLine("id,dotnet_class,dotnet_method,go_file,go_method,old_status,new_status,reason");
        foreach (var (feature, result) in results)
        {
            sb.AppendLine($"{feature.Id},{CsvEscape(feature.DotnetClass)},{CsvEscape(feature.DotnetMethod)},{CsvEscape(feature.GoFile)},{CsvEscape(feature.GoMethod)},unknown,{result.Status},{CsvEscape(result.Reason)}");
        }
        File.WriteAllText(outputPath, sb.ToString());
    }

    private static void ApplyUpdates(
        Database db,
        List<(FeatureClassifier.FeatureRecord Feature, FeatureClassifier.ClassificationResult Result)> results,
        AuditTarget target)
    {
        var groups = results
            .GroupBy(r => (r.Result.Status, Notes: r.Result.Status == "n_a" ? r.Result.Reason : (string?)null))
            .ToList();

        var totalUpdated = 0;
        using var transaction = db.Connection.BeginTransaction();
        try
        {
            foreach (var group in groups)
            {
                var ids = group.Select(r => r.Feature.Id).ToList();
                var status = group.Key.Status;
                var notes = group.Key.Notes;

                var placeholders = new List<string>();
                using var cmd = db.CreateCommand("");
                for (var i = 0; i < ids.Count; i++)
                {
                    placeholders.Add($"@id{i}");
                    cmd.Parameters.AddWithValue($"@id{i}", ids[i]);
                }

                cmd.Parameters.AddWithValue("@status", status);

                if (notes is not null)
                {
                    cmd.CommandText = $"UPDATE {target.Table} SET status = @status, notes = @notes WHERE id IN ({string.Join(", ", placeholders)})";
                    cmd.Parameters.AddWithValue("@notes", notes);
                }
                else
                {
                    cmd.CommandText = $"UPDATE {target.Table} SET status = @status WHERE id IN ({string.Join(", ", placeholders)})";
                }

                cmd.Transaction = transaction;
                var affected = cmd.ExecuteNonQuery();
                totalUpdated += affected;
            }

            transaction.Commit();
            Console.WriteLine($"Updated {totalUpdated} {target.Label}.");
        }
        catch
        {
            transaction.Rollback();
            Console.WriteLine("Error: transaction rolled back.");
            throw;
        }
    }

    private static string CsvEscape(string value)
    {
        if (value.Contains(',') || value.Contains('"') || value.Contains('\n'))
            return $"\"{value.Replace("\"", "\"\"")}\"";
        return value;
    }
}
