/*
 * Copyright (C) 2015-2016 Authlete, Inc.
 *
 * 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 com.authlete.jaxrs;

import java.util.Map;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import com.authlete.common.api.AuthleteApi;
import com.authlete.common.dto.AuthorizationFailRequest.Reason;
import com.authlete.common.dto.AuthorizationResponse;
import com.authlete.common.dto.Property;
import com.authlete.jaxrs.spi.AuthorizationRequestHandlerSpi;

/**
 * Handler for authorization requests to a authorization endpoint
 * of OAuth 2.0 (RFC 6749).
 *
 *

 * In an implementation of authorization endpoint, call {@link
 * #handle(MultivaluedMap) handle()} method and use the response as the response
 * from the endpoint to the client application. {@code handle()} method calls
 * Authlete's {@code /api/auth/authorization} API, receives a response from
 * the API, and dispatches processing according to the {@code action} parameter
 * in the response.
 *

 *
 * @author Takahiko Kawasaki
 */
public class AuthorizationRequestHandler extends BaseHandler
{
    /**
     * Implementation of {@link AuthorizationRequestHandlerSpi} interface.
     */
    private final AuthorizationRequestHandlerSpi mSpi;

    /**
     * Constructor with an implementation of {@link AuthleteApi} interface
     * and an implementation of {@link AuthorizationRequestHandlerSpi} interface.
     *
     * @param api
     *         Implementation of {@link AuthleteApi} interface.
     *
     * @param spi
     *         Implementation of {@link AuthorizationRequestHandlerSpi} interface.
     */
    public AuthorizationRequestHandler(AuthleteApi api, AuthorizationRequestHandlerSpi spi)
    {
        super(api);

        mSpi = spi;
    }

    /**
     * Handle an authorization request to a authorization endpoint
     * of OAuth 2.0 (RFC 6749).
     *
     * @param parameters
     *         Request parameters of an authorization request.
     *
     * @return
     *         A response that should be returned from the endpoint to the
     *         client application.
     *
     * @throws WebApplicationException
     *         An error occurred.
     */
    public Response handle(MultivaluedMap<String, String> parameters) throws WebApplicationException
    {
        try
        {
            // Process the given parameters.
            return process(parameters);
        }
        catch (WebApplicationException e)
        {
            throw e;
        }
        catch (Throwable t)
        {
            // Unexpected error.
            throw unexpected("Unexpected error in AuthorizationRequestHandler", t);
        }
    }

    /**
     * Process the authorization request.
     */
    private Response process(MultivaluedMap<String, String> parameters)
    {
        // Call Authlete's /api/auth/authorization API.
        AuthorizationResponse response = getApiCaller().callAuthorization(parameters);

        // 'action' in the response denotes the next action which
        // this service implementation should take.
        AuthorizationResponse.Action action = response.getAction();

        // The content of the response to the client application.
        // The format of the content varies depending on the action.
        String content = response.getResponseContent();

        // Dispatch according to the action.
        switch (action)
        {
            case INTERNAL_SERVER_ERROR:
                // 500 Internal Server Error
                return ResponseUtil.internalServerError(content);

            case BAD_REQUEST:
                // 400 Bad Request
                return ResponseUtil.badRequest(content);

            case LOCATION:
                // 302 Found
                return ResponseUtil.location(content);

            case FORM:
                // 200 OK
                return ResponseUtil.form(content);

            case INTERACTION:
                // Process the authorization request with user interaction.
                return handleInteraction(response);

            case NO_INTERACTION:
                // Process the authorization request without user interaction.
                // The flow reaches here only when the authorization request
                // contained prompt=none.
                return handleNoInteraction(response);

            default:
                // This never happens.
                throw getApiCaller().unknownAction("/api/auth/authorization", action);
        }
    }

    /**
     * Handle the case where {@code action} parameter in a response from
     * Authlete's {@code /api/auth/authorization} API is {@code INTERACTION}.
     */
    private Response handleInteraction(AuthorizationResponse response)
    {
        return mSpi.generateAuthorizationPage(response);
    }

    /**
     * Handle the case where {@code action} parameter in a response from
     * Authlete's {@code /api/auth/authorization} API is {@code NO_INTERACTION}.
     */
    private Response handleNoInteraction(AuthorizationResponse response)
    {
        // Check 1. End-User Authentication
        noInteractionCheckAuthentication(response);

        // Get the time when the user was authenticated.
        long authTime = mSpi.getUserAuthenticatedAt();

        // Check 2. Max Age
        noInteractionCheckMaxAge(response, authTime);

        // The current subject, i.e. the unique ID assigned by
        // the service to the current user.
        String subject = mSpi.getUserSubject();

        // Check 3. Subject
        noInteractionCheckSubject(response, subject);

        // Get the ACR that was satisfied when the current user
        // was authenticated.
        String acr = mSpi.getAcr();

        // Check 4. ACR
        noInteractionCheckAcr(response, acr);

        // Extra properties to associate with an access token and/or
        // an authorization code.
        Property[] properties = mSpi.getProperties();

        // Scopes to associate with an access token and/or an authorization code.
        // If a non-null value is returned from mSpi.getScopes(), the scope set
        // replaces the scopes that have been specified in the original
        // authorization request.
        String[] scopes = mSpi.getScopes();

        // Issue
        return noInteractionIssue(response, authTime, subject, acr, properties, scopes);
    }

    /**
     * Check whether an end-user has already logged in or not.
     */
    private void noInteractionCheckAuthentication(AuthorizationResponse response)
    {
        // If the current user has already been authenticated.
        if (mSpi.isUserAuthenticated())
        {
            // OK.
            return;
        }

        // A user must have logged in.
        throw getApiCaller().authorizationFail(response.getTicket(), Reason.NOT_LOGGED_IN);
    }

    private void noInteractionCheckMaxAge(AuthorizationResponse response, long authTime)
    {
        // Get the requested maximum authentication age.
        int maxAge = response.getMaxAge();

        // If no maximum authentication age is requested.
        if (maxAge == 0)
        {
            // No check is needed.
            return;
        }

        // The time at which the authentication expires.
        long expiresAtMillis = (authTime + maxAge) * 1000L;

        // If the authentication has not expired yet.
        if (System.currentTimeMillis() < expiresAtMillis)
        {
            // OK.
            return;
        }

        // The maximum authentication age has elapsed.
        throw getApiCaller().authorizationFail(response.getTicket(), Reason.EXCEEDS_MAX_AGE);
    }

    private void noInteractionCheckSubject(AuthorizationResponse response, String subject)
    {
        // Get the requested subject.
        String requestedSubject = response.getSubject();

        // If no subject is requested.
        if (requestedSubject == null)
        {
            // No check is needed.
            return;
        }

        // If the requested subject matches the current user.
        if (requestedSubject.equals(subject))
        {
            // OK.
            return;
        }

        // The current user is different from the requested subject.
        throw getApiCaller().authorizationFail(response.getTicket(), Reason.DIFFERENT_SUBJECT);
    }

    private void noInteractionCheckAcr(AuthorizationResponse response, String acr)
    {
        // Get the list of requested ACRs.
        String[] requestedAcrs = response.getAcrs();

        // If no ACR is requested.
        if (requestedAcrs == null || requestedAcrs.length == 0)
        {
            // No check is needed.
            return;
        }

        for (String requestedAcr : requestedAcrs)
        {
            if (requestedAcr.equals(acr))
            {
                // OK. The ACR satisfied when the current user was
                // authenticated matches one of the requested ACRs.
                return;
            }
        }

        // If one of the requested ACRs must be satisfied.
        if (response.isAcrEssential())
        {
            // None of the requested ACRs is satisfied.
            throw getApiCaller().authorizationFail(response.getTicket(), Reason.ACR_NOT_SATISFIED);
        }

        // The ACR satisfied when the current user was authenticated
        // does not match any one of the requested ACRs, but the
        // authorization request from the client application did
        // not request ACR as essential. Therefore, it is not
        // necessary to raise an error here.
    }

    private Response noInteractionIssue(
            AuthorizationResponse response, long authTime, String subject,
            String acr, Property[] properties, String[] scopes)
    {
        // When prompt=none is contained in an authorization request,
        // response.getClaims() returns null. This means that user
        // claims don't have to be collected. In other words, if an
        // authorization request contains prompt=none and requests
        // user claims at the same time, Authlete regards such a
        // request as illegal, because Authlete does not provide any
        // means to pre-configure consent for claims.
        //
        // See the description about prompt=none in "OpenID Connect
        // Core 1.0, 3.1.2.1. Authentication Request" for details.

        return getApiCaller().authorizationIssue(
            response.getTicket(), subject, authTime, acr,
            (Map<String, Object>)null, properties, scopes);
    }
}


/*
 * Copyright (C) 2015-2016 Authlete, Inc.
 *
 * 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 com.authlete.jaxrs;

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import com.authlete.common.api.AuthleteApi;
import com.authlete.common.dto.RevocationResponse;
import com.authlete.common.dto.RevocationResponse.Action;
import com.authlete.common.web.BasicCredentials;

/**
 * Handler for token revocation requests
 * (RFC 7009).
 *
 *

 * In an implementation of revocation endpoint, call {@link #handle(MultivaluedMap, String)
 * handle()} method and use the response as the response from the endpoint to the client
 * application. {@code handle()} method calls Authlete's {@code /api/auth/revocation} API,
 * receives a response from the API, and dispatches processing according to the
 * {@code action} parameter in the response.
 *

 *
 * @since 1.1
 *
 * @author Takahiko Kawasaki
 */
