/*
 *******************************************************************************
 * Copyright (c) 2019 Contributors to the Eclipse Foundation
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information regarding copyright ownership.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * You may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *******************************************************************************/
package org.eclipse.microprofile.lra.tck;

import org.eclipse.microprofile.lra.annotation.LRAStatus;
import org.eclipse.microprofile.lra.annotation.ws.rs.LRA;
import org.eclipse.microprofile.lra.tck.participant.api.GenericLRAException;
import org.eclipse.microprofile.lra.tck.service.LRAMetricService;
import org.eclipse.microprofile.lra.tck.service.LRAMetricType;

import javax.ws.rs.client.Entity;
import javax.ws.rs.client.Invocation;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Response;

import java.net.URI;
import java.net.URISyntaxException;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;

import static org.eclipse.microprofile.lra.tck.participant.api.NonParticipatingTckResource.END_PATH;
import static org.eclipse.microprofile.lra.tck.participant.api.NonParticipatingTckResource.START_BUT_DONT_END_PATH;
import static org.eclipse.microprofile.lra.tck.participant.api.NonParticipatingTckResource.STATUS_CODE_QUERY_NAME;
import static org.eclipse.microprofile.lra.tck.participant.api.NonParticipatingTckResource.TCK_NON_PARTICIPANT_RESOURCE_PATH;
import static org.eclipse.microprofile.lra.tck.participant.api.ParticipatingTckResource.JOIN_WITH_EXISTING_LRA_PATH;
import static org.eclipse.microprofile.lra.tck.participant.api.ParticipatingTckResource.LEAVE_PATH;
import static org.eclipse.microprofile.lra.tck.participant.api.ParticipatingTckResource.TCK_PARTICIPANT_RESOURCE_PATH;

public class LRAClientOps {
    private static final Logger LOGGER = Logger.getLogger(TckLRATypeTests.class.getName());

    private final WebTarget target;
    private final ScheduledExecutorService executor;
    private final Map<LRATask, ScheduledFuture<?>> lraTasks;

    public LRAClientOps(WebTarget target) {
        this.target = target;
        this.executor = Executors.newSingleThreadScheduledExecutor();
        this.lraTasks = new HashMap<>();
    }

    // see if it is possible to join with an LRA - if it is possible to do that then the LRA is still active
    private int tryToEnlistWithAnLRA(URI lra) {
        // call a JAX-RS endpoint that should result in the enlistment of a resource into the LRA
        int status = invokeRestEndpointAndReturnStatus(lra, TCK_PARTICIPANT_RESOURCE_PATH, JOIN_WITH_EXISTING_LRA_PATH, 200);

        if (status == 200) {
            // leave the LRA otherwise any tests checking completion/compensation counts would fail

            leaveLRA(lra, TCK_PARTICIPANT_RESOURCE_PATH, LEAVE_PATH);

        }

        return status;
    }

    boolean isLRAFinished(URI lra) {
        // if the LRA has finished/finishing or does not exist 412 or 410 MUST be be reported
        int status = tryToEnlistWithAnLRA(lra);

        return status == Response.Status.GONE.getStatusCode() || status == Response.Status.PRECONDITION_FAILED.getStatusCode();
    }

    /**
     * TODO if the current PR is acceptable then delete the old isLRAFinished method
     * (which tests whether an LRA is active by making an attempt to enlist with it
     * which some spec implementations report as a stack trace WARNING if the LRA is
     * no longer active).
     *
     * @param lra the LRA to test
     * @param lraMetricService metrics
     * @param resourceName name of the resource that the metrics parameter applies to
     * @return whether or not an LRA has finished
     */
    boolean isLRAFinished(URI lra, LRAMetricService lraMetricService, String resourceName) {
        return getLRAEndStatus(lra, lraMetricService, resourceName) != null;
    }

    /**
     *
     * @param lra the LRA whose end status is being queried
     * @param lraMetricService the metrics service
     * @param resourceName the name of the resource whose metrics hold the end state of the LRA
     * @return the end status of the LRA (or null if the LRA has not yet finished)
     */
    private LRAStatus getLRAEndStatus(URI lra, LRAMetricService lraMetricService, String resourceName) {
        if (lraMetricService.getMetric(LRAMetricType.Closed, lra, resourceName) >= 1) {
            return LRAStatus.Closed;
        } else if (lraMetricService.getMetric(LRAMetricType.FailedToClose, lra, resourceName) >= 1) {
            return LRAStatus.FailedToClose;
        } else if (lraMetricService.getMetric(LRAMetricType.Cancelled, lra, resourceName) >= 1) {
            return LRAStatus.Cancelled;
        } else if (lraMetricService.getMetric(LRAMetricType.FailedToCancel, lra, resourceName) >= 1) {
            return LRAStatus.FailedToCancel;
        }

        return null;
    }

