using System.Collections.Concurrent;
using System.Text.Json;
using System.Text.Json.Serialization;
using GitHub.Copilot.SDK;
using GitHub.Copilot.SDK.Rpc;
using Eryx.AgentHost.Contracts;

namespace Eryx.AgentHost.Services;

public sealed class SidecarProtocolHost
{
    private const string DescribeCapabilitiesCommandType = "describe-capabilities";
    private const string ValidatePatternCommandType = "validate-pattern";
    private const string RunTurnCommandType = "run-turn";
    private const string ResolveApprovalCommandType = "resolve-approval";

    private static readonly string[] AuthenticationErrorIndicators =
    [
        "login",
        "log in",
        "sign in",
        "authenticate",
        "authentication",
        "not signed in",
        "not logged in",
        "reauth",
        "credential",
    ];

    private readonly Func<CancellationToken, Task<SidecarCapabilitiesDto>> _capabilitiesProvider;
    private readonly PatternValidator _patternValidator;
    private readonly ITurnWorkflowRunner _workflowRunner;
    private readonly JsonSerializerOptions _jsonOptions;
    private readonly IReadOnlyDictionary<string, Func<CommandContext, Task>> _commandHandlers;
    private readonly SemaphoreSlim _writeLock = new(1, 1);
    private readonly ConcurrentDictionary<string, Task> _inFlight = new(StringComparer.Ordinal);

    public SidecarProtocolHost()
        : this(new PatternValidator())
    {
    }

