using System.Collections.Concurrent;
using System.IO.Pipes;
using System.Text;
using YMhut.Box.Core.Api;
using YMhut.Box.Core.App;
using YMhut.Box.Core.Data;
using YMhut.Box.Core.Logging;
using YMhut.Box.Core.Net;
using YMhut.Box.Core.Settings;
using YMhut.Box.Core.Tools;

Console.OutputEncoding = Encoding.UTF8;

var pipeName = ReadPipeName(args);
if (string.IsNullOrWhiteSpace(pipeName))
{
    Console.Error.WriteLine("Missing --pipe argument.");
    return 2;
}

var appPaths = AppPaths.ForCurrentUser();
var logService = new JsonFileLogService(appPaths);
var settingsService = new AppSettingsService(new AppSettingsStore(appPaths.Root));
await settingsService.LoadAsync().ConfigureAwait(false);

using var httpService = new HttpService(logService: logService, settingsService: settingsService);
var apiManager = new ApiManager(httpService, logService);
var referenceDataService = new ReferenceDataService(appPaths);
var catalog = new ToolCatalog();
var running = new ConcurrentDictionary<string, CancellationTokenSource>(StringComparer.Ordinal);
var writeGate = new SemaphoreSlim(1, 1);

await using var pipe = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous);
await pipe.ConnectAsync(8000).ConfigureAwait(false);
using var reader = new StreamReader(pipe, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: 4096, leaveOpen: true);
await using var writer = new StreamWriter(pipe, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), bufferSize: 4096, leaveOpen: true)
{
    AutoFlush = true
};

await WriteAsync(new ToolWorkerMessage(ToolWorkerProtocol.Ready, Version: ToolWorkerProtocol.Version), CancellationToken.None)
    .ConfigureAwait(false);
await logService.WriteAsync("Information", "worker", "Tool worker ready").ConfigureAwait(false);

while (await reader.ReadLineAsync().ConfigureAwait(false) is { } line)
{
    var message = ToolWorkerProtocol.Deserialize(line);
    if (message is null)
    {
        continue;
    }

    if (string.Equals(message.Type, ToolWorkerProtocol.Shutdown, StringComparison.Ordinal))
    {
        foreach (var request in running.Values)
        {
            request.Cancel();
        }

        break;
    }

    if (string.Equals(message.Type, ToolWorkerProtocol.Ping, StringComparison.Ordinal))
    {
        await WriteAsync(new ToolWorkerMessage(ToolWorkerProtocol.Pong, message.RequestId), CancellationToken.None)
            .ConfigureAwait(false);
        continue;
    }

    if (string.Equals(message.Type, ToolWorkerProtocol.Cancel, StringComparison.Ordinal))
    {
        if (running.TryGetValue(message.RequestId, out var cancelSource))
        {
            cancelSource.Cancel();
        }

        continue;
    }

    if (string.Equals(message.Type, ToolWorkerProtocol.ExecuteTool, StringComparison.Ordinal))
    {
        StartToolExecution(message);
    }
}

foreach (var request in running.Values)
{
    request.Dispose();
}

return 0;

void StartToolExecution(ToolWorkerMessage message)
{
    var timeout = message.TimeoutMs > 0
        ? TimeSpan.FromMilliseconds(Math.Clamp(message.TimeoutMs, 1_000, 600_000))
        : TimeSpan.FromMinutes(2);
    var cancelSource = new CancellationTokenSource(timeout);
    running[message.RequestId] = cancelSource;

    _ = Task.Run(async () =>
    {
        try
        {
            if (string.IsNullOrWhiteSpace(message.ToolId))
            {
                await WriteAsync(
                    new ToolWorkerMessage(ToolWorkerProtocol.Error, message.RequestId, Error: "Missing tool id."),
                    CancellationToken.None).ConfigureAwait(false);
                return;
            }

            var module = catalog.GetById(message.ToolId);
            if (module is null)
            {
                await WriteAsync(
                    new ToolWorkerMessage(ToolWorkerProtocol.Error, message.RequestId, Error: $"Unknown tool id: {message.ToolId}"),
                    CancellationToken.None).ConfigureAwait(false);
                return;
            }

            await logService.WriteAsync("Information", "worker", $"Executing tool: {module.Id}", cancellationToken: cancelSource.Token)
                .ConfigureAwait(false);
            var result = await ToolExecutor.ExecuteAsync(
                module,
                message.Input ?? string.Empty,
                cancelSource.Token,
                apiManager,
                referenceDataService,
                message.Language ?? "zh-CN").ConfigureAwait(false);

            await WriteAsync(
                new ToolWorkerMessage(
                    ToolWorkerProtocol.Result,
                    message.RequestId,
                    Ok: result.Ok,
                    Output: result.Output,
                    Error: result.Error,
                    Document: result.Document),
                CancellationToken.None).ConfigureAwait(false);
        }
        catch (OperationCanceledException)
        {
            await WriteAsync(
                new ToolWorkerMessage(ToolWorkerProtocol.Error, message.RequestId, Error: "Tool execution was canceled or timed out."),
                CancellationToken.None).ConfigureAwait(false);
        }
        catch (Exception exception)
        {
            await logService.WriteAsync("Error", "worker", "Tool worker execution failed", exception.Message).ConfigureAwait(false);
            await WriteAsync(
                new ToolWorkerMessage(ToolWorkerProtocol.Error, message.RequestId, Error: exception.Message),
                CancellationToken.None).ConfigureAwait(false);
        }
        finally
        {
            if (running.TryRemove(message.RequestId, out var source))
            {
                source.Dispose();
            }
        }
    });
}

async Task WriteAsync(ToolWorkerMessage message, CancellationToken cancellationToken)
{
    await writeGate.WaitAsync(cancellationToken).ConfigureAwait(false);
    try
    {
        await writer.WriteLineAsync(ToolWorkerProtocol.Serialize(message).AsMemory(), cancellationToken).ConfigureAwait(false);
        await writer.FlushAsync(cancellationToken).ConfigureAwait(false);
    }
    finally
    {
        writeGate.Release();
    }
}

static string? ReadPipeName(string[] args)
{
    for (var index = 0; index < args.Length; index++)
    {
        if (string.Equals(args[index], "--pipe", StringComparison.OrdinalIgnoreCase) &&
            index + 1 < args.Length)
        {
            return args[index + 1];
        }
    }

    return null;
}
