using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using LiquidCode.Tester.Common.Models;

namespace LiquidCode.Tester.Worker.Services;

public class PackageParserService : IPackageParserService
{
    private readonly ILogger<PackageParserService> _logger;
    private readonly PolygonProblemXmlParser _polygonParser;
    private readonly AnswerGenerationService _answerGenerator;
    private readonly CppCompilationServiceIsolate _cppCompilation;

    public PackageParserService(
        ILogger<PackageParserService> logger,
        PolygonProblemXmlParser polygonParser,
        AnswerGenerationService answerGenerator,
        CppCompilationServiceIsolate cppCompilation)
    {
        _logger = logger;
        _polygonParser = polygonParser;
        _answerGenerator = answerGenerator;
        _cppCompilation = cppCompilation;
    }

    public async Task<ProblemPackage> ParsePackageAsync(Stream packageStream)
    {
        var workingDirectory = Path.Combine(Path.GetTempPath(), $"problem_{Guid.NewGuid()}");
        Directory.CreateDirectory(workingDirectory);

        _logger.LogInformation("Extracting package to {WorkingDirectory}", workingDirectory);

        try
        {
            // Extract ZIP archive
            using var archive = new ZipArchive(packageStream, ZipArchiveMode.Read);
            archive.ExtractToDirectory(workingDirectory);

            // Search for problem.xml (Polygon package format is required)
            var problemXmlPath = Directory.EnumerateFiles(workingDirectory, "problem.xml", SearchOption.AllDirectories)
                .FirstOrDefault();

            if (string.IsNullOrEmpty(problemXmlPath))
            {
                _logger.LogError("problem.xml not found in package. Only Polygon format is supported.");
                throw new InvalidOperationException(
                    "Invalid package format: problem.xml not found. " +
                    "Only Polygon package format is supported. " +
                    "Please ensure your package contains a problem.xml file.");
            }

            var packageRoot = Path.GetDirectoryName(problemXmlPath)!;
            _logger.LogInformation("Polygon package detected (problem.xml found at {ProblemXml})", problemXmlPath);
            return await ParsePolygonPackageAsync(packageRoot, problemXmlPath, workingDirectory);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to parse package");
            throw;
        }
    }