    // synchronize access to the connection since it is shared with the LRA background cancellation code
    private synchronized Response invokeRestEndpoint(URI lra, String basePath, String path, int coerceResponse) {
        WebTarget resourcePath = target.path(basePath).path(path).queryParam(STATUS_CODE_QUERY_NAME, coerceResponse);
        Invocation.Builder builder = resourcePath.request();

        if (lra != null) {
            builder.header(LRA.LRA_HTTP_CONTEXT_HEADER, lra);
        }

        return builder.put(Entity.text(""));
    }

    public String invokeRestEndpointAndReturnLRA(URI lra, String basePath, String path, int coerceResponse) {
        Response response = invokeRestEndpoint(lra, basePath, path, coerceResponse);

        try {
            return response.readEntity(String.class);
        } finally {
            response.close();
        }
    }

    private int invokeRestEndpointAndReturnStatus(URI lra, String basePath, String path, int coerceResponse) {
        Response response = invokeRestEndpoint(lra, basePath, path, coerceResponse);

        try {
            return response.getStatus();
        } finally {
            response.close();
        }
    }

    private URI toURI(String lra) throws GenericLRAException {
        try {
            return new URI(lra);
        } catch (URISyntaxException e) {
            throw new GenericLRAException(null, e.getMessage(), e);
        }
    }

    public URI startLRA(URI parentLRA, String clientID, long timeout, ChronoUnit unit)
            throws GenericLRAException {
        String lra = invokeRestEndpoint(parentLRA,
                TCK_NON_PARTICIPANT_RESOURCE_PATH, START_BUT_DONT_END_PATH, 200)
                .readEntity(String.class);

        if (timeout > 0L) {
            scheduleCancelation(clientID, toURI(lra), timeout, unit);
        }

        return toURI(lra);
    }

    public void cancelLRA(URI lraId) throws GenericLRAException {
        cancelCancelation(lraId);

        invokeRestEndpointAndReturnLRA(lraId, TCK_NON_PARTICIPANT_RESOURCE_PATH, END_PATH, 500);
    }

    private void cancelLRA(String clientId, URI lra) {
        LOGGER.warning("cancelling LRA from the timer: clientId: " + clientId + " LRA id: " + lra.toASCIIString());
        cancelLRA(lra);
        throw new IllegalArgumentException("LRA timed out prematurely");
    }

    public void closeLRA(URI lraId) throws GenericLRAException {
        cancelCancelation(lraId);

        invokeRestEndpointAndReturnLRA(lraId, TCK_NON_PARTICIPANT_RESOURCE_PATH, END_PATH, 200);
    }

    void closeLRA(String lraId) {
        closeLRA(toURI(lraId));
    }

    private void leaveLRA(URI lra, String basePath, String resourcePath) throws GenericLRAException {
        invokeRestEndpoint(lra, basePath, resourcePath, 200);
    }

    /*
     * Include support for timing out LRAs. A timed out LRA will generally produce a test failure.
     */

    private static TimeUnit timeUnit(ChronoUnit unit) {
        switch (unit) {
            case NANOS:
                return TimeUnit.NANOSECONDS;
            case MICROS:
                return TimeUnit.MICROSECONDS;
            case MILLIS:
                return TimeUnit.MILLISECONDS;
            case SECONDS:
                return TimeUnit.SECONDS;
            case MINUTES:
                return TimeUnit.MINUTES;
            case HOURS:
                return TimeUnit.HOURS;
            case DAYS:
                return TimeUnit.DAYS;
            default:
                throw new IllegalArgumentException("ChronoUnit cannot be converted to TimeUnit: " + unit);
        }
    }

    /**
     * arrange for an LRA to be automatically cancelled after a specified timeout
     *
     * @param clientId client assigned arbitrary identifier for the LRA
     * @param lra the LRA that should be cancelled when the time limit is reached
     * @param timeout the time to wait before attempting cancellation of the LRA
     * @param unit the time unit
     */
    private void scheduleCancelation(String clientId, URI lra, long timeout, ChronoUnit unit) {
        lraTasks.put(new LRATask(clientId, lra), executor.schedule(() -> cancelLRA(clientId, lra), timeout, timeUnit(unit)));
    }

    private void cancelCancelation(URI lraId) {
        LRATask lra = new LRATask(null, lraId);

        if (lraTasks.containsKey(lra)) {
            lraTasks.remove(lra).cancel(false);
        }
    }

    void cleanUp(Logger logger, String testName) {
        lraTasks.forEach((lra, future) -> {
            logger.warning("Test: " + testName + " didn't finish LRA " + lra.lra + " with clientId " + lra.clientId);
            cancelLRA(lra.lra);
        });
    }

    // class to store the clientId associated with an LRA
    private class LRATask {
        final String clientId; // client assigned arbitrary identifier for the LRA
        final URI lra; // the LRA that clientId is associated with

        LRATask(String clientId, URI lra) {
            this.clientId = clientId;
            this.lra = lra;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            LRATask lraTask = (LRATask) o;
            return lra.equals(lraTask.lra);
        }

        @Override
        public int hashCode() {
            return Objects.hash(lra);
        }
    }
}