package com.linbit.linstor.core.apicallhandler.controller.internal;

import com.linbit.ImplementationError;
import com.linbit.InvalidNameException;
import com.linbit.linstor.InternalApiConsts;
import com.linbit.linstor.annotation.ApiContext;
import com.linbit.linstor.annotation.PeerContext;
import com.linbit.linstor.api.ApiCallRc;
import com.linbit.linstor.api.ApiCallRcImpl;
import com.linbit.linstor.api.ApiConsts;
import com.linbit.linstor.api.interfaces.serializer.CtrlStltSerializer;
import com.linbit.linstor.api.pojo.CapacityInfoPojo;
import com.linbit.linstor.core.CoreModule;
import com.linbit.linstor.core.apicallhandler.controller.CtrlApiDataLoader;
import com.linbit.linstor.core.apicallhandler.controller.CtrlStorPoolApiCallHandler;
import com.linbit.linstor.core.apicallhandler.controller.CtrlTransactionHelper;
import com.linbit.linstor.core.apicallhandler.response.ApiAccessDeniedException;
import com.linbit.linstor.core.apicallhandler.response.ApiOperation;
import com.linbit.linstor.core.apicallhandler.response.ApiRcException;
import com.linbit.linstor.core.apicallhandler.response.ResponseContext;
import com.linbit.linstor.core.apicallhandler.response.ResponseConverter;
import com.linbit.linstor.core.identifier.StorPoolName;
import com.linbit.linstor.core.objects.Node;
import com.linbit.linstor.core.objects.StorPool;
import com.linbit.linstor.dbdrivers.DatabaseException;
import com.linbit.linstor.logging.ErrorReporter;
import com.linbit.linstor.netcom.Peer;
import com.linbit.linstor.security.AccessContext;
import com.linbit.linstor.security.AccessDeniedException;
import com.linbit.locks.LockGuard;

import static com.linbit.linstor.core.apicallhandler.controller.helpers.StorPoolHelper.getStorPoolDescriptionInline;

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.locks.ReadWriteLock;

public class StorPoolInternalCallHandler
{
    private final ErrorReporter errorReporter;
    private final AccessContext apiCtx;
    private final CtrlTransactionHelper ctrlTransactionHelper;
    private final CtrlApiDataLoader ctrlApiDataLoader;
    private final ResponseConverter responseConverter;
    private final CtrlStltSerializer ctrlStltSerializer;
    private final Provider<Peer> peer;
    private final Provider<AccessContext> peerAccCtx;

    private final ReadWriteLock nodesMapLock;
    private final ReadWriteLock storPoolDfnMapLock;

    @Inject
    public StorPoolInternalCallHandler(
        ErrorReporter errorReporterRef,
        @ApiContext AccessContext apiCtxRef,
        CtrlTransactionHelper ctrlTransactionHelperRef,
        CtrlApiDataLoader ctrlApiDataLoaderRef,
        ResponseConverter responseConverterRef,
        CtrlStltSerializer ctrlStltSerializerRef,
        Provider<Peer> peerRef,
        @PeerContext Provider<AccessContext> peerAccCtxRef,
        @Named(CoreModule.NODES_MAP_LOCK) ReadWriteLock nodesMapLockRef,
        @Named(CoreModule.STOR_POOL_DFN_MAP_LOCK) ReadWriteLock storPoolDfnMapLockRef
    )
    {
        errorReporter = errorReporterRef;
        apiCtx = apiCtxRef;
        ctrlTransactionHelper = ctrlTransactionHelperRef;
        ctrlApiDataLoader = ctrlApiDataLoaderRef;
        responseConverter = responseConverterRef;
        ctrlStltSerializer = ctrlStltSerializerRef;
        peer = peerRef;
        peerAccCtx = peerAccCtxRef;
        nodesMapLock = nodesMapLockRef;
        storPoolDfnMapLock = storPoolDfnMapLockRef;
    }

    public void handleStorPoolRequest(UUID storPoolUuid, String storPoolNameStr)
    {
        try (
            LockGuard ls = LockGuard.createLocked(
                nodesMapLock.readLock(),
                storPoolDfnMapLock.readLock(),
                peer.get().getSerializerLock().readLock()
            )
        )
        {
            StorPoolName storPoolName = new StorPoolName(storPoolNameStr);

            Peer currentPeer = peer.get();
            StorPool storPool = currentPeer.getNode().getStorPool(apiCtx, storPoolName);
            // TODO: check if the storPool has the same uuid as storPoolUuid
            if (storPool != null)
            {
                long fullSyncTimestamp = currentPeer.getFullSyncId();
                long updateId = currentPeer.getNextSerializerId();
                currentPeer.sendMessage(
                    ctrlStltSerializer
                        .onewayBuilder(InternalApiConsts.API_APPLY_STOR_POOL)
                        .storPool(storPool, fullSyncTimestamp, updateId)
                        .build()
                );
            }
            else
            {
                long fullSyncTimestamp = currentPeer.getFullSyncId();
                long updateId = currentPeer.getNextSerializerId();
                currentPeer.sendMessage(
                    ctrlStltSerializer
                        .onewayBuilder(InternalApiConsts.API_APPLY_STOR_POOL_DELETED)
                        .deletedStorPool(storPoolNameStr, fullSyncTimestamp, updateId)
                        .build()
                );
            }
        }
        catch (InvalidNameException invalidNameExc)
        {
            errorReporter.reportError(
                new ImplementationError(
                    "Satellite requested data for invalid storpool name '" + storPoolNameStr + "'.",
                    invalidNameExc
                )
            );
        }
        catch (AccessDeniedException accDeniedExc)
        {
            errorReporter.reportError(
                new ImplementationError(
                    "Controller's api context has not enough privileges to gather requested storpool data.",
                    accDeniedExc
                )
            );
        }
    }

