package org.keycloak.protocol.oidc.utils;

import org.keycloak.protocol.oidc.mappers.AbstractPairwiseSubMapper;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.services.ServicesLogger;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

public class PairwiseSubMapperUtils {
    private static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER;

    /**
     * Returns a set of valid redirect URIs from the root url and redirect URIs registered on a client.
     *
     * @param clientRootUrl
     * @param clientRedirectUris
     * @return
     */
    public static Set<String> resolveValidRedirectUris(String clientRootUrl, Set<String> clientRedirectUris) {
        Set<String> validRedirects = new HashSet<String>();
        for (String redirectUri : clientRedirectUris) {
            if (redirectUri.startsWith("/")) {
                redirectUri = relativeToAbsoluteURI(clientRootUrl, redirectUri);
                logger.debugv("replacing relative valid redirect with: {0}", redirectUri);
            }
            if (redirectUri != null) {
                validRedirects.add(redirectUri);
            }
        }
        return validRedirects.stream()
                .filter(r -> r != null && !r.trim().isEmpty())
                .collect(Collectors.toSet());
    }

    /**
     * Tries to resolve a valid sector identifier from a sector identifier URI.
     *
     * @param sectorIdentifierUri
     * @return a sector identifier iff. the sector identifier URI is a valid URI, contains a valid scheme and contains a valid host component.
     */
    public static String resolveValidSectorIdentifier(String sectorIdentifierUri) {
        URI uri;
        try {
            uri = new URI(sectorIdentifierUri);
        } catch (URISyntaxException e) {
            logger.debug("Invalid sector identifier URI", e);
            return null;
        }

        if (uri.getScheme() == null) {
            logger.debugv("Invalid sector identifier URI: {0}", sectorIdentifierUri);
            return null;
        }

        /*if (!uri.getScheme().equalsIgnoreCase("https")) {
            logger.debugv("The sector identifier URI scheme must be HTTPS. Was '{0}'", uri.getScheme());
            return null;
        }*/

        if (uri.getHost() == null) {
            logger.debug("The sector identifier URI must specify a host");
            return null;
        }

        return uri.getHost();
    }

    /**
     * Tries to resolve a valid sector identifier from the redirect URIs registered on a client.
     *
     * @param clientRootUrl      Root url registered on the client.
     * @param clientRedirectUris Redirect URIs registered on the client.
     * @return a sector identifier iff. all the registered redirect URIs are located at the same host, otherwise {@code null}.
     */
    public static String resolveValidSectorIdentifier(String clientRootUrl, Set<String> clientRedirectUris) {
        Set<String> hosts = new HashSet<>();
        for (String redirectUri : resolveValidRedirectUris(clientRootUrl, clientRedirectUris)) {
            try {
                URI uri = new URI(redirectUri);
                hosts.add(uri.getHost());
            } catch (URISyntaxException e) {
                logger.debugv("client redirect uris contained an invalid uri: {0}", redirectUri);
            }
        }
        if (hosts.isEmpty()) {
            logger.debug("could not infer any valid sector_identifiers from client redirect uris");
            return null;
        }
        if (hosts.size() > 1) {
            logger.debug("the client redirect uris contained multiple hosts");
            return null;
        }
        return hosts.iterator().next();
    }

    /**
     * Checks if the the registered client redirect URIs matches the set of redirect URIs from the sector identifier URI.
     *
     * @param clientRootUrl      root url registered on the client.
     * @param clientRedirectUris redirect URIs registered on the client.
     * @param sectorRedirects    value of the sector identifier URI.
     * @return {@code true} iff. the all the redirect URIs can be described by the {@code sectorRedirects}, i.e if the registered redirect URIs is a subset of the {@code sectorRedirects}, otherwise {@code false}.
     */
    public static boolean matchesRedirects(String clientRootUrl, Set<String> clientRedirectUris, Set<String> sectorRedirects) {
        Set<String> validRedirects = resolveValidRedirectUris(clientRootUrl, clientRedirectUris);
        for (String redirect : validRedirects) {
            if (!matchesRedirect(sectorRedirects, redirect)) return false;
        }
        return true;
    }

    private static boolean matchesRedirect(Set<String> validRedirects, String redirect) {
        for (String validRedirect : validRedirects) {
            if (validRedirect.endsWith("*") && !validRedirect.contains("?")) {
                // strip off the query component - we don't check them when wildcards are effective
                String r = redirect.contains("?") ? redirect.substring(0, redirect.indexOf("?")) : redirect;
                // strip off *
                int length = validRedirect.length() - 1;
                validRedirect = validRedirect.substring(0, length);
                if (r.startsWith(validRedirect)) return true;
                // strip off trailing '/'
                if (length - 1 > 0 && validRedirect.charAt(length - 1) == '/') length--;
                validRedirect = validRedirect.substring(0, length);
                if (validRedirect.equals(r)) return true;
            } else if (validRedirect.equals(redirect)) return true;
        }
        return false;
    }

    private static String relativeToAbsoluteURI(String rootUrl, String relative) {
        if (rootUrl == null || rootUrl.isEmpty()) {
            return null;
        }
        relative = rootUrl + relative;
        return relative;
    }

    public static List<ProtocolMapperRepresentation> getPairwiseSubMappers(ClientRepresentation client) {
        List<ProtocolMapperRepresentation> pairwiseMappers = new LinkedList<>();
        List<ProtocolMapperRepresentation> mappers = client.getProtocolMappers();

        if (mappers != null) {
            client.getProtocolMappers().stream().filter((ProtocolMapperRepresentation mapping) -> {
                return mapping.getProtocolMapper().endsWith(AbstractPairwiseSubMapper.PROVIDER_ID_SUFFIX);
            }).forEach((ProtocolMapperRepresentation mapping) -> {
                pairwiseMappers.add(mapping);
            });
        }

        return pairwiseMappers;
    }
}