public class RevocationRequestHandler extends BaseHandler
{
    /**
     * The value for {@code WWW-Authenticate} header on 401 Unauthorized.
     */
    private static final String CHALLENGE = "Basic realm=\"revocation\"";

    /**
     * Constructor with an implementation of {@link AuthleteApi} interface.
     *
     * @param api
     *         Implementation of {@link AuthleteApi} interface.
     */
    public RevocationRequestHandler(AuthleteApi api)
    {
        super(api);
    }

    /**
     * Handle a token revocation request (RFC 7009).
     *
     * @param parameters
     *         Request parameters of a token revocation request.
     *
     * @param authorization
     *         The value of {@code Authorization} header in the token revocation
     *         request. A client application may embed its pair of client ID and
     *         client secret in a token revocation request using Basic
     *         Authentication.
     *
     * @return
     *         A response that should be returned from the endpoint to the
     *         client application.
     *
     * @throws WebApplicationException
     *         An error occurred.
     */
    public Response handle(MultivaluedMap<String, String> parameters, String authorization) throws WebApplicationException
    {
        // Convert the value of Authorization header (credentials of
        // the client application), if any, into BasicCredentials.
        BasicCredentials credentials = BasicCredentials.parse(authorization);

        // The credentials of the client application extracted from
        // 'Authorization' header. These may be null.
        String clientId     = credentials == null ? null : credentials.getUserId();
        String clientSecret = credentials == null ? null : credentials.getPassword();

        try
        {
            // Process the given parameters.
            return process(parameters, clientId, clientSecret);
        }
        catch (WebApplicationException e)
        {
            throw e;
        }
        catch (Throwable t)
        {
            // Unexpected error.
            throw unexpected("Unexpected error in RevocationRequestHandler", t);
        }
    }

    /**
     * Process the parameters of the revocation request.
     */
    private Response process(MultivaluedMap<String, String> parameters, String clientId, String clientSecret)
    {
        // Call Authlete's /api/auth/revocation API.
        RevocationResponse response = getApiCaller().callRevocation(parameters, clientId, clientSecret);

        // 'action' in the response denotes the next action which
        // this service implementation should take.
        Action action = response.getAction();

        // The content of the response to the client application.
        String content = response.getResponseContent();

        // Dispatch according to the action.
        switch (action)
        {
            case INVALID_CLIENT:
                // 401 Unauthorized
                return ResponseUtil.unauthorized(content, CHALLENGE);

            case INTERNAL_SERVER_ERROR:
                // 500 Internal Server Error
                return ResponseUtil.internalServerError(content);

            case BAD_REQUEST:
                // 400 Bad Request
                return ResponseUtil.badRequest(content);

            case OK:
                // 200 OK
                return ResponseUtil.javaScript(content);

            default:
                // This never happens.
                throw getApiCaller().unknownAction("/api/auth/revocation", action);
        }
    }
}


/*
 * Copyright (C) 2016-2018 Authlete, Inc.
 *
 * 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 com.authlete.jaxrs;

import java.util.Arrays;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.WebApplicationException;

/**
 * A base class for endpoints.
 *
 * @since 1.2
 *
 * @author Takahiko Kawasaki
 */
public class BaseEndpoint
{
    private List<ClientCertificateExtractor> clientCertificateExtractors = Arrays.asList(
            new HttpsRequestClientCertificateExtractor(),
            new HeaderClientCertificateExtractor()
            );

    /**
     * Called when the internal request handler raises an exception.
     * The default implementation of this method does nothing.
     * Override this method as necessary.
     *
     * @param exception
     *         An exception thrown by the internal request handler.
     */
    protected void onError(WebApplicationException exception)
    {
    }

    /**
     * Utility method for extracting a single client certificate from the default
     * certificate extractors. First checks the request itself for an attached
     * certificate using {@link javax.servlet.request.X509Certificate}, then
     * checks the incoming request headers for reverse-proxied certificates
     * using default headers.
     *
     * @see ClientCertificateExtractor
     *
     * @param request
     *          The incoming HTTP request to search for the client's certificate.
     *
     * @return
     *          The client's mutual TLS certificate.
     *
     * @since 2.8
     */
    protected String[] extractClientCertificateChain(HttpServletRequest request)
    {
        for (ClientCertificateExtractor extractor : clientCertificateExtractors)
        {
            String[] chain = extractor.extractClientCertificateChain(request);
            if (chain != null && chain.length > 0)
            {
                return chain;
            }
        }

        return null;
    }

    /**
     * Utility method for extracting a single client certificate from the default
     * certificate extractors. Calls extractClientCertificateChain and returns the
     * first entry in the array, if any, null otherwise.
     *
     * @param request
     *          The incoming HTTP request to search for the client's certificate.
     *
     * @return
     *          The client's mutual TLS certificate.
     *
     * @since 2.8
     */
    protected String extractClientCertificate(HttpServletRequest request)
    {
        String[] certs = extractClientCertificateChain(request);

        if (certs != null && certs.length > 0)
        {
            return certs[0];
        }
        else
        {
            return null;
        }
    }
}


/*
 * Copyright (C) 2016 Authlete, Inc.
 *
 * 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 com.authlete.jaxrs;

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import com.authlete.common.api.AuthleteApi;

/**
 * Handler for requests to an endpoint that exposes JSON Web Key Set
 * (RFC 7517) document.
 *
 *

 * An OpenID Provider (OP) is required to expose its JSON Web Key Set document
 * (JWK Set) so that client applications can (1) verify signatures by the OP
 * and (2) encrypt their requests to the OP. The URI of a JWK Set endpoint can
 * be found as the value of {@code jwks_uri} in OpenID Provider Metadata, if the OP supports OpenID
 * Connect Discovery 1.0.
 *

 *
 * @since 1.1
 *
 * @author Takahiko Kawasaki
 */
public class JwksRequestHandler extends BaseHandler
{
    /**
     * Constructor with an implementation of {@link AuthleteApi} interface.
     *
     * @param api
     *         Implementation of {@link AuthleteApi} interface.
     */
    public JwksRequestHandler(AuthleteApi api)
    {
        super(api);
    }

    /**
     * Handle a request to a JWK Set endpoint. This method is an alias
     * of {@link #handle(boolean) handle}{@code (true)}.
     *
     * @return
     *         A response that should be returned from the endpoint to
     *         the client application.
     *
     * @throws WebApplicationException
     *         An error occurred.
     */
    public Response handle() throws WebApplicationException
    {
        return handle(true);
    }

    /**
     * Handle a request to a JWK Set endpoint. This method internally calls
     * Authlete's {@code /api/service/jwks/get} API.
     *
     * @param pretty
     *         {@code true} to return the output JSON in pretty format.
     *
     * @return
     *         A response that should be returned from the endpoint to
     *         the client application.
     *
     * @throws WebApplicationException
     *         An error occurred.
     */
    public Response handle(boolean pretty) throws WebApplicationException
    {
        try
        {
            // Call Authlete's /api/service/jwks/get API. It returns the JWK Set
            // of the service. Of course, private keys are not included.
            return getApiCaller().serviceJwksGet(pretty, false);
        }
        catch (WebApplicationException e)
        {
            // The API call raised an exception.
            throw e;
        }
        catch (Throwable t)
        {
            // Unexpected error.
            throw unexpected("Unexpected error in JwksRequestHandler", t);
        }
    }
}


/*
 * Copyright (C) 2017 Authlete, Inc.
 *
 * 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 com.authlete.jaxrs.util;

import java.util.Map;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;

/**
 * JAX-RS Utilities.
 *
 * @since 2.6
 */
public class JaxRsUtils
{
    /**
     * Create a {@code Map<TKey, TValue>} instance from a
     * {@code Map<TKey, TValue[]} instance.
     *
     * @param data
     *         Input data.
     *
     * @return
     *         A new instance which contains all the entries
     *         in the input data. If the input data is {@code null}
     *         or empty, an empty {@code MultivaluedMap} instance
     *         is returned.
     */
    public static <TKey, TValue> MultivaluedMap<TKey, TValue>
            createMultivaluedMap(Map<TKey, TValue[]> data)
    {
        MultivaluedMap<TKey, TValue> target
            = new MultivaluedHashMap<TKey, TValue>();

        return putAll(target, data);
    }

    /**
     * Put all entries in a {@code Map<TKey, TValue[]>} instance
     * into an existing {@code MultivaluedMap<TKey, TValue>}.
     *
     * @param target
     *         Target {@code MultivaluedMap} instance.
     *
     * @param data
     *         Input data.
     *
     * @return
     *         The object given as {@code target}.
     */
    public static <TKey, TValue> MultivaluedMap<TKey, TValue>
            putAll(MultivaluedMap<TKey, TValue> target, Map<TKey, TValue[]> data)
    {
        if (target == null || data == null)
        {
            return target;
        }

        for (Map.Entry<TKey, TValue[]> entry : data.entrySet())
        {
            target.addAll(entry.getKey(), entry.getValue());
        }

        return target;
    }
}


/*
 * Copyright (C) 2019 Authlete, Inc.
 *
 * 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 com.authlete.jaxrs;

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import com.authlete.common.api.AuthleteApi;
import com.authlete.jaxrs.spi.BackchannelAuthenticationRequestHandlerSpi;

/**
 * A base class for backchannel authentication endpoints of CIBA (Client Initiated
 * Backchannel Authentication).
 *
 * @since 2.13
 *
 * @author Hideki Ikeda
 */