    private async Task<ProblemPackage> ParsePolygonPackageAsync(string packageRoot, string problemXmlPath, string extractionRoot)
    {
        var descriptor = _polygonParser.ParseProblemXml(problemXmlPath);

        if (descriptor == null)
        {
            _logger.LogError("Failed to parse problem.xml");
            throw new InvalidOperationException(
                "Failed to parse problem.xml. The Polygon package format may be corrupted or invalid.");
        }

        // Check for interactive problems
        if (descriptor.Interactor != null)
        {
            _logger.LogError("Interactive problem detected: {ShortName}", descriptor.ShortName);
            throw new NotSupportedException(
                "Interactive problems are not currently supported. " +
                "The problem package contains an interactor, which is used for interactive tasks.");
        }

        var package = new ProblemPackage
        {
            WorkingDirectory = packageRoot,
            ExtractionRoot = extractionRoot,
            DefaultTimeLimit = descriptor.TimeLimitMs,
            DefaultMemoryLimit = descriptor.MemoryLimitMb
        };

        var buildDirectory = Path.Combine(packageRoot, ".lc-build");
        Directory.CreateDirectory(buildDirectory);

        var compiledExecutables = await CompilePolygonExecutablesAsync(descriptor, packageRoot, buildDirectory);

        package.CheckerPath = await CompileCheckerForPackageAsync(descriptor, packageRoot, buildDirectory)
            ?? await FindAndCompileCheckerAsync(packageRoot);

    var validatorPath = await CompileValidatorAsync(descriptor, packageRoot, buildDirectory, compiledExecutables);

        await GenerateAndValidateTestsAsync(descriptor, packageRoot, compiledExecutables, validatorPath);

        var testIndices = descriptor.Tests.Count > 0
            ? descriptor.Tests.Select(t => t.Index).Distinct().OrderBy(i => i).ToList()
            : Enumerable.Range(1, descriptor.TestCount > 0 ? descriptor.TestCount : 0).ToList();

        if (testIndices.Count == 0)
        {
            _logger.LogError("No test definitions found in problem.xml");
            throw new InvalidOperationException(
                "No test definitions found in problem.xml. " +
                "The Polygon package must define tests in the problem.xml file.");
        }

        var inputs = new List<(int index, string inputPath)>();
        var answers = new Dictionary<int, string>();

        foreach (var testIndex in testIndices)
        {
            var inputRelative = FormatPolygonPattern(descriptor.InputPathPattern, testIndex);
            var inputFullPath = Path.Combine(packageRoot, NormalizeRelativePath(inputRelative));

            if (!File.Exists(inputFullPath))
            {
                _logger.LogWarning("Input file not found for test {Index}: {RelativePath}", testIndex, inputRelative);
                continue;
            }

            var answerRelative = FormatPolygonPattern(descriptor.AnswerPathPattern, testIndex);
            var answerFullPath = Path.Combine(packageRoot, NormalizeRelativePath(answerRelative));

            inputs.Add((testIndex, inputFullPath));
            answers[testIndex] = answerFullPath;
        }

        var missingAnswerInputs = new List<string>();
        var missingAnswerPaths = new List<string>();

        foreach (var (index, inputPath) in inputs)
        {
            if (!answers.TryGetValue(index, out var answerPath))
            {
                continue;
            }

            if (!File.Exists(answerPath))
            {
                missingAnswerInputs.Add(inputPath);
                missingAnswerPaths.Add(answerPath);
            }
        }

        if (missingAnswerPaths.Count > 0)
        {
            _logger.LogInformation("Found {Count} tests without answers, attempting to generate them", missingAnswerPaths.Count);

            var generated = await _answerGenerator.GenerateAnswersAsync(
                descriptor,
                packageRoot,
                missingAnswerInputs,
                missingAnswerPaths);

            if (!generated)
            {
                _logger.LogWarning("Failed to generate answer files, affected tests will be skipped");
            }
            else
            {
                _logger.LogInformation("Answer files generated successfully");
            }
        }

        foreach (var (index, inputPath) in inputs.OrderBy(item => item.index))
        {
            if (!answers.TryGetValue(index, out var answerPath))
            {
                continue;
            }

            if (!File.Exists(answerPath))
            {
                _logger.LogWarning("Answer file not found for test {Index}: {AnswerPath}", index, answerPath);
                continue;
            }

            package.TestCases.Add(new TestCase
            {
                Number = index,
                InputFilePath = inputPath,
                OutputFilePath = answerPath,
                TimeLimit = descriptor.TimeLimitMs,
                MemoryLimit = descriptor.MemoryLimitMb
            });
        }

        if (package.TestCases.Count == 0)
        {
            _logger.LogWarning("No test cases with answer files found for Polygon package");
        }

        _logger.LogInformation("Parsed Polygon package with {TestCount} tests (declared {TotalTests})",
            package.TestCases.Count, descriptor.TestCount);

        return package;
    }