    private StorPool loadStorPool(String nodeNameStr, String storPoolNameStr, boolean failIfNull)
    {
        return ctrlApiDataLoader.loadStorPool(
            ctrlApiDataLoader.loadStorPoolDfn(storPoolNameStr, true),
            ctrlApiDataLoader.loadNode(nodeNameStr, true),
            failIfNull
        );
    }

    private void setCapacityInfo(StorPool storPool, long freeCapacity, long totalCapacity)
    {
        try
        {
            storPool.getFreeSpaceTracker().setCapacityInfo(peerAccCtx.get(), freeCapacity, totalCapacity);
        }
        catch (AccessDeniedException accDeniedExc)
        {
            throw new ApiAccessDeniedException(
                accDeniedExc,
                "update free space of free space manager '" +
                    storPool.getFreeSpaceTracker().getName().displayValue +
                    "'",
                ApiConsts.FAIL_ACC_DENIED_FREE_SPACE_MGR
            );
        }
    }

    public void updateRealFreeSpace(List<CapacityInfoPojo> capacityInfoPojoList)
    {
        updateRealFreeSpace(peer.get(), capacityInfoPojoList);
    }

    public void updateRealFreeSpace(Peer peerRef, List<CapacityInfoPojo> capacityInfoPojoList)
    {
        try (LockGuard ls = LockGuard.createLocked(nodesMapLock.writeLock(), storPoolDfnMapLock.writeLock()))
        {
            Node node = peerRef.getNode();
            if (!node.isDeleted())
            {
                String nodeName = node.getName().displayValue;

                try
                {
                    for (CapacityInfoPojo capacityInfoPojo : capacityInfoPojoList)
                    {
                        ResponseContext context = CtrlStorPoolApiCallHandler.makeStorPoolContext(
                            ApiOperation.makeModifyOperation(),
                            nodeName,
                            capacityInfoPojo.getStorPoolName()
                        );

                        try
                        {
                            StorPool storPool = loadStorPool(nodeName, capacityInfoPojo.getStorPoolName(), true);
                            if (storPool.getUuid().equals(capacityInfoPojo.getStorPoolUuid()))
                            {
                                storPool.clearReports();
                                storPool.addReports(capacityInfoPojo.getErrors());
                                setCapacityInfo(
                                    storPool,
                                    capacityInfoPojo.getFreeCapacity(),
                                    capacityInfoPojo.getTotalCapacity()
                                );
                            }
                            else
                            {
                                throw new ApiRcException(ApiCallRcImpl.simpleEntry(
                                    ApiConsts.FAIL_UUID_STOR_POOL,
                                    "UUIDs mismatched when updating free space of " +
                                    getStorPoolDescriptionInline(storPool)
                                ));
                            }
                        }
                        catch (Exception | ImplementationError exc)
                        {
                            // Add context to exception
                            throw new ApiRcException(
                                responseConverter.exceptionToResponse(peerRef, context, exc), exc, true);
                        }
                    }

                    ctrlTransactionHelper.commit();
                }
                catch (ApiRcException exc)
                {
                    ApiCallRc apiCallRc = exc.getApiCallRc();
                    for (ApiCallRc.RcEntry entry : apiCallRc.getEntries())
                    {
                        errorReporter.reportError(
                            exc.getCause() != null ? exc.getCause() : exc,
                            peerAccCtx.get(),
                            peerRef,
                            entry.getMessage()
                        );
                    }
                }
            }
        }
        // else: the node is deleted, thus if it still has any storpools left, those will
        // soon be deleted as well.
    }

    public void handleStorPoolApplied(
        String storPoolNameRef,
        boolean supportsSnapshotsRef,
        CapacityInfoPojo capacityInfoPojoRef
    )
    {
        Peer currentPeer = peer.get();
        StorPool storPool;
        try
        {
            storPool = (StorPool) currentPeer.getNode().getStorPool(apiCtx, new StorPoolName(storPoolNameRef));
            storPool.setSupportsSnapshot(apiCtx, supportsSnapshotsRef);
        }
        catch (AccessDeniedException | InvalidNameException | DatabaseException exc)
        {
            throw new ImplementationError(exc);
        }
        updateRealFreeSpace(currentPeer, Collections.singletonList(capacityInfoPojoRef));
    }
}