public class BaseBackchannelAuthenticationEndpoint extends BaseEndpoint
{
    /**
     * Handle a backchannel authentication request in CIBA (Client Initiated
     * Backchannel Authentication) flow.
     *
     *

     * This method internally creates a {@link BackchannelAuthenticationRequestHandler}
     * instance and calls its {@link BackchannelAuthenticationRequestHandler#handle(MultivaluedMap, String)}
     * method. Then, this method uses the value returned from the {@code handle()}
     * method as a response from this method.
     *

     *
     *

     * When {@code BackchannelAuthenticationRequestHandler.handle()} method raises a {@link
     * WebApplicationException}, this method calls {@link #onError(WebApplicationException)
     * onError()} method with the exception. The default implementation of {@code onError()}
     * does nothing. You can override the method as necessary. After calling
     * {@code onError()} method, this method calls {@code getResponse()} method of
     * the exception and uses the returned value as a response from this method.
     *

     *
     * @param api
     *         An implementation of {@link AuthleteApi}.
     *
     * @param spi
     *         An implementation of {@link BackchannelAuthenticationRequestHandlerSpi}.
     *
     * @param parameters
     *         Request parameters of the backchannel authentication request.
     *
     * @param authorization
     *         The value of {@code Authorization} header of the backchannel authentication
     *         request.
     *
     * @param clientCertificatePath
     *         The certificate path used in mutual TLS authentication, in PEM format. The
     *         client's own certificate is the first in this array. Can be {@code null}.
     *
     * @return
     *         A response that should be returned to the client application.
     *
     * @since 2.13
     */
    public Response handle(
            AuthleteApi api, BackchannelAuthenticationRequestHandlerSpi spi,
            MultivaluedMap<String, String> parameters, String authorization, String[] clientCertificatePath)
    {
        try
        {
            // Create a handler.
            BackchannelAuthenticationRequestHandler handler = new BackchannelAuthenticationRequestHandler(api, spi);

            // Delegate the task to the handler.
            return handler.handle(parameters, authorization, clientCertificatePath);
        }
        catch (WebApplicationException e)
        {
            // An error occurred in the handler.
            onError(e);

            // Convert the error to a Response.
            return e.getResponse();
        }
    }
}


/*******************************************************************************
 *  Copyright (c) 2017 ModelSolv, Inc. and others.
 *  All rights reserved. This program and the accompanying materials
 *  are made available under the terms of the Eclipse Public License v1.0
 *  which accompanies this distribution, and is available at
 *  http://www.eclipse.org/legal/epl-v10.html
 *
 *  Contributors:
 *     ModelSolv, Inc. - initial API and implementation and/or initial documentation
 *******************************************************************************/
package com.networknt.oas.validator.msg;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class MessagesHelper {
	private static final Pattern placeholderPattern = Pattern.compile("%(\\d+)|%\\{(\\d+)\\}");

	/**
	 * Interpolate arguments into the message format string for the given message.
	 *

	 * If a non-null locale is provided, an attempt is made to load a properties
	 * file for the Message class of which this message is an instance, and the
	 * property named by this message's name is used as the format string. If that
	 * fails, or if the locale is null, the message object's default format string
	 * is used.
	 *

	 * The format string contains embedded place holders indicating where arguments
	 * are to be substituted. Each place holder takes the form %n or
	 * %{n}, where n is an integer from 1 to the number of
	 * arguments. The second form allows interpolation at a site immediately
	 * followed by other digits.
	 *

	 * To prevent a percent sign from triggering interpolation, double it:
	 * %%. For example, "%%1 %%{2}" will always produce
	 * "%1 %{2}".
	 *

	 * The result is obtained by replacing each placeholder with the corresponding
	 * argument's {@link #toString()} value (except that null arguments are rendered
	 * as empty strings).
	 *
	 * @param message Messages
	 * @param args Args
	 * @param locale Locale
	 * @return message with interpolated arguments
	 */
	public static String format(Locale locale, Messages message, Object... args) {
		String formatString = message.getFormatString(locale);
		Object[] sortArgs = sortArgs(formatString, args);
		String printfString = Stream.of(formatString.split("%%"))
				.map(part -> placeholderPattern.matcher(part).replaceAll("%s")) //
				.collect(Collectors.joining("%%"));
		return String.format(printfString, sortArgs);
	}

	private static Object[] sortArgs(String format, Object[] args) {
		List<Integer> sites = findInterpolationSites(format);
		sites.stream().forEach(site -> {
			if (site < 1 || site > args.length) {
				String msg = String.format("Interpolation position must be from 1 to %d: %%%d", args.length, site);
				throw new IndexOutOfBoundsException(msg);
			}
		});
		return sites.stream().map(i -> args[i - 1] != null ? args[i - 1] : "").toArray();
	}

	private static List<Integer> findInterpolationSites(String format) {
		List<Integer> sites = new ArrayList<>();
		String[] parts = format.split("%%");
		for (String part : parts) {
			Matcher m = placeholderPattern.matcher(part);
			while (m.find()) {
				boolean braces = m.group(1) == null;
				String digits = m.group(braces ? 2 : 1);
				int index = digits.isEmpty() ? 0 : Integer.valueOf(digits);
				sites.add(index);
			}
		}
		return sites;
	}

	private static Map<Class<?>, Map<String, Properties>> localizationsByClass = new HashMap<>();

	public static Properties loadLocalizations(Class<? extends Messages> msgClass, Locale locale) {
		if (!localizationsByClass.containsKey(msgClass)) {
			localizationsByClass.put(msgClass, new HashMap<>());
		}
		Map<String, Properties> localizations = localizationsByClass.get(msgClass);
		// try locale-specified variant tag first, then fall back to just the language
		// code
		for (String tag : Arrays.asList(locale.toLanguageTag(), locale.getLanguage())) {
			if (!localizations.containsKey(tag)) {
				URL resource = msgClass.getResource(String.format("localizations/%s/messages.properties", tag));
				Properties props = null;
				if (resource != null) {
					props = new Properties();
					try (InputStream in = resource.openStream()) {
						props.load(in);
					} catch (IOException e) {
						props = null;
					}
				}
				// if no props loaded, set null value in localizations so we don't keep trying
				localizations.put(tag, props);
			}
			Properties result = localizations.get(tag);
			if (result != null) {
				return result;
			}
		}
		return null;
	}
}


