package ru.yandex.qe.dispenser.ws.param.batch;

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import com.google.common.base.Stopwatch;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ru.yandex.qe.dispenser.api.v1.DiMetaField;
import ru.yandex.qe.dispenser.api.v1.DiMetaValueSet;
import ru.yandex.qe.dispenser.api.v1.DiQuotaChangeCause;
import ru.yandex.qe.dispenser.api.v1.DiQuotingMode;
import ru.yandex.qe.dispenser.api.v1.request.DiActionType;
import ru.yandex.qe.dispenser.api.v1.request.DiEntity;
import ru.yandex.qe.dispenser.api.v1.request.DiEntityReference;
import ru.yandex.qe.dispenser.api.v1.request.DiEntityUsage;
import ru.yandex.qe.dispenser.api.v1.request.DiOperation;
import ru.yandex.qe.dispenser.api.v1.request.DiProcessingMode;
import ru.yandex.qe.dispenser.api.v1.request.DiResourceAmount;
import ru.yandex.qe.dispenser.domain.Entity;
import ru.yandex.qe.dispenser.domain.EntitySpec;
import ru.yandex.qe.dispenser.domain.Project;
import ru.yandex.qe.dispenser.domain.Resource;
import ru.yandex.qe.dispenser.domain.Segment;
import ru.yandex.qe.dispenser.domain.dao.segment.SegmentUtils;
import ru.yandex.qe.dispenser.domain.distributed.Identifier;
import ru.yandex.qe.dispenser.domain.hierarchy.Hierarchy;
import ru.yandex.qe.dispenser.domain.support.EntityRelease;
import ru.yandex.qe.dispenser.domain.support.EntitySharingRelease;
import ru.yandex.qe.dispenser.domain.support.EntityUsageDiff;
import ru.yandex.qe.dispenser.domain.support.Operation;
import ru.yandex.qe.dispenser.domain.support.QuotaDiff;
import ru.yandex.qe.dispenser.domain.util.CollectionUtils;
import ru.yandex.qe.dispenser.domain.util.ValidationUtils;
import ru.yandex.qe.dispenser.ws.reqbody.QuotaChangeBody;

enum ConversionUtils {
    ;

    private static final Logger LOG = LoggerFactory.getLogger(ConversionUtils.class);

    @NotNull
    static QuotaChangeBody convert(@NotNull final Map<Integer, DiOperation<?>> operations, @NotNull final Context ctx) {
        final Stopwatch stopwatch = Stopwatch.createStarted();
        LOG.debug("Conversion of {} operations started", operations.size());
        final List<Operation<?>> convertedOperations = CollectionUtils.reduce(operations, (id, o) -> convert(id, o, ctx))
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        LOG.debug("Conversion of {} operations finished in {} ms", operations.size(), stopwatch.elapsed(TimeUnit.MILLISECONDS));
        return new QuotaChangeBody(ctx.mode, ctx.service, convertedOperations);
    }