    private async Task<Dictionary<string, string>> CompilePolygonExecutablesAsync(
        PolygonProblemDescriptor descriptor,
        string packageRoot,
        string buildDirectory)
    {
        var compiled = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

        foreach (var executable in descriptor.Executables)
        {
            if (string.IsNullOrWhiteSpace(executable.SourcePath))
            {
                continue;
            }

            var alias = !string.IsNullOrWhiteSpace(executable.Name)
                ? executable.Name
                : Path.GetFileNameWithoutExtension(executable.SourcePath);

            if (string.IsNullOrWhiteSpace(alias))
            {
                continue;
            }

            var sourceFullPath = Path.Combine(packageRoot, NormalizeRelativePath(executable.SourcePath));

            if (!File.Exists(sourceFullPath))
            {
                _logger.LogWarning("Executable source not found: {Path}", executable.SourcePath);
                continue;
            }

            if (IsCppAsset(executable.Type, sourceFullPath))
            {
                var outputPath = Path.Combine(buildDirectory, alias);
                var includeDirs = GetIncludeDirectories(packageRoot, sourceFullPath);
                var stdVersion = ExtractCppStandard(executable.Type);

                var compilationResult = await _cppCompilation.CompileFileAsync(
                    sourceFullPath,
                    outputPath,
                    stdVersion,
                    includeDirs);

                if (compilationResult.Success && !string.IsNullOrEmpty(compilationResult.ExecutablePath))
                {
                    _logger.LogInformation("Compiled executable {Alias} -> {Path}", alias, compilationResult.ExecutablePath);
                    compiled[alias] = compilationResult.ExecutablePath;
                }
                else
                {
                    _logger.LogWarning("Failed to compile executable {Alias}: {Error}", alias, compilationResult.CompilerOutput);
                }
            }
            else
            {
                var binaryPath = !string.IsNullOrWhiteSpace(executable.BinaryPath)
                    ? Path.Combine(packageRoot, NormalizeRelativePath(executable.BinaryPath))
                    : null;

                if (!string.IsNullOrEmpty(binaryPath) && File.Exists(binaryPath))
                {
                    _logger.LogInformation("Using prebuilt executable {Alias} at {Path}", alias, binaryPath);
                    compiled[alias] = binaryPath;
                }
                else
                {
                    _logger.LogWarning("Unsupported executable type {Type} for {Alias}", executable.Type, alias);
                }
            }
        }

        return compiled;
    }

    private async Task<string?> CompileCheckerForPackageAsync(
        PolygonProblemDescriptor descriptor,
        string packageRoot,
        string buildDirectory)
    {
        if (descriptor.Checker == null || string.IsNullOrWhiteSpace(descriptor.Checker.SourcePath))
        {
            _logger.LogInformation("No checker declared in problem.xml");
            return null;
        }

        var sourcePath = Path.Combine(packageRoot, NormalizeRelativePath(descriptor.Checker.SourcePath));

        if (!File.Exists(sourcePath))
        {
            _logger.LogWarning("Checker source not found: {SourcePath}", descriptor.Checker.SourcePath);
            return null;
        }

        var alias = !string.IsNullOrWhiteSpace(descriptor.Checker.CopyPath)
            ? Path.GetFileNameWithoutExtension(descriptor.Checker.CopyPath)
            : Path.GetFileNameWithoutExtension(descriptor.Checker.BinaryPath ?? descriptor.Checker.SourcePath);

        if (string.IsNullOrWhiteSpace(alias))
        {
            alias = "checker";
        }

        var outputPath = Path.Combine(buildDirectory, alias);
        var includeDirs = GetIncludeDirectories(packageRoot, sourcePath);
        var stdVersion = ExtractCppStandard(descriptor.Checker.Type);

        var result = await _cppCompilation.CompileFileAsync(
            sourcePath,
            outputPath,
            stdVersion,
            includeDirs);

        if (result.Success && !string.IsNullOrEmpty(result.ExecutablePath))
        {
            _logger.LogInformation("Checker compiled to {Path}", result.ExecutablePath);
            return result.ExecutablePath;
        }

        _logger.LogWarning("Failed to compile checker: {Error}", result.CompilerOutput);
        return null;
    }