/**
 * Copyright (C) 2011-2018 Red Hat, Inc. (https://github.com/Commonjava/indy)
 *
 * 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.commonjava.indy.core.content;

import org.commonjava.cdi.util.weft.ExecutorConfig;
import org.commonjava.cdi.util.weft.WeftExecutorService;
import org.commonjava.cdi.util.weft.WeftManaged;
import org.commonjava.indy.IndyWorkflowException;
import org.commonjava.indy.change.event.ArtifactStorePostRescanEvent;
import org.commonjava.indy.change.event.ArtifactStorePreRescanEvent;
import org.commonjava.indy.change.event.ArtifactStoreRescanEvent;
import org.commonjava.indy.change.event.IndyStoreErrorEvent;
import org.commonjava.indy.content.DownloadManager;
import org.commonjava.indy.content.StoreResource;
import org.commonjava.indy.core.change.event.IndyFileEventManager;
import org.commonjava.indy.data.IndyDataException;
import org.commonjava.indy.data.StoreDataManager;
import org.commonjava.indy.measure.annotation.Measure;
import org.commonjava.indy.measure.annotation.MetricNamed;
import org.commonjava.indy.model.core.ArtifactStore;
import org.commonjava.indy.model.core.HostedRepository;
import org.commonjava.indy.model.core.RemoteRepository;
import org.commonjava.indy.model.core.StoreKey;
import org.commonjava.indy.model.core.StoreType;
import org.commonjava.indy.model.galley.CacheOnlyLocation;
import org.commonjava.indy.model.galley.KeyedLocation;
import org.commonjava.indy.spi.pkg.ContentAdvisor;
import org.commonjava.indy.spi.pkg.ContentQuality;
import org.commonjava.indy.util.ApplicationStatus;
import org.commonjava.indy.util.LocationUtils;
import org.commonjava.indy.util.PathUtils;
import org.commonjava.maven.galley.BadGatewayException;
import org.commonjava.maven.galley.TransferContentException;
import org.commonjava.maven.galley.TransferException;
import org.commonjava.maven.galley.TransferLocationException;
import org.commonjava.maven.galley.TransferManager;
import org.commonjava.maven.galley.TransferTimeoutException;
import org.commonjava.maven.galley.event.EventMetadata;
import org.commonjava.maven.galley.event.FileAccessEvent;
import org.commonjava.maven.galley.model.ConcreteResource;
import org.commonjava.maven.galley.model.ListingResult;
import org.commonjava.maven.galley.model.Location;
import org.commonjava.maven.galley.model.Transfer;
import org.commonjava.maven.galley.model.TransferOperation;
import org.commonjava.maven.galley.model.VirtualResource;
import org.commonjava.maven.galley.spi.nfc.NotFoundCache;
import org.commonjava.maven.galley.spi.transport.LocationExpander;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.enterprise.event.Event;
import javax.enterprise.inject.Any;
import javax.enterprise.inject.Instance;
import javax.inject.Inject;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.StreamSupport;

import static org.commonjava.cdi.util.weft.ExecutorConfig.BooleanLiteral.TRUE;
import static org.commonjava.indy.IndyContentConstants.CHECK_CACHE_ONLY;
import static org.commonjava.indy.change.EventUtils.fireEvent;
import static org.commonjava.indy.core.ctl.PoolUtils.detectOverloadVoid;
import static org.commonjava.indy.data.StoreDataManager.IGNORE_READONLY;
import static org.commonjava.indy.measure.annotation.MetricNamed.DEFAULT;
import static org.commonjava.indy.model.core.StoreType.hosted;
import static org.commonjava.indy.util.ContentUtils.dedupeListing;
import static org.commonjava.maven.galley.model.TransferOperation.DOWNLOAD;
import static org.commonjava.maven.galley.model.TransferOperation.LISTING;

@javax.enterprise.context.ApplicationScoped
public class DefaultDownloadManager
        implements DownloadManager
{

    private final Logger logger = LoggerFactory.getLogger( getClass() );

    @Inject
    private Event<ArtifactStoreRescanEvent> rescanEvent;

    @Inject
    private IndyFileEventManager fileEventManager;

    // Byte, because it's small, and we really only care about the keys anyway.
    private final Map<StoreKey, Byte> rescansInProgress = new ConcurrentHashMap<>();

    @Inject
    @WeftManaged
    @ExecutorConfig( priority = 10, threads = 2, named = "rescan-manager", loadSensitive = TRUE, maxLoadFactor = 2 )
    private WeftExecutorService rescanService;

    @Inject
    private TransferManager transfers;

    @Inject
    private LocationExpander locationExpander;

    @Inject
    private StoreDataManager storeManager;

    @Inject
    private NotFoundCache nfc;

    @Inject
    @Any
    private Instance<ContentAdvisor> contentAdvisors;

    protected DefaultDownloadManager()
    {
    }

    public DefaultDownloadManager( final StoreDataManager storeManager, final TransferManager transfers,
                                   final LocationExpander locationExpander, WeftExecutorService rescanService )
    {
        this.storeManager = storeManager;
        this.transfers = transfers;
        this.locationExpander = locationExpander;
        this.fileEventManager = new IndyFileEventManager();
        this.rescanService = rescanService;
    }

    public DefaultDownloadManager( final StoreDataManager storeManager, final TransferManager transfers,
                                   final LocationExpander locationExpander, Instance<ContentAdvisor> contentAdvisors,
                                   WeftExecutorService rescanService )
    {
        this(storeManager, transfers, locationExpander, rescanService);
        this.contentAdvisors = contentAdvisors;
    }

    public DefaultDownloadManager( final StoreDataManager storeManager, final TransferManager transfers,
                                   final LocationExpander locationExpander, Instance<ContentAdvisor> contentAdvisors,
                                   final NotFoundCache nfc, WeftExecutorService rescanService )
    {
        this(storeManager, transfers, locationExpander, contentAdvisors, rescanService);
        this.nfc = nfc;
    }
    @Override
    public List<StoreResource> list( final ArtifactStore store, final String path )
            throws IndyWorkflowException
    {
        return list(store, path, new EventMetadata() );
    }

    @Override
    @Measure( timers = @MetricNamed( DEFAULT ) )
    public List<StoreResource> list( final ArtifactStore store, final String path, final EventMetadata eventMetadata )
            throws IndyWorkflowException
    {
        final List<StoreResource> result = new ArrayList<>();

        //        final String dir = PathUtils.dirname( path );

        if ( store.getKey().getType() == StoreType.group )
        {
            try
            {
                final List<ListingResult> results = transfers.listAll(
                        locationExpander.expand( new VirtualResource( LocationUtils.toLocations( store ), path ) ), eventMetadata );

                for ( final ListingResult lr : results )
                {
                    if ( lr != null && lr.getListing() != null )
                    {
                        for ( final String file : lr.getListing() )
                        {
                            result.add( new StoreResource( (KeyedLocation) lr.getLocation(), path, file ) );
                        }
                    }
                }
            }
            catch ( final BadGatewayException e )
            {
                fireIndyStoreErrorEvent( e );
                logger.warn( "Bad gateway: " + e.getMessage(), e );
            }
            catch ( final TransferTimeoutException e )
            {
                fireIndyStoreErrorEvent( e );
                logger.warn( "Timeout: " + e.getMessage(), e );
            }
            catch ( final TransferLocationException e )
            {
                fireIndyStoreErrorEvent( e );
                logger.warn( "Location Error: " + e.getMessage(), e );
            }
            catch ( final TransferException e )
            {
                logger.error( e.getMessage(), e );
                throw new IndyWorkflowException( "Failed to list ALL paths: {} from: {}. Reason: {}", e, path,
                                                 store.getKey(), e.getMessage() );
            }
        }
        else
        {
            if ( ! PathMaskChecker.checkListingMask( store, path ) )
            {
                return result; // if list not permitted for the path, return empty list
            }

            final KeyedLocation loc = LocationUtils.toLocation( store );
            final StoreResource res = new StoreResource( loc, path );
            if ( store instanceof RemoteRepository )
            {
                try
                {
                    final ListingResult lr = transfers.list( res, eventMetadata );
                    if ( lr != null && lr.getListing() != null )
                    {
                        for ( final String file : lr.getListing() )
                        {
                            result.add( new StoreResource( loc, path, file ) );
                        }
                    }
                }
                catch ( final BadGatewayException e )
                {
                    fireIndyStoreErrorEvent( e );
                    logger.warn( "Bad gateway: " + e.getMessage(), e );
                }
                catch ( final TransferTimeoutException e )
                {
                    fireIndyStoreErrorEvent( e );
                    logger.warn( "Timeout: " + e.getMessage(), e );
                }
                catch ( final TransferLocationException e )
                {
                    fireIndyStoreErrorEvent( e );
                    logger.warn( "Location Error: " + e.getMessage(), e );
                }
                catch ( final TransferException e )
                {
                    logger.error( e.getMessage(), e );
                    throw new IndyWorkflowException( "Failed to list path: {} from: {}. Reason: {}", e, path,
                                                     store.getKey(), e.getMessage() );
                }
            }
            else
            {
                try
                {
                    final ListingResult listing = transfers.list( res, eventMetadata );
                    if ( listing != null && listing.getListing() != null )
                    {
                        for ( final String child : listing.getListing() )
                        {
                            result.add( new StoreResource( loc, path, child ) );
                        }
                    }
                }
                catch ( final TransferLocationException e )
                {
                    fireIndyStoreErrorEvent( e );
                    logger.warn( "Timeout  / bad gateway: " + e.getMessage(), e );
                }
                catch ( final TransferException e )
                {
                    logger.error( e.getMessage(), e );
                    throw new IndyWorkflowException( "Failed to list path: {} from: {}. Reason: {}", e, path,
                                                     store.getKey(), e.getMessage() );
                }
            }
        }

        return dedupeListing( result );
    }

    @Override
    public List<StoreResource> list( final List<? extends ArtifactStore> stores, final String path )
            throws IndyWorkflowException
    {
        return list( stores, path, new EventMetadata() );
    }

    @Override
    @Measure( timers = @MetricNamed( DEFAULT ) )
    public List<StoreResource> list( final List<? extends ArtifactStore> stores, final String path, final EventMetadata eventMetadata )
            throws IndyWorkflowException
    {
        final String dir = PathUtils.dirname( path );

        final List<StoreResource> result = new ArrayList<>();
        try
        {
            final List<ListingResult> results = transfers.listAll(
                    locationExpander.expand( new VirtualResource( LocationUtils.toLocations( stores ), path ) ), eventMetadata );

            for ( final ListingResult lr : results )
            {
                if ( lr != null && lr.getListing() != null )
                {
                    for ( final String file : lr.getListing() )
                    {
                        result.add( new StoreResource( (KeyedLocation) lr.getLocation(), dir, file ) );
                    }
                }
            }
        }
        catch ( final BadGatewayException e )
        {
            fireIndyStoreErrorEvent( e );
            logger.warn( "Bad gateway: " + e.getMessage(), e );
        }
        catch ( final TransferTimeoutException e )
        {
            fireIndyStoreErrorEvent( e );
            logger.warn( "Timeout: " + e.getMessage(), e );
        }
        catch ( final TransferLocationException e )
        {
            fireIndyStoreErrorEvent( e );
            logger.warn( "Location Error: " + e.getMessage(), e );
        }
        catch ( final TransferException e )
        {
            logger.error( e.getMessage(), e );
            throw new IndyWorkflowException( "Failed to list ALL paths: {} from: {}. Reason: {}", e, path, stores,
                                             e.getMessage() );
        }

        return dedupeListing( result );
    }

    @Override
    public Transfer retrieveFirst( final List<? extends ArtifactStore> stores, final String path )
            throws IndyWorkflowException
    {
        return retrieveFirst( stores, path, new EventMetadata() );
    }

    @Override
    @Measure( timers = @MetricNamed( DEFAULT ) )
    public Transfer retrieveFirst( final List<? extends ArtifactStore> stores, final String path,
                                   final EventMetadata eventMetadata )
            throws IndyWorkflowException
    {
        try
        {
            return transfers.retrieveFirst(
                    locationExpander.expand( new VirtualResource( LocationUtils.toLocations( stores ), path ) ),
                    eventMetadata );
        }
        catch ( final BadGatewayException e )
        {
            fireIndyStoreErrorEvent( e );
            logger.warn( "Bad gateway: " + e.getMessage(), e );
        }
        catch ( final TransferTimeoutException e )
        {
            fireIndyStoreErrorEvent( e );
            logger.warn( "Timeout: " + e.getMessage(), e );
        }
        catch ( final TransferLocationException e )
        {
            fireIndyStoreErrorEvent( e );
            logger.warn( "Location Error: " + e.getMessage(), e );
        }
        catch ( final TransferException e )
        {
            logger.error( e.getMessage(), e );
            throw new IndyWorkflowException( "Failed to retrieve first path: {} from: {}. Reason: {}", e, path, stores,
                                             e.getMessage() );
        }

        return null;
    }

    /*
     * (non-Javadoc)
     * @see org.commonjava.indy.core.rest.util.FileManager#downloadAll(java.util.List, java.lang.String)
     */
    @Override
    public List<Transfer> retrieveAll( final List<? extends ArtifactStore> stores, final String path )
            throws IndyWorkflowException
    {
        return retrieveAll( stores, path, new EventMetadata() );
    }

    /*
     * (non-Javadoc)
     * @see org.commonjava.indy.core.rest.util.FileManager#downloadAll(java.util.List, java.lang.String)
     */
    @Override
    @Measure( timers = @MetricNamed( DEFAULT ) )
    public List<Transfer> retrieveAll( final List<? extends ArtifactStore> stores, final String path,
                                       final EventMetadata eventMetadata )
            throws IndyWorkflowException
    {
        try
        {
            List<Transfer> txfrs = transfers.retrieveAll(
                    locationExpander.expand( new VirtualResource( LocationUtils.toLocations( stores ), path ) ),
                    eventMetadata );

            return txfrs;
        }
        catch ( final TransferException e )
        {
            logger.error( e.getMessage(), e );
            throw new IndyWorkflowException( "Failed to retrieve ALL paths: {} from: {}. Reason: {}", e, path, stores,
                                             e.getMessage() );
        }
    }

    /*
     * (non-Javadoc)
     * @see org.commonjava.indy.core.rest.util.FileManager#download(org.commonjava.indy.core.model.ArtifactStore,
     * java.lang.String)
     */
    @Override
    public Transfer retrieve( final ArtifactStore store, final String path )
            throws IndyWorkflowException
    {
        return retrieve( store, path, new EventMetadata() );
    }

    /*
     * (non-Javadoc)
     * @see org.commonjava.indy.core.rest.util.FileManager#download(org.commonjava.indy.core.model.ArtifactStore,
     * java.lang.String)
     */
    @Override
    @Measure( timers = @MetricNamed( DEFAULT ) )
    public Transfer retrieve( final ArtifactStore store, final String path, final EventMetadata eventMetadata )
            throws IndyWorkflowException
    {
        return retrieve( store, path, false, eventMetadata );
    }

    private Transfer retrieve( final ArtifactStore store, final String path, final boolean suppressFailures,
                               final EventMetadata eventMetadata )
            throws IndyWorkflowException
    {
        if ( store.getKey().getType() == StoreType.group )
        {
            return null;
        }

        if ( !PathMaskChecker.checkMask( store, path))
        {
            return null;
        }

        final ConcreteResource res = new ConcreteResource( LocationUtils.toLocation( store ), path );

        if ( store.getType() != hosted && nfc.isMissing( res ) )
        {
            return null;
        }

        Transfer target;
        try
        {
            if ( store instanceof RemoteRepository )
            {
                target = transfers.retrieve( res, suppressFailures, eventMetadata );
            }
            else
            {
                target = transfers.getCacheReference( res );
                if ( target == null || !target.exists() )
                {
                    target = null;
                }
            }
        }
        catch ( final TransferLocationException e )
        {
            fileEventManager.fire( new IndyStoreErrorEvent( store.getKey(), e ) );
            logger.warn( "Timeout / bad gateway: " + res + ". Reason: " + e.getMessage(), e );
            target = null;
        }
        catch ( final TransferContentException e )
        {
            logger.warn( "Content-Length mismatch: " + res + ". Reason: " + e.getMessage()
                                 + "\nNOTE: This may be a network error; will retry download on next request.", e );
            target = null;
        }
        catch ( final TransferException e )
        {
            logger.error( e.getMessage(), e );
            throw new IndyWorkflowException( "Failed to retrieve path: {} from: {}. Reason: {}", e, path, store,
                                             e.getMessage() );
        }

        return target;
    }

    @Override
    @Measure( timers = @MetricNamed( DEFAULT ) )
    public boolean exists(final ArtifactStore store, String path)
            throws IndyWorkflowException
    {
        if ( !PathMaskChecker.checkMask( store, path ) )
        {
            return false;
        }

        final ConcreteResource res = new ConcreteResource( LocationUtils.toLocation( store ), path );
        if ( store instanceof RemoteRepository )
        {
            try {
                return transfers.exists( res );
            } catch (TransferException e) {
                logger.warn( "Existence check: " + e.getMessage(), e );
                return false;
            }
        }
        else
        {
            Transfer target = transfers.getCacheReference(res);
            if ( target != null )
            {
                return target.exists();
            }
        }
        return false;
    }

    /*
     * (non-Javadoc)
     * @see org.commonjava.indy.core.rest.util.FileManager#upload(org.commonjava.indy.core.model.DeployPoint,
     * java.lang.String, java.io.InputStream)
     */
    @Override
    public Transfer store( final ArtifactStore store, final String path, final InputStream stream,
                           final TransferOperation op )
            throws IndyWorkflowException
    {
        return store( store, path, stream, op, new EventMetadata() );
    }

    /*
     * (non-Javadoc)
     * @see org.commonjava.indy.core.rest.util.FileManager#upload(org.commonjava.indy.core.model.DeployPoint,
     * java.lang.String, java.io.InputStream)
     */
    @Override
    @Measure( timers = @MetricNamed( DEFAULT ) )
    public Transfer store( final ArtifactStore store, final String path, final InputStream stream,
                           final TransferOperation op, final EventMetadata eventMetadata )
            throws IndyWorkflowException
    {
        if ( store.getKey().getType() == StoreType.group )
        {
            //FIXME: Why is this null? Investigate.
            return null;
        }

        if ( store.getKey().getType() != hosted )
        {
            throw new IndyWorkflowException( ApplicationStatus.BAD_REQUEST.code(),
                                             "Cannot deploy to non-deploy point artifact store: {}.", store.getKey() );
        }

        if ( !isIgnoreReadonly( eventMetadata ) && storeManager.isReadonly( store ) )
        {
            throw new IndyWorkflowException( ApplicationStatus.METHOD_NOT_ALLOWED.code(),
                                             "The store {} is readonly. If you want to store any content to this store, please modify it to non-readonly",
                                             store.getKey() );
        }

        if ( store instanceof HostedRepository )
        {
            final HostedRepository deploy = (HostedRepository) store;

//            final ArtifactPathInfo pathInfo = ArtifactPathInfo.parse( path );
            final ContentQuality quality = getQuality( path );
            if ( quality != null && quality == ContentQuality.SNAPSHOT )
            {
                if ( !deploy.isAllowSnapshots() )
                {
                    logger.error( "Cannot store snapshot in non-snapshot deploy point: {}", deploy.getName() );
                    throw new IndyWorkflowException( ApplicationStatus.BAD_REQUEST.code(),
                                                     "Cannot store snapshot in non-snapshot deploy point: {}",
                                                     deploy.getName() );
                }
            }
            else if ( !deploy.isAllowReleases() )
            {
                logger.error( "Cannot store release in snapshot-only deploy point: {}", deploy.getName() );
                throw new IndyWorkflowException( ApplicationStatus.BAD_REQUEST.code(),
                                                 "Cannot store release in snapshot-only deploy point: {}",
                                                 deploy.getName() );
            }
        }

        //        final Transfer target = getStorageReference( deploy, path );

        // TODO: Need some protection for released files!
        // if ( target.exists() )
        // {
        // throw new WebApplicationException(
        // Response.status( Status.BAD_REQUEST ).entity( "Deployment path already exists." ).build() );
        // }

        try
        {
            KeyedLocation loc = LocationUtils.toLocation( store );
            if ( isIgnoreReadonly( eventMetadata ) && loc instanceof CacheOnlyLocation )
            {
                ( (CacheOnlyLocation) loc ).setReadonly( false );
            }
            final ConcreteResource resource = new ConcreteResource( loc, path );
            Transfer txfr = transfers.store( resource, stream, eventMetadata );
            nfc.clearMissing( resource );
            return txfr;
        }
        catch ( final BadGatewayException e )
        {
            fireIndyStoreErrorEvent( e );
            logger.warn( "Bad gateway: " + e.getMessage(), e );
            throw new IndyWorkflowException( "Failed to store path: {} in: {}. Reason: {}", e, path, store,
                                             e.getMessage() );
        }
        catch ( final TransferTimeoutException e )
        {
            fireIndyStoreErrorEvent( e );
            logger.warn( "Timeout: " + e.getMessage(), e );
            throw new IndyWorkflowException( "Failed to store path: {} in: {}. Reason: {}", e, path, store,
                                             e.getMessage() );
        }
        catch ( final TransferLocationException e )
        {
            fireIndyStoreErrorEvent( e );
            logger.warn( "Location Error: " + e.getMessage(), e );
            throw new IndyWorkflowException( "Failed to store path: {} in: {}. Reason: {}", e, path, store,
                                             e.getMessage() );
        }
        catch ( TransferException e )
        {
            logger.error(
                    String.format( "Failed to store: %s in: %s. Reason: %s", path, store.getKey(), e.getMessage() ),
                    e );

            throw new IndyWorkflowException( "Failed to store: %s in: %s. Reason: %s", e, path, store.getKey(),
                                             e.getMessage() );
        }
    }

    private boolean isIgnoreReadonly( EventMetadata eventMetadata )
    {
        return Boolean.TRUE.equals( eventMetadata.get( IGNORE_READONLY ) );
    }

    /*
     * (non-Javadoc)
     * @see org.commonjava.indy.core.rest.util.FileManager#upload(java.util.List, java.lang.String,
     * java.io.InputStream)
     */
    @Override
    public Transfer store( final List<? extends ArtifactStore> stores, final String path, final InputStream stream,
                           final TransferOperation op )
            throws IndyWorkflowException
    {
        return store( stores, path, stream, op, new EventMetadata() );
    }

    /*
     * (non-Javadoc)
     * @see org.commonjava.indy.core.rest.util.FileManager#upload(java.util.List, java.lang.String,
     * java.io.InputStream)
     */
    @Override
    public Transfer store( final List<? extends ArtifactStore> stores, final String path, final InputStream stream,
                           final TransferOperation op, final EventMetadata eventMetadata )
            throws IndyWorkflowException
    {
        final ContentQuality quality = getQuality( path );

        HostedRepository selected = null;
        for ( final ArtifactStore store : stores )
        {
            if ( !isIgnoreReadonly( eventMetadata ) && storeManager.isReadonly( store ) )
            {
                logger.debug( "The store {} is readonly, store operation not allowed" );
                continue;
            }
            if ( storeIsSuitableFor( store, quality, op ) )
            {
                selected = (HostedRepository) store;
                break;
            }
        }

        if ( selected == null )
        {
            logger.warn( "Cannot deploy. No valid deploy points in group." );
            throw new IndyWorkflowException( ApplicationStatus.BAD_REQUEST.code(),
                                             "No deployment locations available." );
        }

        logger.info( "Storing: {} in selected: {} with event metadata: {}", path, selected, eventMetadata );
        store( selected, path, stream, op, eventMetadata );

        return getStorageReference( selected.getKey(), path );
    }

    @Override
    public Transfer getStoreRootDirectory( final StoreKey key )
            throws IndyWorkflowException
    {
        ArtifactStore store;
        try
        {
            store = storeManager.getArtifactStore( key );
        }
        catch ( final IndyDataException e )
        {
            throw new IndyWorkflowException( "Failed to retrieve ArtifactStore for: %s. Reason: %s", e, key,
                                             e.getMessage() );
        }

        if ( store == null )
        {
            throw new IndyWorkflowException( ApplicationStatus.NOT_FOUND.code(), "Cannot find store: {}", key );
        }

        return transfers.getStoreRootDirectory( LocationUtils.toLocation( store ) );
    }

    @Override
    public Transfer getStoreRootDirectory( final ArtifactStore store )
    {
        return transfers.getStoreRootDirectory( LocationUtils.toLocation( store ) );
    }

    @Override
    public Transfer getStorageReference( final List<ArtifactStore> stores, final String path,
                                         final TransferOperation op )
            throws IndyWorkflowException
    {
//        final ArtifactPathInfo pathInfo = ArtifactPathInfo.parse( path );
        final ContentQuality quality = getQuality( path );

        Transfer transfer = null;

        logger.trace( "Checking {} stores to find one suitable for {} of: {}", stores.size(), op, path );
        boolean suitableFound = false;
        for ( final ArtifactStore store : stores )
        {
            if ( storeIsSuitableFor( store, quality, op ) )
            {
                suitableFound = true;

                logger.trace( "Attempting to retrieve storage reference in: {} for: {} (operation: {})", store, path,
                              op );

                // [jdcasey]: We don't want to use NFC for hosted repos any more...consumes memory and isn't much faster than filesystem
//                if ( store.getKey().getType() == hosted && ( op == DOWNLOAD || op == LISTING ) )
//                {
//                    transfer = getStorageReferenceWithNFC( store, path );
//                }
//                else
//                {
                    transfer = getStorageReference( store, path );
//                }
                logger.trace( "Checking {} (exists? {}; file: {})", transfer, transfer != null && transfer.exists(), transfer == null ? "NONE" : transfer.getFullPath() );
                if ( transfer != null && !transfer.exists() && ( op == DOWNLOAD || op == LISTING ) )
                {
                    transfer = null;
                }

                if ( transfer != null )
                {
                    logger.debug( "Using transfer: {}", transfer );
                    break;
                }
            }
        }

        if ( !stores.isEmpty() && !suitableFound )
        {
            logger.warn( "No suitable stores in list." );
            throw new IndyWorkflowException( ApplicationStatus.BAD_REQUEST.code(), "No suitable store available." );
        }

        return transfer;
    }

    private Transfer getStorageReferenceWithNFC( final ArtifactStore store, final String... path )
    {
        ConcreteResource resource = new ConcreteResource( LocationUtils.toLocation( store ), path );
        if ( store.getType() != hosted && nfc.isMissing( resource ) )
        {
            logger.trace( "Resource {} is missing, return null", resource );
            return null;
        }

        Transfer txfr = transfers.getCacheReference( resource );
        if ( txfr == null || !txfr.exists() )
        {
            logger.trace( "Resource not found when retrieving cached reference; added to NFC: {}", resource );
            nfc.addMissing( resource );
        }

        return txfr;
    }

    private boolean storeIsSuitableFor( final ArtifactStore store, final ContentQuality pathQuality,
                                        final TransferOperation op )
    {
        if ( TransferOperation.UPLOAD == op )
        {
            if ( store instanceof HostedRepository )
            {
                //                logger.info( "Found deploy point: %s", store.getName() );
                final HostedRepository dp = (HostedRepository) store;
                if ( pathQuality == null )
                {
                    // probably not an artifact, most likely metadata instead...
                    //                    logger.info( "Selecting it for non-artifact storage: {}", path );
                    return true;
                }
                else if (  ContentQuality.SNAPSHOT == pathQuality )
                {
                    if ( dp.isAllowSnapshots() )
                    {
                        //                        logger.info( "Selecting it for snapshot storage: {}", pathInfo );
                        return true;
                    }
                    else
                    {
                        logger.trace( "Hosted repo doesn't allow snapshot uploads: {}", store.getKey() );
                    }
                }
                else if ( dp.isAllowReleases() )
                {
                    //                    logger.info( "Selecting it for release storage: {}", pathInfo );
                    return true;
                }
                else
                {
                    logger.trace( "Hosted repo doesn't allow release uploads: {}", store.getKey() );
                }
            }
            else
            {
                logger.trace( "Store not suitable for upload: {}", store.getKey() );
            }

            // TODO: Allow push-through via remote repositories too.
        }
        else
        {
            return true;
        }

        return false;
    }

    @Override
    public Transfer getStorageReference( final ArtifactStore store, final String path, final TransferOperation op )
            throws IndyWorkflowException
    {
        final ContentQuality quality = getQuality( path );
        if ( storeIsSuitableFor( store, quality, op ) )
        {
            // [jdcasey]: We don't want to use NFC for hosted repos any more...consumes memory and isn't much faster than filesystem
//            if ( store.getKey().getType() == hosted && ( op == DOWNLOAD || op == LISTING ) )
//            {
//                return getStorageReferenceWithNFC( store, path );
//            }
//            else
//            {
                return getStorageReference( store, path );
//            }
        }

        logger.warn( "Store {} not suitable for: {}", store, op );
        throw new IndyWorkflowException( ApplicationStatus.BAD_REQUEST.code(),
                                         "Store is not suitable for this operation." );
    }

    @Override
    @Measure( timers = @MetricNamed( DEFAULT ) )
    public Transfer getStorageReference( final ArtifactStore store, final String... path )
    {
        Logger logger = LoggerFactory.getLogger( getClass() );
        logger.trace( "Retrieving cache reference (Transfer) to: {} in: {}", Arrays.asList( path ), store.getKey() );

        ConcreteResource resource = new ConcreteResource( LocationUtils.toLocation( store ), path );
        return transfers.getCacheReference( resource );
    }

    @Override
    @Measure( timers = @MetricNamed( DEFAULT ) )
    public Transfer getStorageReference( final StoreKey key, final String... path )
            throws IndyWorkflowException
    {
        ArtifactStore store;
        try
        {
            store = storeManager.getArtifactStore( key );
        }
        catch ( final IndyDataException e )
        {
            throw new IndyWorkflowException( "Failed to retrieve ArtifactStore for: %s. Reason: %s", e, key,
                                             e.getMessage() );
        }

        if ( store == null )
        {
            throw new IndyWorkflowException( ApplicationStatus.NOT_FOUND.code(), "Cannot find store: {}", key );
        }

        return getStorageReference( store, path );
    }

    @Override
    public boolean deleteAll( final List<? extends ArtifactStore> stores, final String path )
            throws IndyWorkflowException
    {
        boolean result = false;
        for ( final ArtifactStore store : stores )
        {
            if ( storeManager.isReadonly( store ) )
            {
                logger.warn( "The store {} is readonly, store operation not allowed" );
                continue;
            }

            result = delete( store, path, new EventMetadata() ) || result;
        }

        return result;
    }

    @Override
    public boolean delete( final ArtifactStore store, final String path )
            throws IndyWorkflowException
    {
        return delete( store, path, new EventMetadata() );
    }

    @Override
    public boolean delete( final ArtifactStore store, final String path, final EventMetadata eventMetadata )
            throws IndyWorkflowException
    {
        if ( Boolean.TRUE.equals( eventMetadata.get( CHECK_CACHE_ONLY ) ) )
        {
            return deleteCache( store, path, eventMetadata );
        }

        if ( store.getKey().getType() == StoreType.group )
        {
            return false;
        }

        if ( storeManager.isReadonly( store ) )
        {
            throw new IndyWorkflowException( ApplicationStatus.METHOD_NOT_ALLOWED.code(),
                                             "The store {} is readonly. If you want to store any content to this store, please modify it to non-readonly",
                                             store.getKey() );
        }

        final Transfer item = getStorageReference( store, path == null ? ROOT_PATH : path );

        final boolean deleted = doDelete( item, eventMetadata );

        return deleted;
    }

    /**
     * clean just the cache (storage of groups and remote repos)
     */
    private boolean deleteCache( ArtifactStore store, String path, EventMetadata eventMetadata )
                    throws IndyWorkflowException
    {
        if ( store.getKey().getType() == hosted )
        {
            return false;
        }
        final Transfer item = getStorageReference( store, path == null ? ROOT_PATH : path );
        logger.trace( "Delete cache, item: {}", item );
        return doDelete( item, eventMetadata );
    }

    private Boolean doDelete( final Transfer item, final EventMetadata eventMetadata )
            throws IndyWorkflowException
    {
        try
        {
            final ConcreteResource res = item.getResource();
            transfers.delete( res, eventMetadata );
        }
        catch ( final TransferException e )
        {
            throw new IndyWorkflowException( "Failed to delete: {}. Reason: {}", e, item, e.getMessage() );
        }

        return true;
    }

    @Override
    public void rescanAll( final List<? extends ArtifactStore> stores )
            throws IndyWorkflowException
    {
        rescanAll( stores, new EventMetadata() );
    }

    @Override
    public void rescanAll( final List<? extends ArtifactStore> stores, final EventMetadata eventMetadata )
            throws IndyWorkflowException
    {
        for ( final ArtifactStore store : stores )
        {
            rescan( store, eventMetadata );
        }
    }

    @Override
    public void rescan( final ArtifactStore store )
            throws IndyWorkflowException
    {
        rescan( store, new EventMetadata() );
    }

    @Override
    public void rescan( final ArtifactStore store, final EventMetadata eventMetadata )
            throws IndyWorkflowException
    {
        detectOverloadVoid( () -> rescanService.execute(
                new Rescanner( store, getStorageReference( store.getKey() ), rescansInProgress, fileEventManager,
                               rescanEvent, eventMetadata ) ) );
    }

    private static final class Rescanner
            implements Runnable
    {
        private final Logger logger = LoggerFactory.getLogger( getClass() );

        private static final Byte IN_PROGRESS_FLAG = (byte) 0x1;

        private final Map<StoreKey, Byte> rescansInProgress;

        private final Transfer start;

        private final Event<ArtifactStoreRescanEvent> rescanEvent;

        private final IndyFileEventManager fileEventManager;

        private final ArtifactStore store;

        private final EventMetadata eventMetadata;

        public Rescanner( final ArtifactStore store, final Transfer start, final Map<StoreKey, Byte> rescansInProgress,
                          final IndyFileEventManager fileEventManager,
                          final Event<ArtifactStoreRescanEvent> rescanEvent, final EventMetadata eventMetadata )
        {
            this.store = store;
            this.start = start;
            this.rescansInProgress = rescansInProgress;
            this.fileEventManager = fileEventManager;
            this.rescanEvent = rescanEvent;
            this.eventMetadata = eventMetadata;
        }

        @Override
        public void run()
        {
            final StoreKey storeKey = store.getKey();
            synchronized ( rescansInProgress )
            {
                if ( rescansInProgress.containsKey( storeKey ) )
                {
                    return;
                }

                rescansInProgress.put( storeKey, IN_PROGRESS_FLAG );
                store.setRescanInProgress( true );
            }

            try
            {
                fireEvent( rescanEvent, new ArtifactStorePreRescanEvent( eventMetadata, store ) );

                doRescan( start );

                fireEvent( rescanEvent, new ArtifactStorePostRescanEvent( eventMetadata, store ) );
            }
            finally
            {
                synchronized ( rescansInProgress )
                {
                    rescansInProgress.remove( storeKey );
                    store.setRescanInProgress( false );
                }
            }
        }

        private void doRescan( final Transfer item )
        {
            if ( !item.exists() )
            {
                return;
            }

            if ( item.isDirectory() )
            {
                try
                {
                    final String[] listing = item.list();
                    for ( final String sub : listing )
                    {
                        doRescan( item.getChild( sub ) );
                    }
                }
                catch ( final IOException e )
                {
                    logger.error( "Failed to list local contents: {}. Reason: {}", e, item, e.getMessage() );
                }
            }

            fileEventManager.fire( new FileAccessEvent( item, eventMetadata ) );
        }

    }

    @Override
    @Measure( timers = @MetricNamed( DEFAULT ) )
    public List<Transfer> listRecursively( final StoreKey src, final String startPath )
            throws IndyWorkflowException
    {
        final List<Transfer> result = new ArrayList<>();
        final Transfer transfer = getStorageReference( src, startPath );
        recurseListing( transfer, result );

        return result;
    }

    private void recurseListing( final Transfer transfer, final List<Transfer> result )
            throws IndyWorkflowException
    {
        if ( transfer.isDirectory() )
        {
            try
            {
                final String[] children = transfer.list();
                for ( final String child : children )
                {
                    final Transfer childTransfer = transfer.getChild( child );
                    recurseListing( childTransfer, result );
                }
            }
            catch ( final IOException e )
            {
                throw new IndyWorkflowException( "Failed to list children of: %s. Reason: %s", e, transfer,
                                                 e.getMessage() );
            }
        }
        else if ( transfer.exists() )
        {
            result.add( transfer );
        }
    }

    private ContentQuality getQuality( String path )
    {
        if ( contentAdvisors != null )
        {
            final ContentAdvisor advisor = StreamSupport.stream(
                    Spliterators.spliteratorUnknownSize( contentAdvisors.iterator(), Spliterator.ORDERED ), false )
                                                        .filter( Objects::nonNull )
                                                        .findFirst()
                                                        .orElse( null );
            return advisor == null ? null : advisor.getContentQuality( path );
        }

        return null;
    }

    private void fireIndyStoreErrorEvent( TransferLocationException e )
    {
        Location location = e.getLocation();
        KeyedLocation kl = (KeyedLocation) location;

        fileEventManager.fire( new IndyStoreErrorEvent( kl.getKey(), e ) );
    }

}