    public SidecarProtocolHost(
        PatternValidator patternValidator,
        ITurnWorkflowRunner? workflowRunner = null,
        Func<CancellationToken, Task<SidecarCapabilitiesDto>>? capabilitiesProvider = null)
    {
        _patternValidator = patternValidator;
        _workflowRunner = workflowRunner ?? new CopilotWorkflowRunner(_patternValidator);
        _capabilitiesProvider = capabilitiesProvider ?? BuildCapabilitiesAsync;
        _jsonOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web)
        {
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
            PropertyNameCaseInsensitive = true,
        };
        _commandHandlers = new Dictionary<string, Func<CommandContext, Task>>(StringComparer.Ordinal)
        {
            [DescribeCapabilitiesCommandType] = HandleDescribeCapabilitiesAsync,
            [ValidatePatternCommandType] = HandleValidatePatternAsync,
            [RunTurnCommandType] = HandleRunTurnAsync,
            [ResolveApprovalCommandType] = HandleResolveApprovalAsync,
        };
    }

    public async Task RunAsync(TextReader input, TextWriter output, CancellationToken cancellationToken)
    {
        while (!cancellationToken.IsCancellationRequested)
        {
            string? line = await input.ReadLineAsync(cancellationToken).ConfigureAwait(false);
            if (line is null)
            {
                break;
            }

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

            SidecarCommandEnvelope envelope = DeserializeEnvelope(line);
            TrackInFlightRequest(
                envelope.RequestId,
                HandleCommandAsync(line, envelope, output, cancellationToken));
        }

        await Task.WhenAll(_inFlight.Values).ConfigureAwait(false);
    }

    private SidecarCommandEnvelope DeserializeEnvelope(string line)
    {
        return JsonSerializer.Deserialize<SidecarCommandEnvelope>(line, _jsonOptions)
            ?? throw new InvalidOperationException("Could not deserialize sidecar command envelope.");
    }

    private void TrackInFlightRequest(string requestId, Task task)
    {
        _inFlight[requestId] = task;
        _ = task.ContinueWith(
            _ =>
            {
                _inFlight.TryRemove(requestId, out Task? removedTask);
                return removedTask is not null;
            },
            CancellationToken.None,
            TaskContinuationOptions.None,
            TaskScheduler.Default);
    }

    private async Task HandleCommandAsync(
        string rawCommand,
        SidecarCommandEnvelope envelope,
        TextWriter output,
        CancellationToken cancellationToken)
    {
        CommandContext context = new(rawCommand, envelope, output, cancellationToken);

        try
        {
            await ExecuteCommandAsync(context).ConfigureAwait(false);
            await WriteCommandCompleteAsync(context).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            await WriteCommandErrorAsync(context, ex.Message).ConfigureAwait(false);
        }
    }

    private Task ExecuteCommandAsync(CommandContext context)
    {
        if (_commandHandlers.TryGetValue(context.Envelope.Type, out Func<CommandContext, Task>? handler))
        {
            return handler(context);
        }

        throw new NotSupportedException($"Unknown sidecar command type '{context.Envelope.Type}'.");
    }

    private async Task HandleDescribeCapabilitiesAsync(CommandContext context)
    {
        await WriteAsync(context.Output, new CapabilitiesEventDto
        {
            Type = "capabilities",
            RequestId = context.Envelope.RequestId,
            Capabilities = await _capabilitiesProvider(context.CancellationToken).ConfigureAwait(false),
        }, context.CancellationToken).ConfigureAwait(false);
    }

    private async Task HandleValidatePatternAsync(CommandContext context)
    {
        ValidatePatternCommandDto command = DeserializeCommand<ValidatePatternCommandDto>(context);

        await WriteAsync(context.Output, new PatternValidationEventDto
        {
            Type = "pattern-validation",
            RequestId = context.Envelope.RequestId,
            Issues = _patternValidator.Validate(command.Pattern),
        }, context.CancellationToken).ConfigureAwait(false);
    }

    private async Task HandleRunTurnAsync(CommandContext context)
    {
        RunTurnCommandDto command = DeserializeCommand<RunTurnCommandDto>(context);
        IReadOnlyList<ChatMessageDto> messages = await _workflowRunner.RunTurnAsync(
                command,
                delta => WriteAsync(context.Output, delta, context.CancellationToken),
                activity => WriteAsync(context.Output, activity, context.CancellationToken),
                approval => WriteAsync(context.Output, approval, context.CancellationToken),
                context.CancellationToken)
            .ConfigureAwait(false);

        await WriteAsync(context.Output, new TurnCompleteEventDto
        {
            Type = "turn-complete",
            RequestId = context.Envelope.RequestId,
            SessionId = command.SessionId,
            Messages = messages,
        }, context.CancellationToken).ConfigureAwait(false);
    }

    private async Task HandleResolveApprovalAsync(CommandContext context)
    {
        ResolveApprovalCommandDto command = DeserializeCommand<ResolveApprovalCommandDto>(context);
        await _workflowRunner.ResolveApprovalAsync(command, context.CancellationToken).ConfigureAwait(false);
    }

    private TCommand DeserializeCommand<TCommand>(CommandContext context)
        where TCommand : SidecarCommandEnvelope
    {
        return JsonSerializer.Deserialize<TCommand>(context.RawCommand, _jsonOptions)
            ?? throw new InvalidOperationException(
                $"Could not deserialize {context.Envelope.Type} command.");
    }

    private Task WriteCommandCompleteAsync(CommandContext context)
    {
        return WriteAsync(context.Output, new CommandCompleteEventDto
        {
            Type = "command-complete",
            RequestId = context.Envelope.RequestId,
        }, context.CancellationToken);
    }

    private Task WriteCommandErrorAsync(CommandContext context, string message)
    {
        return WriteAsync(context.Output, new CommandErrorEventDto
        {
            Type = "command-error",
            RequestId = context.Envelope.RequestId,
            Message = message,
        }, context.CancellationToken);
    }

    private async Task WriteAsync(TextWriter output, object payload, CancellationToken cancellationToken)
    {
        string json = JsonSerializer.Serialize(payload, _jsonOptions);
        await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            await output.WriteLineAsync(json).ConfigureAwait(false);
            await output.FlushAsync(cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            _writeLock.Release();
        }
    }

    private static async Task<SidecarCapabilitiesDto> BuildCapabilitiesAsync(CancellationToken cancellationToken)
    {
        try
        {
            CopilotCliContext cliContext = CopilotCliPathResolver.ResolveCliContext();
            CapabilityProbeResult probe = await ProbeCapabilitiesAsync(cliContext, cancellationToken).ConfigureAwait(false);
            return CreateCapabilities(probe.Models, probe.RuntimeTools, probe.Connection);
        }
        catch (Exception exception)
        {
            SidecarConnectionDiagnosticsDto connection = CreateMissingCliDiagnostics(exception);
            Console.Error.WriteLine($"[eryx sidecar] {connection.Summary} {exception.Message}");
            return CreateCapabilities([], [], connection);
        }
    }

    private static async Task<CapabilityProbeResult> ProbeCapabilitiesAsync(
        CopilotCliContext cliContext,
        CancellationToken cancellationToken)
    {
        IReadOnlyList<SidecarModelCapabilityDto> models = [];
        IReadOnlyList<SidecarRuntimeToolDto> runtimeTools = [];
        SidecarCopilotAccountDiagnosticsDto? account = null;
        SidecarCopilotCliVersionDiagnosticsDto? cliVersion = null;
        Task<SidecarCopilotCliVersionDiagnosticsDto> cliVersionTask =
            CopilotConnectionMetadataResolver.GetCliVersionDiagnosticsAsync(cliContext, cancellationToken);

        try
        {
            CopilotClientOptions clientOptions = CopilotCliPathResolver.CreateClientOptions(cliContext);

            await using CopilotClient client = new(clientOptions);
            await client.StartAsync(cancellationToken).ConfigureAwait(false);

            GetAuthStatusResponse? authStatus =
                await CopilotConnectionMetadataResolver.TryGetAuthStatusAsync(client, cancellationToken).ConfigureAwait(false);
            account = await CopilotConnectionMetadataResolver.CreateAccountDiagnosticsAsync(
                    authStatus,
                    cliContext.Environment,
                    cancellationToken)
                .ConfigureAwait(false);

            models = await ListAvailableModelsAsync(client, cancellationToken).ConfigureAwait(false);
            runtimeTools = await TryListAvailableRuntimeToolsAsync(client, cancellationToken).ConfigureAwait(false);
            cliVersion = await cliVersionTask.ConfigureAwait(false);

            return new CapabilityProbeResult(
                models,
                runtimeTools,
                CreateReadyConnectionDiagnostics(cliContext.CliPath, models.Count, cliVersion, account));
        }
        catch (Exception exception)
        {
            cliVersion = await cliVersionTask.ConfigureAwait(false);
            Console.Error.WriteLine($"[eryx sidecar] Failed to list available Copilot models: {exception.Message}");

            return new CapabilityProbeResult(
                models,
                runtimeTools,
                CreateFailureConnectionDiagnostics(cliContext.CliPath, exception, cliVersion, account));
        }
    }

    private static SidecarCapabilitiesDto CreateCapabilities(
        IReadOnlyList<SidecarModelCapabilityDto> models,
        IReadOnlyList<SidecarRuntimeToolDto> runtimeTools,
        SidecarConnectionDiagnosticsDto connection)
    {
        return new SidecarCapabilitiesDto
        {
            Modes = BuildModeCapabilities(),
            Models = models,
            RuntimeTools = runtimeTools,
            Connection = connection,
        };
    }

    private static Dictionary<string, SidecarModeCapabilityDto> BuildModeCapabilities()
    {
        return new Dictionary<string, SidecarModeCapabilityDto>(StringComparer.OrdinalIgnoreCase)
        {
            ["single"] = new() { Available = true },
            ["sequential"] = new() { Available = true },
            ["concurrent"] = new() { Available = true },
            ["handoff"] = new() { Available = true },
            ["group-chat"] = new() { Available = true },
            ["magentic"] = new()
            {
                Available = false,
                Reason = "Microsoft Agent Framework currently documents Magentic orchestration as unsupported in C#.",
            },
        };
    }

    private static async Task<IReadOnlyList<SidecarModelCapabilityDto>> ListAvailableModelsAsync(
        CopilotClient client,
        CancellationToken cancellationToken)
    {
        List<ModelInfo> models = await client.ListModelsAsync(cancellationToken).ConfigureAwait(false);
        return models
            .Select(model => new SidecarModelCapabilityDto
            {
                Id = model.Id,
                Name = model.Name,
                SupportedReasoningEfforts = (model.SupportedReasoningEfforts ?? [])
                    .Where(IsReasoningEffort)
                    .Distinct(StringComparer.Ordinal)
                    .ToList(),
                DefaultReasoningEffort = IsReasoningEffort(model.DefaultReasoningEffort)
                    ? model.DefaultReasoningEffort
                    : null,
            })
            .OrderBy(model => model.Name, StringComparer.OrdinalIgnoreCase)
            .ToList();
    }

    private static async Task<IReadOnlyList<SidecarRuntimeToolDto>> TryListAvailableRuntimeToolsAsync(
        CopilotClient client,
        CancellationToken cancellationToken)
    {
        try
        {
            return await ListAvailableRuntimeToolsAsync(client, cancellationToken).ConfigureAwait(false);
        }
        catch (Exception exception)
        {
            Console.Error.WriteLine($"[eryx sidecar] Failed to list available Copilot runtime tools: {exception.Message}");
            return [];
        }
    }

    private static async Task<IReadOnlyList<SidecarRuntimeToolDto>> ListAvailableRuntimeToolsAsync(
        CopilotClient client,
        CancellationToken cancellationToken)
    {
        ToolsListResult result = await client.Rpc.Tools.ListAsync(null!, cancellationToken).ConfigureAwait(false);
        return result.Tools
            .Where(tool => !string.IsNullOrWhiteSpace(tool.Name))
            .Select(tool => new SidecarRuntimeToolDto
            {
                Id = tool.Name.Trim(),
                Label = tool.Name.Trim(),
                Description = string.IsNullOrWhiteSpace(tool.Description) ? null : tool.Description.Trim(),
            })
            .DistinctBy(tool => tool.Id, StringComparer.OrdinalIgnoreCase)
            .OrderBy(tool => tool.Label, StringComparer.OrdinalIgnoreCase)
            .ToList();
    }

    private static bool IsReasoningEffort(string? value)
    {
        return value is "low" or "medium" or "high" or "xhigh";
    }

    internal static SidecarConnectionDiagnosticsDto CreateMissingCliDiagnostics(Exception exception)
    {
        return new SidecarConnectionDiagnosticsDto
        {
            Status = "copilot-cli-missing",
            Summary = "GitHub Copilot CLI is not installed or is not available on PATH.",
            Detail = exception.Message,
            CheckedAt = DateTimeOffset.UtcNow.ToString("O"),
        };
    }

    internal static SidecarConnectionDiagnosticsDto CreateReadyConnectionDiagnostics(
        string cliPath,
        int modelCount,
        SidecarCopilotCliVersionDiagnosticsDto? cliVersion = null,
        SidecarCopilotAccountDiagnosticsDto? account = null)
    {
        string summary = modelCount switch
        {
            0 => "Connected to GitHub Copilot, but no models were reported.",
            1 => "Connected to GitHub Copilot. 1 model is available.",
            _ => $"Connected to GitHub Copilot. {modelCount} models are available.",
        };

        return new SidecarConnectionDiagnosticsDto
        {
            Status = "ready",
            Summary = summary,
            Detail = $"Using Copilot CLI at {cliPath}.",
            CopilotCliPath = cliPath,
            CopilotCliVersion = cliVersion,
            Account = account,
            CheckedAt = DateTimeOffset.UtcNow.ToString("O"),
        };
    }

    internal static SidecarConnectionDiagnosticsDto CreateFailureConnectionDiagnostics(
        string? cliPath,
        Exception exception,
        SidecarCopilotCliVersionDiagnosticsDto? cliVersion = null,
        SidecarCopilotAccountDiagnosticsDto? account = null)
    {
        string status = ClassifyConnectionStatus(exception);
        string summary = status == "copilot-auth-required"
            ? "GitHub Copilot requires authentication before Eryx can load models."
            : "GitHub Copilot was found, but Eryx could not load its model list.";

        return new SidecarConnectionDiagnosticsDto
        {
            Status = status,
            Summary = summary,
            Detail = exception.Message,
            CopilotCliPath = cliPath,
            CopilotCliVersion = cliVersion,
            Account = account,
            CheckedAt = DateTimeOffset.UtcNow.ToString("O"),
        };
    }

    internal static string ClassifyConnectionStatus(Exception exception)
    {
        string message = exception.Message;
        if (AuthenticationErrorIndicators.Any(indicator =>
            message.Contains(indicator, StringComparison.OrdinalIgnoreCase)))
        {
            return "copilot-auth-required";
        }

        return "copilot-error";
    }

    private sealed record CommandContext(
        string RawCommand,
        SidecarCommandEnvelope Envelope,
        TextWriter Output,
        CancellationToken CancellationToken);

    private sealed record CapabilityProbeResult(
        IReadOnlyList<SidecarModelCapabilityDto> Models,
        IReadOnlyList<SidecarRuntimeToolDto> RuntimeTools,
        SidecarConnectionDiagnosticsDto Connection);
}