    private async Task<string?> CompileValidatorAsync(
        PolygonProblemDescriptor descriptor,
        string packageRoot,
        string buildDirectory,
        IDictionary<string, string> compiledExecutables)
    {
        var validator = descriptor.Validators.FirstOrDefault();
        if (validator == null || string.IsNullOrWhiteSpace(validator.SourcePath))
        {
            _logger.LogInformation("No validator declared in problem.xml");
            return null;
        }

        var sourcePath = Path.Combine(packageRoot, NormalizeRelativePath(validator.SourcePath));
        if (!File.Exists(sourcePath))
        {
            _logger.LogWarning("Validator source not found: {SourcePath}", validator.SourcePath);
            return null;
        }

        var alias = Path.GetFileNameWithoutExtension(validator.BinaryPath ?? validator.SourcePath);
        if (string.IsNullOrWhiteSpace(alias))
        {
            alias = "validator";
        }

        if (compiledExecutables.TryGetValue(alias, out var compiledPath) && File.Exists(compiledPath))
        {
            _logger.LogInformation("Reusing precompiled validator executable at {Path}", compiledPath);
            return compiledPath;
        }

        var outputPath = Path.Combine(buildDirectory, alias);
        var includeDirs = GetIncludeDirectories(packageRoot, sourcePath);
        var stdVersion = ExtractCppStandard(validator.Type);

        var result = await _cppCompilation.CompileFileAsync(
            sourcePath,
            outputPath,
            stdVersion,
            includeDirs);

        if (result.Success && !string.IsNullOrEmpty(result.ExecutablePath))
        {
            _logger.LogInformation("Validator compiled to {Path}", result.ExecutablePath);
            compiledExecutables[alias] = result.ExecutablePath;
            return result.ExecutablePath;
        }

        _logger.LogWarning("Failed to compile validator: {Error}", result.CompilerOutput);
        return null;
    }

    private async Task GenerateAndValidateTestsAsync(
        PolygonProblemDescriptor descriptor,
        string packageRoot,
        IDictionary<string, string> compiledExecutables,
        string? validatorPath)
    {
        if (descriptor.Tests.Count == 0)
        {
            _logger.LogInformation("problem.xml does not enumerate tests; skipping generation step");
            return;
        }

        foreach (var test in descriptor.Tests)
        {
            var inputRelative = FormatPolygonPattern(descriptor.InputPathPattern, test.Index);
            var inputFullPath = Path.Combine(packageRoot, NormalizeRelativePath(inputRelative));

            if (test.Method is PolygonTestMethod.Generated or PolygonTestMethod.Script)
            {
                await GenerateTestInputAsync(test, packageRoot, inputFullPath, compiledExecutables);
            }
            else if (!File.Exists(inputFullPath))
            {
                _logger.LogWarning("Manual test {Index} expected at {Path} but not found", test.Index, inputRelative);
                continue;
            }

            if (!File.Exists(inputFullPath))
            {
                throw new InvalidOperationException($"Failed to produce input file for test {test.Index} ({inputRelative})");
            }

            if (!string.IsNullOrEmpty(validatorPath))
            {
                await ValidateInputAsync(validatorPath, inputFullPath, test.Index);
            }
        }
    }