/**
 * Copyright (C) 2011-2018 Red Hat, Inc. (https://github.com/Commonjava/indy)
 *
 * 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.commonjava.indy.content.index;

import org.commonjava.indy.data.IndyDataException;
import org.commonjava.indy.data.StoreDataManager;
import org.commonjava.indy.model.core.ArtifactStore;
import org.commonjava.indy.model.core.StoreKey;
import org.commonjava.indy.model.core.StoreType;
import org.commonjava.indy.util.LocationUtils;
import org.commonjava.maven.galley.model.ConcreteResource;
import org.commonjava.maven.galley.spi.nfc.NotFoundCache;
import org.infinispan.notifications.Listener;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryCreated;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryRemoved;
import org.infinispan.notifications.cachelistener.event.CacheEntryCreatedEvent;
import org.infinispan.notifications.cachelistener.event.CacheEntryRemovedEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;

/**
 * This listener is used to bind the content index cache event to nfc content clearing/adding. The following cases:
 *

 *
When new content index entry added, it means that a new resource is available in repo(s),
 *     so all nfc content for this resource of the cascaded repos(low level concrete repo to higher group which contains it)
 *     should be cleared

 *
When content index entry removed, it means that the resource is missing in repo(s), so this resource with specified
 *     repo should be added into nfc. Note that only group level is processed here because remote and hosted are handled in
 *     other functions.

 *

 *
 * So here we used ISPN event listener to handle these types of logic, by CacheEntryCreatedEvent and CacheEntryRemovedEvent
 */
