using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Threading.Tasks;

using FrostFS.Container;
using FrostFS.Refs;
using FrostFS.SDK.ClientV2;
using FrostFS.SDK.ClientV2.Mappers.GRPC;
using FrostFS.SDK.Cryptography;
using FrostFS.Session;

namespace FrostFS.SDK.ClientV2;

internal sealed class ContainerServiceProvider(ContainerService.ContainerServiceClient service, EnvironmentContext envCtx) : ContextAccessor(envCtx), ISessionProvider
{
    readonly SessionProvider sessions = new(envCtx);

    public async ValueTask<SessionToken> GetOrCreateSession(ISessionToken args, CallContext ctx)
    {
        return await sessions.GetOrCreateSession(args, ctx).ConfigureAwait(false);
    }

    internal async Task<FrostFsContainerInfo> GetContainerAsync(PrmContainerGet args)
    {
        GetRequest request = GetContainerRequest(args.Container.ContainerID, args.XHeaders, args.Context!);

        var response = await service.GetAsync(request, null, args.Context!.Deadline, args.Context.CancellationToken);

        Verifier.CheckResponse(response);

        return response.Body.Container.ToModel();
    }

    internal async IAsyncEnumerable<FrostFsContainerId> ListContainersAsync(PrmContainerGetAll args)
    {
        var ctx = args.Context!;
        ctx.OwnerId ??= EnvironmentContext.Owner;
        ctx.Key ??= EnvironmentContext.Key?.ECDsaKey;

        if (ctx.Key == null)
            throw new InvalidObjectException(nameof(ctx.Key));
        if (ctx.OwnerId == null)
            throw new InvalidObjectException(nameof(ctx.OwnerId));

        var request = new ListRequest
        {
            Body = new()
            {
                OwnerId = ctx.OwnerId.ToMessage()
            }
        };

        request.AddMetaHeader(args.XHeaders);
        request.Sign(ctx.Key);

        var response = await service.ListAsync(request, null, ctx.Deadline, ctx.CancellationToken);

        Verifier.CheckResponse(response);

        foreach (var cid in response.Body.ContainerIds)
        {
            yield return new FrostFsContainerId(Base58.Encode(cid.Value.ToByteArray()));
        }
    }

    internal async Task<FrostFsContainerId> CreateContainerAsync(PrmContainerCreate args)
    {
        var ctx = args.Context!;

        var grpcContainer = args.Container.GetContainer();

        grpcContainer.OwnerId ??= ctx.OwnerId?.ToMessage();
        grpcContainer.Version ??= ctx.Version?.ToMessage();

        if (ctx.Key == null)
            throw new InvalidObjectException(nameof(ctx.Key));
        if (grpcContainer.OwnerId == null)
            throw new InvalidObjectException(nameof(grpcContainer.OwnerId));
        if (grpcContainer.Version == null)
            throw new InvalidObjectException(nameof(grpcContainer.Version));

        var request = new PutRequest
        {
            Body = new PutRequest.Types.Body
            {
                Container = grpcContainer,
                Signature = ctx.Key.SignRFC6979(grpcContainer)
            }
        };

        var sessionToken = await GetOrCreateSession(args, ctx).ConfigureAwait(false);

        sessionToken.CreateContainerTokenContext(
            null,
            ContainerSessionContext.Types.Verb.Put,
            ctx.Key,
            ctx.GetPublicKeyCache()!);

        request.AddMetaHeader(args.XHeaders, sessionToken);

        request.Sign(ctx.Key);

        var response = await service.PutAsync(request, null, ctx.Deadline, ctx.CancellationToken);

        Verifier.CheckResponse(response);

        await WaitForContainer(WaitExpects.Exists, response.Body.ContainerId, args.WaitParams, ctx).ConfigureAwait(false);

        return new FrostFsContainerId(response.Body.ContainerId);
    }

    internal async Task DeleteContainerAsync(PrmContainerDelete args)
    {
        var ctx = args.Context!;
        if (ctx.Key == null)
            throw new InvalidObjectException(nameof(ctx.Key));

        var request = new DeleteRequest
        {
            Body = new DeleteRequest.Types.Body
            {
                ContainerId = args.ContainerId.ToMessage(),
                Signature = ctx.Key.SignRFC6979(args.ContainerId.ToMessage().Value)
            }
        };

        var sessionToken = await GetOrCreateSession(args, ctx).ConfigureAwait(false);

        sessionToken.CreateContainerTokenContext(
            request.Body.ContainerId,
            ContainerSessionContext.Types.Verb.Delete,
            ctx.Key,
            ctx.GetPublicKeyCache()!);

        request.AddMetaHeader(args.XHeaders, sessionToken);

        request.Sign(ctx.Key);

        var response = await service.DeleteAsync(request, null, ctx.Deadline, ctx.CancellationToken);

        Verifier.CheckResponse(response);

        await WaitForContainer(WaitExpects.Removed, request.Body.ContainerId, args.WaitParams, ctx)
            .ConfigureAwait(false);

        Verifier.CheckResponse(response);
    }

    private static GetRequest GetContainerRequest(ContainerID id, NameValueCollection? xHeaders, CallContext ctx)
    {
        if (ctx.Key == null)
            throw new InvalidObjectException(nameof(ctx.Key));

        var request = new GetRequest
        {
            Body = new GetRequest.Types.Body
            {
                ContainerId = id
            }
        };

        request.AddMetaHeader(xHeaders);
        request.Sign(ctx.Key);

        return request;
    }

    private enum WaitExpects
    {
        Exists,
        Removed
    }

    private async Task WaitForContainer(WaitExpects expect, ContainerID id, PrmWait? waitParams, CallContext ctx)
    {
        var request = GetContainerRequest(id, null, ctx);

        async Task action()
        {
            var response = await service.GetAsync(request, null, ctx.Deadline, ctx.CancellationToken);
            Verifier.CheckResponse(response);
        }

        await WaitFor(action, expect, waitParams).ConfigureAwait(false);
    }

    private static async Task WaitFor(
        Func<Task> action,
        WaitExpects expect,
        PrmWait? waitParams)
    {
        waitParams ??= PrmWait.DefaultParams;
        var deadLine = waitParams.GetDeadline();

        while (true)
        {
            try
            {
                await action().ConfigureAwait(false);

                if (expect == WaitExpects.Exists)
                    return;

                if (DateTime.UtcNow >= deadLine)
                    throw new TimeoutException();

                await Task.Delay(waitParams.PollInterval).ConfigureAwait(false);
            }
            catch (ResponseException ex)
            {
                if (DateTime.UtcNow >= deadLine)
                    throw new TimeoutException();

                if (ex.Status?.Code != FrostFsStatusCode.ContainerNotFound)
                    throw;

                if (expect == WaitExpects.Removed)
                    return;

                await Task.Delay(waitParams.PollInterval).ConfigureAwait(false);
            }
        }
    }
}