    private async Task GenerateTestInputAsync(
        PolygonTestDefinition test,
        string packageRoot,
        string inputFullPath,
        IDictionary<string, string> compiledExecutables)
    {
        if (string.IsNullOrWhiteSpace(test.Command))
        {
            _logger.LogWarning("Test {Index} is marked as generated but has no command", test.Index);
            return;
        }

        var args = SplitCommandLine(test.Command);
        if (args.Count == 0)
        {
            _logger.LogWarning("Failed to parse generator command for test {Index}", test.Index);
            return;
        }

        var generatorAlias = args[0];

        if (!compiledExecutables.TryGetValue(generatorAlias, out var generatorPath))
        {
            _logger.LogWarning("Generator {Generator} not found for test {Index}", generatorAlias, test.Index);
            return;
        }

        Directory.CreateDirectory(Path.GetDirectoryName(inputFullPath)!);

        _logger.LogInformation("Generating test {Index} using {Generator} {Arguments}",
            test.Index,
            generatorAlias,
            string.Join(' ', args.Skip(1)));

        using var process = new Process
        {
            StartInfo = new ProcessStartInfo
            {
                FileName = generatorPath,
                WorkingDirectory = packageRoot,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                UseShellExecute = false,
                CreateNoWindow = true
            }
        };

        foreach (var argument in args.Skip(1))
        {
            process.StartInfo.ArgumentList.Add(argument);
        }

        process.Start();

        var stderrTask = process.StandardError.ReadToEndAsync();

        await using (var outputStream = File.Create(inputFullPath))
        {
            await process.StandardOutput.BaseStream.CopyToAsync(outputStream);
        }

        await process.WaitForExitAsync();
        var stderr = await stderrTask;

        if (process.ExitCode != 0)
        {
            throw new InvalidOperationException($"Generator '{generatorAlias}' failed for test {test.Index}: {stderr}");
        }

        if (!string.IsNullOrWhiteSpace(stderr))
        {
            _logger.LogDebug("Generator '{Generator}' stderr for test {Index}: {Message}", generatorAlias, test.Index, stderr.Trim());
        }
    }

    private async Task ValidateInputAsync(string validatorPath, string inputFilePath, int testIndex)
    {
        using var process = new Process
        {
            StartInfo = new ProcessStartInfo
            {
                FileName = validatorPath,
                RedirectStandardInput = true,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                UseShellExecute = false,
                CreateNoWindow = true
            }
        };

        process.Start();

        var inputContent = await File.ReadAllTextAsync(inputFilePath);
        var normalizedInput = inputContent.Replace("\r\n", "\n").Replace("\r", "\n");
        await process.StandardInput.WriteAsync(normalizedInput);
        process.StandardInput.Close();

        var stdoutTask = process.StandardOutput.ReadToEndAsync();
        var stderrTask = process.StandardError.ReadToEndAsync();

        await process.WaitForExitAsync();

        var stdout = await stdoutTask;
        var stderr = await stderrTask;

        if (process.ExitCode != 0)
        {
            var message = string.IsNullOrWhiteSpace(stderr) ? stdout : stderr;
            throw new InvalidOperationException($"Validator rejected test {testIndex}: {message?.Trim()}");
        }

        if (!string.IsNullOrWhiteSpace(stdout))
        {
            _logger.LogDebug("Validator output for test {Index}: {Message}", testIndex, stdout.Trim());
        }
    }

    private static IEnumerable<string> GetIncludeDirectories(string packageRoot, string sourceFullPath)
    {
        var sourceDirectory = Path.GetDirectoryName(sourceFullPath);
        var filesDirectory = Path.Combine(packageRoot, "files");

        return new[] { sourceDirectory, filesDirectory, packageRoot }
            .Where(dir => !string.IsNullOrWhiteSpace(dir))
            .Select(dir => dir!)
            .Where(Directory.Exists)
            .Distinct();
    }

    private static string NormalizeRelativePath(string relativePath)
    {
        return relativePath
            .Replace("\\", Path.DirectorySeparatorChar.ToString(CultureInfo.InvariantCulture))
            .Replace("/", Path.DirectorySeparatorChar.ToString(CultureInfo.InvariantCulture));
    }

    private static string FormatPolygonPattern(string pattern, int index)
    {
        if (string.IsNullOrWhiteSpace(pattern))
        {
            return index.ToString(CultureInfo.InvariantCulture);
        }

        return Regex.Replace(pattern, "%0?(\\d*)d", match =>
        {
            if (int.TryParse(match.Groups[1].Value, out var width) && width > 0)
            {
                return index.ToString($"D{width}", CultureInfo.InvariantCulture);
            }

            return index.ToString(CultureInfo.InvariantCulture);
        });
    }