@ApplicationScoped
@Listener
class NFCContentListener
{
    private final Logger logger = LoggerFactory.getLogger( this.getClass() );

    @Inject
    private StoreDataManager storeDataManager;

    @Inject
    private NotFoundCache nfc;

    @CacheEntryCreated
    public void newIndex( final CacheEntryCreatedEvent<IndexedStorePath, IndexedStorePath> e )
    {
        if ( !e.isPre() )
        {
            IndexedStorePath isp = e.getValue();
            final StoreKey key =
                    new StoreKey( isp.getPackageType(), isp.getStoreType(), isp.getStoreName() );
            logger.debug( "New artifact created in store {} of path {}, will start to clear nfc cache for it.", key,
                          isp.getPath() );
            try
            {
                final ArtifactStore store = storeDataManager.getArtifactStore( key );
                nfc.clearMissing( new ConcreteResource( LocationUtils.toLocation( store ), isp.getPath() ) );
                nfcClearByContaining( store, isp.getPath() );
            }
            catch ( IndyDataException ex )
            {
                logger.error( String.format(
                        "When clear nfc missing for indexed artifact of path %s in store %s, failed to lookup store. Reason: %s",
                        isp.getPath(), key, ex.getMessage() ), ex );
            }
        }
    }

    // Not sure if this entry modified event should be watched, need some further check
    //    @CacheEntryModified
    //    public void updateIndex( final CacheEntryModifiedEvent<IndexedStorePath, IndexedStorePath> e){
    //
    //    }