    @Nullable
    private static Operation<?> convert(final int id, @NotNull final DiOperation<?> operation, @NotNull final Context ctx) {
        final DiProcessingMode mode = ctx.mode;
        final DiActionType type = operation.getType();
        final int sign = type.isAboutAcquire() ? 1 : -1;
        final Object action = operation.getAction();
        final Project project = Optional.ofNullable(operation.getPerformer()).map(ctx::getPersonalProject).orElse(null);
        final DiQuotaChangeCause cause = operation.getCause();
        switch (type) {
            case FORCE_ACQUIRE_RESOURCE:
            case RELEASE_RESOURCE:
                ValidationUtils.requireNonNull(project, "Project is required");
                final DiResourceAmount amount = (DiResourceAmount) action;
                final Resource resource = Hierarchy.get().getResourceReader().read(new Resource.Key(amount.getResourceKey(), ctx.service));
                final Set<Segment> segments = SegmentUtils.getCompleteSegmentSet(resource, amount.getSegmentKeys());

                if (resource.getMode() != DiQuotingMode.DEFAULT) {
                    throw new IllegalArgumentException("Operation is not supported: resource '" + resource.getKey() + "' has quoting mode " +
                            resource.getMode() + " instead of " + DiQuotingMode.DEFAULT);
                }
                if (segments.stream().anyMatch(Segment::isAggregationSegment)) {
                    throw new IllegalArgumentException("Cannot acquire/release resource amount with aggregation segments");
                }

                return new Operation<>(id, type, new QuotaDiff(resource, project, sign * resource.getType().getBaseUnit().convert(amount.getAmount()), cause, segments));
            case CREATE_ENTITY:
                ValidationUtils.requireNonNull(project, "Project is required");
                final DiEntity clientEntity = (DiEntity) action;
                final Entity serverEntity = ctx.entities.get(convert((DiEntityReference) clientEntity, ctx));
                Objects.requireNonNull(serverEntity, "BUG: no created entity in context!");
                return new Operation<>(id, type, new EntityUsageDiff(mode, serverEntity, project, clientEntity.getMetaValues(), 1, cause));
            case RELEASE_ENTITY:
                final Entity trashEntity = ctx.entities.get(convert((DiEntityReference) action, ctx));
                if (trashEntity == null && ctx.mode == DiProcessingMode.IGNORE_UNKNOWN_ENTITIES_AND_USAGES) {
                    return null;
                }
                Objects.requireNonNull(trashEntity, "BUG: no present trash entity in context!");
                return new Operation<>(id, type, new EntityRelease(mode, trashEntity, cause));
            case SHARE_ENTITY:
            case RELEASE_ENTITY_SHARING:
                ValidationUtils.requireNonNull(project, "Project is required");
                final DiEntityUsage entityUsage = (DiEntityUsage) action;
                final Entity presentEntity = ctx.entities.get(convert(entityUsage.getEntity(), ctx));
                if (presentEntity == null && ctx.mode == DiProcessingMode.IGNORE_UNKNOWN_ENTITIES_AND_USAGES) {
                    return null;
                }
                Objects.requireNonNull(presentEntity, "BUG: no present entity in context!");
                final int usageDiff = sign * ((DiEntityUsage) action).getUsagesCount();
                return new Operation<>(id, type, new EntityUsageDiff(mode, presentEntity, project, entityUsage.getMetaValues(), usageDiff, cause));
            case RELEASE_ALL_ENTITY_SHARINGS:
                ValidationUtils.requireNonNull(project, "Project is required");
                final Entity unnecessaryEntity = ctx.entities.get(convert((DiEntityReference) action, ctx));
                Objects.requireNonNull(unnecessaryEntity, "BUG: no present unnecessary entity in context!");
                return new Operation<>(id, type, new EntitySharingRelease(mode, unnecessaryEntity, project, cause));
            default:
                throw new UnsupportedOperationException("Unknown action type '" + type + "'!");
        }
    }

    @NotNull
    static Entity convert(@NotNull final DiEntity rawEntity, @NotNull final Context context) {
        final EntitySpec spec = read(rawEntity, context);
        final Map<String, Resource> resources = CollectionUtils.toMap(spec.getResources(), r -> r.getKey().getPublicKey());

        final Entity.Builder builder = Entity.builder(rawEntity.getKey()).spec(spec);
        rawEntity.getDimensions().forEach(d -> {
            final Resource resource = resources.get(d.getResourceKey());
            if (resource == null) {
                final String message = "No resource '" + d.getResourceKey() + "' in entity specification '" + spec.getKey().getPublicKey() + "'!";
                throw new IllegalArgumentException(message);
            }
            final Set<Segment> segments = SegmentUtils.getCompleteSegmentSet(resource, d.getSegmentKeys());
            if (segments.stream().anyMatch(Segment::isAggregationSegment)) {
                throw new IllegalArgumentException("Cannot create entity occupying resource amount with aggregation segments");
            }
            builder.dimension(Entity.ResourceKey.of(resource, segments), d.getAmount());
        });
        builder.ttlIfPresent(rawEntity.getTtl());
        {
            builder.identifier(context.myIdentifier);
            final DiMetaValueSet metaValues = rawEntity.getMetaValues();
            for (final DiMetaField<?> field : metaValues.getKeys()) {
                if ("identifier".equals(field.getKey())) {
                    builder.identifier(Identifier.fromString(metaValues.getValue((DiMetaField<String>) field)));
                }
            }
        }
        return builder.build();
    }

    @NotNull
    static Entity.Key convert(@NotNull final DiEntityReference entityRef, @NotNull final Context context) {
        return new Entity.Key(entityRef.getKey(), read(entityRef, context));
    }

    @NotNull
    private static EntitySpec read(@NotNull final DiEntityReference entity, @NotNull final Context ctx) {
        return Hierarchy.get().getEntitySpecReader().read(new EntitySpec.Key(entity.getSpecificationKey(), ctx.service));
    }
}