    private static IReadOnlyList<string> SplitCommandLine(string command)
    {
        var result = new List<string>();
        if (string.IsNullOrWhiteSpace(command))
        {
            return result;
        }

        var current = new StringBuilder();
        var inQuotes = false;

        foreach (var ch in command)
        {
            if (ch == '"')
            {
                inQuotes = !inQuotes;
                continue;
            }

            if (char.IsWhiteSpace(ch) && !inQuotes)
            {
                if (current.Length > 0)
                {
                    result.Add(current.ToString());
                    current.Clear();
                }
            }
            else
            {
                current.Append(ch);
            }
        }

        if (current.Length > 0)
        {
            result.Add(current.ToString());
        }

        return result;
    }

    private static bool IsCppAsset(string? type, string sourceFullPath)
    {
        if (!string.IsNullOrWhiteSpace(type) && type.StartsWith("cpp", StringComparison.OrdinalIgnoreCase))
        {
            return true;
        }

        var extension = Path.GetExtension(sourceFullPath);
        return extension.Equals(".cpp", StringComparison.OrdinalIgnoreCase) ||
               extension.Equals(".cc", StringComparison.OrdinalIgnoreCase) ||
               extension.Equals(".cxx", StringComparison.OrdinalIgnoreCase);
    }

    private static string? ExtractCppStandard(string? type)
    {
        if (string.IsNullOrWhiteSpace(type))
        {
            return null;
        }

        var normalized = type.ToLowerInvariant();

        if (normalized.Contains("++26")) return "26";
        if (normalized.Contains("++23")) return "23";
        if (normalized.Contains("++20")) return "20";
        if (normalized.Contains("++17")) return "17";
        if (normalized.Contains("++14")) return "14";
        if (normalized.Contains("++11")) return "11";

        return null;
    }


    private async Task<string?> FindAndCompileCheckerAsync(string workingDirectory)
    {
        // Try to find checker in common locations
        var checkerCandidates = new[]
        {
            Path.Combine(workingDirectory, "check.exe"),
            Path.Combine(workingDirectory, "checker.exe"),
            Path.Combine(workingDirectory, "check.cpp"),
            Path.Combine(workingDirectory, "checker.cpp"),
            Path.Combine(workingDirectory, "files", "check.exe"),
            Path.Combine(workingDirectory, "files", "checker.exe"),
            Path.Combine(workingDirectory, "files", "check.cpp"),
            Path.Combine(workingDirectory, "files", "checker.cpp")
        };

        foreach (var candidate in checkerCandidates)
        {
            if (!File.Exists(candidate))
                continue;

            // If it's already an executable, use it
            if (candidate.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
            {
                _logger.LogInformation("Found checker executable: {CheckerPath}", candidate);
                return candidate;
            }

            // If it's C++ source, compile it
            if (candidate.EndsWith(".cpp", StringComparison.OrdinalIgnoreCase))
            {
                _logger.LogInformation("Found checker source: {CheckerPath}, compiling...", candidate);

                try
                {
                    var checkerDir = Path.GetDirectoryName(candidate)!;
                    var outputPath = Path.Combine(checkerDir, Path.GetFileNameWithoutExtension(candidate));
                    var includeDirs = GetIncludeDirectories(workingDirectory, candidate);

                    // Compile checker with inferred standard (default C++17)
                    var compilationResult = await _cppCompilation.CompileFileAsync(
                        candidate,
                        outputPath,
                        "17",
                        includeDirs);

                    if (!compilationResult.Success)
                    {
                        _logger.LogError("Failed to compile checker: {Error}", compilationResult.CompilerOutput);
                        continue; // Try next candidate
                    }

                    _logger.LogInformation("Checker compiled successfully: {ExecutablePath}", compilationResult.ExecutablePath);
                    return compilationResult.ExecutablePath;
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Error compiling checker: {CheckerPath}", candidate);
                    continue; // Try next candidate
                }
            }
        }

        _logger.LogWarning("No checker found in package");
        return null;
    }
}