    @CacheEntryRemoved
    public void removeIndex( final CacheEntryRemovedEvent<IndexedStorePath, IndexedStorePath> e )
    {
        if ( e.isPre() )
        {
            IndexedStorePath isp = e.getValue();
            final StoreKey key =
                    new StoreKey( isp.getPackageType(), isp.getStoreType(), isp.getStoreName() );
            // Only care about group level, as remote repo nfc is handled by remote timeout handler(see galley DownloadHandler),
            // and hosted is handled by DownloadManager
            if ( key.getType() == StoreType.group )
            {
                try
                {
                    final ArtifactStore store = storeDataManager.getArtifactStore( key );
                    final ConcreteResource r = new ConcreteResource( LocationUtils.toLocation( store ), isp.getPath() );
                    logger.debug( "Add NFC of resource {} in store {}", r, store );
                    if ( StoreType.hosted != key.getType() )
                    {
                        nfc.addMissing( r );
                    }
                }
                catch ( IndyDataException ex )
                {
                    logger.error( String.format(
                            "When add nfc missing for indexed artifact of path %s in store %s, failed to lookup store. Reason: %s",
                            isp.getPath(), key, ex.getMessage() ), ex );
                }
            }
        }
    }

    private void nfcClearByContaining( final ArtifactStore store, final String path )
    {
        try
        {
            if ( store == null )
            {
                return;
            }
            logger.debug( "Start to clear nfc for groups affected by {} of path {}", store, path );
            storeDataManager.query()
                            .packageType( store.getKey().getPackageType() )
                            .getGroupsAffectedBy( store.getKey() )
                            .forEach( g -> {
                                final ConcreteResource r = new ConcreteResource( LocationUtils.toLocation( g ), path );
                                logger.debug( "Clear NFC in terms of containing {} in {} for resource {}", store, g,
                                              r );
                                nfc.clearMissing( r );
                            } );

        }
        catch ( IndyDataException e )
        {
            logger.error( String.format( "Failed to lookup parent stores which contain %s. Reason: %s", store.getKey(),
                                         e.getMessage() ), e );
        }
    }
}


