package cz.senslog.connector.fetch.azure.auth;

import com.google.gson.reflect.TypeToken;
import cz.senslog.connector.model.config.HostConfig;
import cz.senslog.connector.tools.exception.SyntaxException;
import cz.senslog.connector.tools.http.HttpClient;
import cz.senslog.connector.tools.http.HttpRequest;
import cz.senslog.connector.tools.http.HttpResponse;
import cz.senslog.connector.tools.http.URLBuilder;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.lang.reflect.Type;
import java.time.format.DateTimeParseException;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import static cz.senslog.connector.tools.http.HttpCode.UNAUTHORIZED;
import static cz.senslog.connector.tools.json.BasicJson.jsonToObject;
import static cz.senslog.connector.tools.util.StringUtils.isNotBlank;
import static java.time.format.DateTimeFormatter.ISO_DATE_TIME;

/**
 * The class {@code AzureAuthenticationService} represents a service which wraps
 * all functionality around authentication. The main method is {@see AzureAuthenticationService#getAccessToken}
 * and provides a valid access token.
 *
 * @author Lukas Cerny
 * @version 1.0
 * @since 1.0
 */
public class AuthenticationService {

    private static final Logger logger = LogManager.getLogger(AuthenticationService.class);


    /** Default period value if token can not be refreshed  */
    private static final Integer DEFAULT_REFRESH_PERIOD = 5000;


    /** Configuration */
    private final AzureAuthConfig authConfig;

    /** Http client */
    private final HttpClient httpClient;


    /** Received authorization information */
    private AzureAuthorizationInfo authorizationInfo;

    /**
     * Static factory method to create a new instance of service.
     * @param authConfig - authentication configuration.
     * @param httpClient - http client.
     * @return new instance of {@code AzureAuthenticationService}.
     */
    public static AuthenticationService newAuthService(AzureAuthConfig authConfig, HttpClient httpClient) {
        return new AuthenticationService(authConfig, httpClient);
    }

    /**
     * Constructor of the class sets all attributes.
     * @param authConfig - authentication configuration.
     * @param httpClient - http client.
     */
    private AuthenticationService(AzureAuthConfig authConfig, HttpClient httpClient) {
        this.authConfig = authConfig;
        this.httpClient = httpClient;
    }

    /**
     * Creates a http request to get actual authorization information.
     * Request contains credentials (email and password) in header
     * and are gotten list of access information in json format.
     * Each record contains token and expire time. The first
     * non expired token si chosen to use.
     * @return true or false if token is get successfully and valid.
     */
    private boolean refreshToken() {

        HostConfig host = authConfig.getHost();
        logger.info("Creating a http request to the Azure service at {}.", host);
        HttpRequest request = HttpRequest.newBuilder()
                .url(URLBuilder.newBuilder(host.getDomain(), host.getPath()).build())
                .header("Email", authConfig.getUsername())
                .header("Password", authConfig.getPassword())
                .POST().build();

        logger.info("Sending the http request.");
        HttpResponse response = httpClient.send(request);
        logger.info("Received a response with a status: {}.", response.getStatus());

        if (response.isError()) {
            if (response.getStatus() == UNAUTHORIZED) {
                logger.debug("Parsing response body to a Map.");
                Map<?,?> jsonMap = jsonToObject(response.getBody(), Map.class);
                String message = (String) jsonMap.get("Message");
                logger.error("Unauthorized {}: {}.", response.getStatus(), message);
            } else {
                logger.error("Error with the code {}: {}.", response.getStatus(), response.getBody());
            }
            return false;
        }

        try {
            logger.debug("Parsing the response body.");
            Type azureInfoListType = new TypeToken<Collection<AzureAuthorizationInfo>>(){}.getType();
            List<AzureAuthorizationInfo> azureInfos = jsonToObject(response.getBody(), azureInfoListType);

            if (azureInfos.isEmpty()) {
                logger.error("Response does not contain authorization information.");
                return false;
            }

            logger.info("Response body was parsed successfully and getting authorization information.");
            for (AzureAuthorizationInfo azureInfo : azureInfos) {
                if (azureInfo.isAuthorized()) {
                    authorizationInfo = azureInfo;
                    return true;
                }
            }

            logger.warn("Received authorization information contains only expired tokens.");
            return false;
        } catch (SyntaxException | DateTimeParseException e) {
            logger.catching(e);
            return false;
        }
    }

    /**
     * Main public method of this service which provides a valid access token.
     * Content of the method contains a loop where periodically is get access token.
     * If token is expired or does not exist, it is refreshed. If refresh process fails,
     * the service goes to sleep and tries it later.
     * @return string valid access token.
     */
    public String getAccessToken() {
        logger.info("Getting the actual authorization token to Azure.");

        while (true) {
            if (authorizationInfo != null && authorizationInfo.isAuthorized()) {

                String token = authorizationInfo.getAccessToken();

                if (isNotBlank(token)) {
                    logger.debug("The access token is valid and will expired at {}.",
                            authorizationInfo.getExpires().format(ISO_DATE_TIME));
                    return token;
                } else {
                    logger.error("The token '{}' can not be used as an access token.", token);
                }
            }

            if (!refreshToken()) {
                try {
                    int sleepPeriod = authConfig.getRefreshPeriodIfFail().orElse(DEFAULT_REFRESH_PERIOD);
                    logger.warn("Can not get valid access token at this moment. Thread is going to sleep for {} ms.", sleepPeriod);
                    Thread.sleep(sleepPeriod);
                } catch (InterruptedException e) {
                    logger.catching(e);
                }
            }
        }
    }
}