/**
 * Copyright (C) 2011-2018 Red Hat, Inc. (https://github.com/Commonjava/indy)
 *
 * 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.commonjava.indy.core.expire;

import org.commonjava.indy.IndyWorkflowException;
import org.commonjava.indy.change.event.ArtifactStorePostUpdateEvent;
import org.commonjava.indy.change.event.ArtifactStoreUpdateType;
import org.commonjava.indy.content.ContentManager;
import org.commonjava.indy.content.StoreContentAction;
import org.commonjava.indy.data.IndyDataException;
import org.commonjava.indy.data.StoreDataManager;
import org.commonjava.indy.model.core.ArtifactStore;
import org.commonjava.indy.model.core.Group;
import org.commonjava.indy.model.core.HostedRepository;
import org.commonjava.indy.model.core.RemoteRepository;
import org.commonjava.indy.model.core.StoreKey;
import org.commonjava.indy.model.core.StoreType;
import org.commonjava.indy.model.core.io.IndyObjectMapper;
import org.commonjava.maven.galley.event.FileAccessEvent;
import org.commonjava.maven.galley.event.FileDeletionEvent;
import org.commonjava.maven.galley.event.FileStorageEvent;
import org.commonjava.maven.galley.model.SpecialPathInfo;
import org.commonjava.maven.galley.model.Transfer;
import org.commonjava.maven.galley.model.TransferOperation;
import org.commonjava.maven.galley.spi.io.SpecialPathManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Observes;
import javax.inject.Inject;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import java.util.Set;

import static org.commonjava.indy.util.LocationUtils.getKey;

@ApplicationScoped
public class TimeoutEventListener
        implements StoreContentAction
{
    private final Logger logger = LoggerFactory.getLogger( getClass() );

    @Inject
    private ScheduleManager scheduleManager;

    @Inject
    private ContentManager contentManager;

    @Inject
    private StoreDataManager storeManager;

    @Inject
    private IndyObjectMapper objectMapper;

    @Inject
    private SpecialPathManager specialPathManager;

    //    @Inject
    //    @ExecutorConfig( daemon = true, priority = 7, named = "indy-events" )
    //    private Executor executor;

    public void onExpirationEvent( @Observes final SchedulerEvent event )
    {
        if ( !(event instanceof SchedulerTriggerEvent) || !event.getJobType()
                                                                         .equals( ScheduleManager.CONTENT_JOB_TYPE ) )
        {
            return;
        }

        ContentExpiration expiration;
        try
        {
            expiration = objectMapper.readValue( event.getPayload(), ContentExpiration.class );
        }
        catch ( final IOException e )
        {
            logger.error( "Failed to read ContentExpiration from event payload.", e );
            return;
        }

        final StoreKey key = expiration.getKey();
        final String path = expiration.getPath();
        try
        {
            ArtifactStore store = storeManager.getArtifactStore( key );
            if ( store == null )
            {
                logger.warn( "Store: {} is no longer available. Nothing to do for path: {}", key, path );
            }

            boolean deleted = store == null || contentManager.delete( store, path );

            if ( !deleted )
            {
                logger.error("Failed to delete Transfer for: {} in: {} (for content timeout).", path, key );
            }
            else
            {
                deleteExpiration( key, path );
            }
        }
        catch ( IndyWorkflowException e )
        {
            logger.error(
                    String.format( "Failed to retrieve Transfer for: %s in: %s (for content timeout). Reason: %s", path,
                                   key, e ), e );
        }
        catch ( IndyDataException e )
        {
            scheduleManager.deleteJob( scheduleManager.groupName( key, ScheduleManager.CONTENT_JOB_TYPE ), path );
            logger.error(
                    String.format( "Failed to retrieve ArtifactStore for: %s (for content timeout). Reason: %s", key, e ), e );
        }
    }

    public void onFileStorageEvent( @Observes final FileStorageEvent event )
    {
        final StoreKey key = getKey( event );
        if ( key == null )
        {
            return;
        }

        final Transfer transfer = event.getTransfer();
        final TransferOperation type = event.getType();

        switch ( type )
        {
            case UPLOAD:
            {
                try
                {
                    scheduleManager.setSnapshotTimeouts( key, transfer.getPath() );
                }
                catch ( final IndySchedulerException e )
                {
                    logger.error( "Failed to clean up metadata / set snapshot timeouts related to: " + transfer, e );
                }

                break;
            }
            case DOWNLOAD:
            {
                try
                {
                    scheduleManager.setProxyTimeouts( key, transfer.getPath() );
                }
                catch ( final IndySchedulerException e )
                {
                    logger.error( "Failed to clean up metadata / set proxy-cache timeouts related to: " + transfer, e );
                }

                break;
            }
            default:
            {
                break;
            }
        }
    }

    public void onFileAccessEvent( @Observes final FileAccessEvent event )
    {
        // TODO: handle this stuff in Weft somehow...
        Map original = MDC.getCopyOfContextMap();
        try
        {
            MDC.setContextMap( event.getMDCMap() );

            final StoreKey key = getKey( event );
            if ( key != null )
            {
                final Transfer transfer = event.getTransfer();
                final StoreType type = key.getType();

                if ( type == StoreType.hosted )
                {
                    try
                    {
                        scheduleManager.setSnapshotTimeouts( key, transfer.getPath() );
                    }
                    catch ( final IndySchedulerException e )
                    {
                        logger.error( "Failed to set snapshot timeouts related to: " + transfer, e );
                    }
                }
                else if ( type == StoreType.remote )
                {
                    SpecialPathInfo info = specialPathManager.getSpecialPathInfo( transfer );
                    if ( info == null || !info.isMetadata() )
                    {
                        logger.debug( "Accessed resource {} timeout will be reset.", transfer );
                        try
                        {
                            scheduleManager.setProxyTimeouts( key, transfer.getPath() );
                        }
                        catch ( final IndySchedulerException e )
                        {
                            logger.error( "Failed to set proxy-cache timeouts related to: " + transfer, e );
                        }
                    }
                    else
                    {
                        logger.debug( "Accessed resource {} is metadata. NOT rescheduling timeout!", transfer );
                    }
                }
            }
        }
        finally
        {
            if ( original != null && !original.isEmpty() )
            {
                MDC.setContextMap( original );
            }
            else
            {
                MDC.setContextMap( Collections.emptyMap() );
            }
        }
    }

    public void onFileDeletionEvent( @Observes final FileDeletionEvent event )
    {
        final StoreKey key = getKey( event );
        if ( key != null )
        {
            deleteExpiration( key, event.getTransfer().getPath() );
        }
    }

    public void onStoreUpdate( @Observes final ArtifactStorePostUpdateEvent event )
    {
        final ArtifactStoreUpdateType eventType = event.getType();
        if ( eventType == ArtifactStoreUpdateType.UPDATE )
        {
            for ( final ArtifactStore store : event )
            {
                final StoreKey key = store.getKey();
                final StoreType type = key.getType();
                if ( type == StoreType.hosted )
                {
                    logger.info( "[ADJUST TIMEOUTS] Adjusting snapshot expirations in: {}", store.getKey() );
                    try
                    {
                        scheduleManager.rescheduleSnapshotTimeouts( (HostedRepository) store );
                    }
                    catch ( final IndySchedulerException e )
                    {
                        logger.error( "Failed to update snapshot timeouts in: " + store.getKey(), e );
                    }
                }
                else if ( type == StoreType.remote )
                {
                    logger.info( "[ADJUST TIMEOUTS] Adjusting proxied-file expirations in: {}", store.getKey() );
                    try
                    {
                        scheduleManager.rescheduleProxyTimeouts( (RemoteRepository) store );
                    }
                    catch ( final IndySchedulerException e )
                    {
                        logger.error( "Failed to update proxy-cache timeouts in: " + store.getKey(), e );
                    }
                }
            }
        }
    }

    @Override
    public void clearStoreContent( String path, ArtifactStore store, Set<Group> affectedGroups,
                                   boolean clearOriginPath )
    {
        if ( clearOriginPath )
        {
            deleteExpiration( store.getKey(), path );
        }

        if ( affectedGroups != null && !affectedGroups.isEmpty() )
        {
            affectedGroups.forEach( g -> deleteExpiration( g.getKey(), path ) );
        }
    }

    private void deleteExpiration( StoreKey key, String path )
    {
        if ( key != null )
        {
            scheduleManager.deleteJob( ScheduleManager.groupName( key, ScheduleManager.CONTENT_JOB_TYPE ), path );
        }
        else
        {
            logger.warn( "Store is no longer available. No need to do deletion for path: {}", path );
        }
    }

}