/*
 * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com).
 *
 * WSO2 LLC. licenses this file to you 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.wso2.carbon.identity.api.server.consent.management.v2.core;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.consent.mgt.core.ConsentManager;
import org.wso2.carbon.consent.mgt.core.exception.ConsentManagementException;
import org.wso2.carbon.consent.mgt.core.model.AddReceiptResponse;
import org.wso2.carbon.consent.mgt.core.model.ConsentAuthorization;
import org.wso2.carbon.consent.mgt.core.model.ConsentPurpose;
import org.wso2.carbon.consent.mgt.core.model.ConsentRelation;
import org.wso2.carbon.consent.mgt.core.model.PIICategory;
import org.wso2.carbon.consent.mgt.core.model.PIICategoryValidity;
import org.wso2.carbon.consent.mgt.core.model.Purpose;
import org.wso2.carbon.consent.mgt.core.model.PurposePIICategoryBinding;
import org.wso2.carbon.consent.mgt.core.model.PurposeVersion;
import org.wso2.carbon.consent.mgt.core.model.Receipt;
import org.wso2.carbon.consent.mgt.core.model.ReceiptInput;
import org.wso2.carbon.consent.mgt.core.model.ReceiptService;
import org.wso2.carbon.consent.mgt.core.model.ReceiptUpdateInput;
import org.wso2.carbon.consent.mgt.core.util.ConsentReceiptUtils;
import org.wso2.carbon.consent.mgt.core.util.FilterQueriesUtil;
import org.wso2.carbon.context.PrivilegedCarbonContext;
import org.wso2.carbon.identity.api.server.consent.management.common.ConsentManagementConstants;
import org.wso2.carbon.identity.api.server.consent.management.v2.model.AuthorizationDTO;
import org.wso2.carbon.identity.api.server.consent.management.v2.model.AuthorizationEntry;
import org.wso2.carbon.identity.api.server.consent.management.v2.model.AuthorizationUpdateEntry;
import org.wso2.carbon.identity.api.server.consent.management.v2.model.ConsentCreateRequest;
import org.wso2.carbon.identity.api.server.consent.management.v2.model.ConsentDTO;
import org.wso2.carbon.identity.api.server.consent.management.v2.model.ConsentListResponse;
import org.wso2.carbon.identity.api.server.consent.management.v2.model.ConsentPurposeBinding;
import org.wso2.carbon.identity.api.server.consent.management.v2.model.ConsentResponseDTO;
import org.wso2.carbon.identity.api.server.consent.management.v2.model.ConsentSummaryDTO;
import org.wso2.carbon.identity.api.server.consent.management.v2.model.ConsentUpdateRequest;
import org.wso2.carbon.identity.api.server.consent.management.v2.model.ConsentValidateResponse;
import org.wso2.carbon.identity.api.server.consent.management.v2.model.ConsentedElementDTO;
import org.wso2.carbon.identity.api.server.consent.management.v2.model.ConsentedPurposeDTO;
import org.wso2.carbon.identity.api.server.consent.management.v2.model.ConsentedPurposeSummaryDTO;
import org.wso2.carbon.identity.api.server.consent.management.v2.model.ElementTerminationInfo;
import org.wso2.carbon.identity.api.server.consent.management.v2.model.PaginationLink;
import org.wso2.carbon.identity.api.server.consent.management.v2.util.ConsentMgtEndpointUtil;
import org.wso2.carbon.identity.core.model.ExpressionNode;
import org.wso2.carbon.identity.core.util.IdentityUtil;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import static org.wso2.carbon.consent.mgt.core.constant.ConsentConstants.DEFAULT_LIMIT;
import static org.wso2.carbon.consent.mgt.core.constant.ConsentConstants.ErrorMessages.ERROR_CODE_CONSENT_REJECTED_WITH_AUTHORIZATIONS;
import static org.wso2.carbon.consent.mgt.core.constant.ConsentConstants.ErrorMessages.ERROR_CODE_ELEMENT_UUID_NOT_FOUND;
import static org.wso2.carbon.consent.mgt.core.constant.ConsentConstants.ErrorMessages.ERROR_CODE_INVALID_FILTER_EXPRESSION;
import static org.wso2.carbon.consent.mgt.core.constant.ConsentConstants.ErrorMessages.ERROR_CODE_INVALID_QUERY_PARAM;
import static org.wso2.carbon.consent.mgt.core.constant.ConsentConstants.FilterConstants;
import static org.wso2.carbon.consent.mgt.core.constant.ConsentConstants.REVOKE_STATE;
import static org.wso2.carbon.consent.mgt.core.util.ConsentUtils.handleClientException;

/**
 * Service class for consent (receipt) operations in the V2 Consent Management API.
 */
public class ConsentManagementService {

    private static final Log LOG = LogFactory.getLog(ConsentManagementService.class);

    private static final String ATTRIBUTE_PURPOSES = "purposes";
    private static final String ATTRIBUTE_AUTHORIZATIONS = "authorizations";
    private static final String ATTRIBUTE_PROPERTIES = "properties";

    private static final Set<String> SUPPORTED_CONSENT_ATTRIBUTES = new HashSet<>(Arrays.asList(
            ATTRIBUTE_PURPOSES, ATTRIBUTE_AUTHORIZATIONS, ATTRIBUTE_PROPERTIES));

    private final ConsentManager consentManager;

    public ConsentManagementService(ConsentManager consentManager) {

        this.consentManager = consentManager;
    }

    /**
     * Creates a new consent receipt.
     *
     * @param request Consent create request.
     * @return ConsentResponseDTO with receipt details.
     * @throws ConsentManagementException if creation fails.
     */
    public ConsentResponseDTO createConsent(ConsentCreateRequest request) {

        try {
            return createConsentInternal(request);
        } catch (ConsentManagementException e) {
            throw ConsentMgtEndpointUtil.handleConsentManagementException(e);
        }
    }

    private ConsentResponseDTO createConsentInternal(ConsentCreateRequest request) throws ConsentManagementException {

        String subjectId = request.getSubjectId();
        boolean hasAuthorizations = request.getAuthorizations() != null && !request.getAuthorizations().isEmpty();
        boolean rejected = ConsentCreateRequest.StateEnum.REJECTED.equals(request.getState());
        if (rejected && hasAuthorizations) {
            throw handleClientException(ERROR_CODE_CONSENT_REJECTED_WITH_AUTHORIZATIONS, null);
        }

        ReceiptInput receiptInput = buildReceiptInput(request, subjectId);
        AddReceiptResponse addReceiptResponse = consentManager.addConsent(receiptInput);

        ConsentResponseDTO responseDTO = new ConsentResponseDTO();
        responseDTO.setId(addReceiptResponse.getConsentReceiptId());
        responseDTO.setLanguage(addReceiptResponse.getLanguage());
        responseDTO.setSubjectId(addReceiptResponse.getPiiPrincipalId());
        responseDTO.setTenantDomain(addReceiptResponse.getTenantDomain());
        return responseDTO;
    }

    /**
     * Retrieves a consent receipt by ID.
     *
     * @param receiptId Receipt ID.
     * @return ConsentDTO with receipt details.
     * @throws ConsentManagementException if retrieval fails.
     */
    public ConsentDTO getConsent(String receiptId) {

        try {
            Receipt receipt = consentManager.getReceiptWithExtendedSchema(receiptId);
            return toConsentDTO(receipt);
        } catch (ConsentManagementException e) {
            throw ConsentMgtEndpointUtil.handleConsentManagementException(e);
        }
    }

    /**
     * Lists consent receipts with explicit filter params and pagination.
     *
     * @param subjectId        Filter by subject user ID.
     * @param serviceId        Filter by service ID.
     * @param state            Filter by consent state.
     * @param purposeId        Filter by purpose ID.
     * @param purposeVersionId Filter by specific purpose version ID.
     * @param limit            Maximum results.
     * @param after            Cursor for pagination (results after this cursor).
     * @param before           Cursor for pagination (results before this cursor).
     * @return Response with list of ConsentSummaryDTOs.
     * @deprecated Use
     * {@link #listConsents(String, String, String, String, String, String, String, String, Integer, String, String)}
     * instead.
     */
    @Deprecated
    public ConsentListResponse listConsents(String subjectId, String serviceId, String state, String purposeId,
                                            String purposeVersionId, String filter, Integer limit,
                                            String after, String before) {

        String relation = StringUtils.isNotBlank(subjectId) ? ConsentRelation.SUBJECT.name() : null;
        return listConsents(subjectId, relation, serviceId, state, purposeId, purposeVersionId, filter, null, limit,
                after, before);
    }

    /**
     * Lists consent receipts with explicit filter params and pagination.
     *
     * @param userId           Filter by user ID, matched according to {@code relation}.
     * @param relation         Relation of {@code userId} to the retrieved consents.
     * @param serviceId        Filter by service ID.
     * @param state            Filter by consent state.
     * @param purposeId        Filter by purpose ID.
     * @param purposeVersionId Filter by specific purpose version ID.
     * @param attributes       Comma-separated optional attributes to include in each result.
     * @param limit            Maximum results.
     * @param after            Cursor for pagination (results after this cursor).
     * @param before           Cursor for pagination (results before this cursor).
     * @return Response with list of ConsentSummaryDTOs.
     */
    public ConsentListResponse listConsents(String userId, String relation, String serviceId, String state,
                                            String purposeId, String purposeVersionId, String filter,
                                            String attributes, Integer limit, String after, String before) {

        try {
            return listConsentsInternal(userId, relation, serviceId, state, purposeId, purposeVersionId, filter,
                    attributes, limit, after, before);
        } catch (ConsentManagementException e) {
            throw ConsentMgtEndpointUtil.handleConsentManagementException(e);
        }
    }

    private ConsentListResponse listConsentsInternal(String userId, String relation, String serviceId, String state,
                                                     String purposeId, String purposeVersionId, String filter,
                                                     String attributes, Integer limit, String after, String before)
            throws ConsentManagementException {

        limit = validatedLimit(limit);

        if (StringUtils.isNotBlank(before) && StringUtils.isNotBlank(after)) {
            throw handleClientException(ERROR_CODE_INVALID_QUERY_PARAM,
                    "Both 'before' and 'after' parameters are provided.");
        }

        if (StringUtils.isNotBlank(userId) != StringUtils.isNotBlank(relation)) {
            throw handleClientException(ERROR_CODE_INVALID_QUERY_PARAM,
                    "'userId' and 'relation' must be provided together.");
        }
        ConsentRelation consentRelation = StringUtils.isNotBlank(relation) ? resolveRelation(relation) : null;
        Set<String> requestedAttributes = resolveAttributes(attributes);

        List<ExpressionNode> expressionNodes = FilterQueriesUtil.getExpressionNodes(filter, after, before);
        for (ExpressionNode node : expressionNodes) {
            String attr = node.getAttributeValue();
            if (FilterConstants.FILTER_ATTR_AFTER.equals(attr) || FilterConstants.FILTER_ATTR_BEFORE.equals(attr)) {
                continue;
            }
            String op = node.getOperation() != null ? node.getOperation().toLowerCase() : "";
            if (FilterConstants.FILTER_ATTR_TIMESTAMP.equals(attr)) {
                if (!("ge".equals(op) || "le".equals(op))) {
                    throw handleClientException(ERROR_CODE_INVALID_FILTER_EXPRESSION,
                            "Only 'ge' and 'le' operations are supported for consent timestamp filter.");
                }
                continue;
            }
            if (attr == null || !attr.startsWith("properties.") || attr.length() <= "properties.".length()) {
                throw handleClientException(ERROR_CODE_INVALID_FILTER_EXPRESSION,
                        "Only 'properties.<key>' and 'timestamp' attributes are supported in consent filter. Got: "
                                + attr);
            }
            if (!("eq".equals(op) || "sw".equals(op) || "co".equals(op) || "ew".equals(op))) {
                throw handleClientException(ERROR_CODE_INVALID_FILTER_EXPRESSION,
                        "Only 'eq', 'sw', 'co', and 'ew' operations are supported for consent property filter.");
            }
        }

        List<Receipt> receipts = consentManager.listReceipts(
                userId, consentRelation, serviceId, state, purposeId, purposeVersionId, expressionNodes, limit + 1);

        ConsentListResponse result = new ConsentListResponse();
        List<PaginationLink> links = new ArrayList<>();

        if (receipts != null && !receipts.isEmpty()) {
            boolean hasMoreItems = receipts.size() > limit;
            boolean needsReverse = StringUtils.isNotBlank(before);
            boolean isFirstPage = (StringUtils.isBlank(before) && StringUtils.isBlank(after)) ||
                    (StringUtils.isNotBlank(before) && !hasMoreItems);
            boolean isLastPage = !hasMoreItems && (StringUtils.isNotBlank(after) || StringUtils.isBlank(before));

            String url = "?limit=" + limit;
            if (StringUtils.isNotBlank(userId)) {
                url += "&userId=" + URLEncoder.encode(userId, StandardCharsets.UTF_8);
                url += "&relation=" + consentRelation.name();
            }
            if (StringUtils.isNotBlank(serviceId)) {
                url += "&serviceId=" + URLEncoder.encode(serviceId, StandardCharsets.UTF_8);
            }
            if (StringUtils.isNotBlank(state)) {
                url += "&state=" + URLEncoder.encode(state, StandardCharsets.UTF_8);
            }
            if (purposeId != null) {
                url += "&purposeId=" + purposeId;
            }
            if (purposeVersionId != null) {
                url += "&purposeVersionId=" + purposeVersionId;
            }
            if (StringUtils.isNotBlank(filter)) {
                url += "&filter=" + URLEncoder.encode(filter, StandardCharsets.UTF_8);
            }
            if (!requestedAttributes.isEmpty()) {
                url += "&attributes=" + String.join(",", requestedAttributes);
            }

            if (hasMoreItems) {
                receipts = new ArrayList<>(receipts.subList(0, limit));
            }
            if (needsReverse) {
                Collections.reverse(receipts);
            }
            if (!isFirstPage) {
                String encodedString = Base64.getEncoder().encodeToString(
                        receipts.get(0).getCursor().getBytes(StandardCharsets.UTF_8));
                links.add(buildPaginationLink(url + "&" + FilterConstants.FILTER_ATTR_BEFORE + "=" + encodedString,
                        ConsentManagementConstants.LINK_REL_PREVIOUS));
            }
            if (!isLastPage) {
                String encodedString = Base64.getEncoder().encodeToString(
                        receipts.get(receipts.size() - 1).getCursor().getBytes(StandardCharsets.UTF_8));
                links.add(buildPaginationLink(url + "&" + FilterConstants.FILTER_ATTR_AFTER + "=" + encodedString,
                        ConsentManagementConstants.LINK_REL_NEXT));
            }
        }

        List<ConsentSummaryDTO> summaries = new ArrayList<>();
        if (receipts != null) {
            for (Receipt receipt : receipts) {
                summaries.add(toConsentSummaryDTO(receipt));
            }
        }
        populateRequestedAttributes(summaries, requestedAttributes);

        result.setTotalResults(summaries.size());
        result.setLinks(links);
        result.setConsents(summaries);
        return result;
    }

    /**
     * Revokes a consent receipt.
     *
     * @param receiptId Receipt ID.
     * @throws ConsentManagementException if revocation fails.
     */
    public void revokeConsent(String receiptId) {

        try {
            Receipt receipt = consentManager.getReceiptWithExtendedSchema(receiptId);
            consentManager.authorizeConsent(receiptId, receipt.getPiiPrincipalId(), REVOKE_STATE);
        } catch (ConsentManagementException e) {
            throw ConsentMgtEndpointUtil.handleConsentManagementException(e);
        }
    }

    /**
     * Admin update of an existing consent's expiryTime, properties, and/or authorizations.
     *
     * @param consentId Consent receipt ID.
     * @param request   Update request (all fields optional).
     * @return Updated ConsentDTO.
     */
    public ConsentDTO updateConsent(String consentId, ConsentUpdateRequest request) {

        try {
            return updateConsentInternal(consentId, request);
        } catch (ConsentManagementException e) {
            throw ConsentMgtEndpointUtil.handleConsentManagementException(e);
        }
    }

    private ConsentDTO updateConsentInternal(String consentId, ConsentUpdateRequest request)
            throws ConsentManagementException {

        ReceiptUpdateInput updateInput = new ReceiptUpdateInput();
        updateInput.setConsentReceiptId(consentId);

        if (request.getExpiryTime() != null) {
            updateInput.setExpiryTime(new Timestamp(request.getExpiryTime()));
        }

        updateInput.setProperties(request.getProperties());

        if (request.getAuthorizations() != null) {
            List<ConsentAuthorization> authorizations = new ArrayList<>();
            for (AuthorizationUpdateEntry entry : request.getAuthorizations()) {
                ConsentAuthorization auth = new ConsentAuthorization();
                auth.setUserId(entry.getUserId());
                auth.setType(entry.getType());
                if (entry.getState() != null) {
                    auth.setStatus(ConsentAuthorization.AuthorizationStatus.valueOf(entry.getState().toString()));
                }
                authorizations.add(auth);
            }
            updateInput.setAuthorizations(authorizations);
        }

        consentManager.updateConsent(updateInput);
        return toConsentDTO(consentManager.getReceiptWithExtendedSchema(consentId));
    }

    private ReceiptInput buildReceiptInput(ConsentCreateRequest request, String subjectId)
            throws ConsentManagementException {

        List<PurposePIICategoryBinding> purposeBindings = new ArrayList<>();
        if (request.getPurposes() != null) {
            for (ConsentPurposeBinding purposeBinding : request.getPurposes()) {
                List<PIICategory> piiCategories = new ArrayList<>();
                if (purposeBinding.getElements() != null) {
                    for (ElementTerminationInfo elementInfo : purposeBinding.getElements()) {
                        String elementId = elementInfo.getId().toString();
                        PIICategory piiCategory = consentManager.getPIICategoryByUuid(elementId);
                        if (piiCategory == null) {
                            throw handleClientException(ERROR_CODE_ELEMENT_UUID_NOT_FOUND, elementId);
                        }
                        piiCategories.add(piiCategory);
                    }
                }
                purposeBindings.add(new PurposePIICategoryBinding(
                        purposeBinding.getId().toString(), piiCategories));
            }
        }

        boolean rejected = ConsentCreateRequest.StateEnum.REJECTED.equals(request.getState());
        String tenantDomain = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantDomain();
        Long expiryMillis = request.getExpiryTime();
        Timestamp expiryTime = expiryMillis != null ? new Timestamp(expiryMillis) : null;
        List<ConsentAuthorization> authorizations = null;
        if (request.getAuthorizations() != null) {
            authorizations = new ArrayList<>();
            for (AuthorizationEntry entry : request.getAuthorizations()) {
                ConsentAuthorization auth = new ConsentAuthorization();
                auth.setUserId(entry.getUserId());
                auth.setType(entry.getType());
                authorizations.add(auth);
            }
        }
        return ConsentReceiptUtils.buildReceiptInput(request.getLanguage(), subjectId,
                tenantDomain, expiryTime, rejected, authorizations, request.getProperties(),
                request.getServiceId(), purposeBindings, consentManager);
    }

    private ConsentDTO toConsentDTO(Receipt receipt) throws ConsentManagementException {

        ConsentDTO dto = new ConsentDTO();
        dto.setId(receipt.getConsentReceiptId());
        dto.setTimestamp(receipt.getConsentTimestamp());
        dto.setLanguage(receipt.getLanguage());
        dto.setSubjectId(receipt.getPiiPrincipalId());
        dto.setState(ConsentDTO.StateEnum.fromValue(receipt.getState()));
        dto.setExpiryTime(receipt.getExpiryTime() != null ? receipt.getExpiryTime().getTime() : null);

        // Extract service name and purposes from the first service entry (V2 is single-service).
        List<ConsentedPurposeDTO> purposeDTOs = new ArrayList<>();
        List<ReceiptService> services = receipt.getServices();
        if (services != null && !services.isEmpty()) {
            ReceiptService receiptService = services.get(0);
            dto.setServiceId(receiptService.getService());

            if (receiptService.getPurposes() != null) {
                for (ConsentPurpose consentPurpose : receiptService.getPurposes()) {
                    purposeDTOs.add(toConsentedPurposeDTO(consentPurpose));
                }
            }
        }
        dto.setPurposes(purposeDTOs);

        dto.setProperties(receipt.getProperties());
        dto.setAuthorizations(
                toAuthorizationDTOs(consentManager.getConsentAuthorizations(receipt.getConsentReceiptId())));
        return dto;
    }

    private List<AuthorizationDTO> toAuthorizationDTOs(List<ConsentAuthorization> auths) {

        List<AuthorizationDTO> authDTOs = new ArrayList<>();
        if (auths != null) {
            for (ConsentAuthorization auth : auths) {
                try {
                    AuthorizationDTO.StateEnum stateEnum =
                            AuthorizationDTO.StateEnum.fromValue(auth.getStatus().name());
                    AuthorizationDTO authDTO = new AuthorizationDTO();
                    authDTO.setUserId(auth.getUserId());
                    authDTO.setState(stateEnum);
                    authDTO.setUpdatedTime(auth.getUpdatedTime());
                    authDTOs.add(authDTO);
                } catch (IllegalArgumentException e) {
                    // Skip unrecognized authorization states
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("Skipping unrecognized authorization state: " + auth.getStatus(), e);
                    }
                }
            }
        }
        return authDTOs;
    }

    private ConsentedPurposeDTO toConsentedPurposeDTO(ConsentPurpose consentPurpose) {

        ConsentedPurposeDTO dto = new ConsentedPurposeDTO();
        dto.setName(consentPurpose.getPurpose());

        String versionUuid = consentPurpose.getPurposeVersionId();
        try {
            Purpose purpose = consentManager.getPurposeByUuid(consentPurpose.getUuid());
            if (purpose == null) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Could not resolve purpose UUID for purposeId: " + consentPurpose.getUuid());
                }
            } else {
                if (StringUtils.isNotBlank(purpose.getUuid())) {
                    dto.setId(purpose.getUuid());
                }
                if (StringUtils.isNotBlank(purpose.getGroupType())) {
                    dto.setType(purpose.getGroupType());
                }
                if (StringUtils.isNotBlank(versionUuid)) {
                    dto.setVersionId(versionUuid);
                    PurposeVersion latestVersion = purpose.getLatestVersion();
                    if (latestVersion != null && versionUuid.equals(latestVersion.getUuid())) {
                        dto.setVersion(latestVersion.getVersion());
                        dto.setProperties(latestVersion.getProperties());
                    } else if (dto.getId() != null) {
                        PurposeVersion pv = consentManager.getPurposeVersion(dto.getId(), versionUuid);
                        if (pv != null) {
                            dto.setVersion(pv.getVersion());
                            dto.setProperties(pv.getProperties());
                        }
                    }
                }
            }
        } catch (ConsentManagementException e) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Could not resolve purpose UUID: " + consentPurpose.getUuid(), e);
            }
        }

        List<ConsentedElementDTO> elementDTOs = new ArrayList<>();
        if (consentPurpose.getPiiCategory() != null) {
            for (PIICategoryValidity piiCategoryValidity : consentPurpose.getPiiCategory()) {
                ConsentedElementDTO elementDTO = new ConsentedElementDTO();
                elementDTO.setName(piiCategoryValidity.getName());
                elementDTO.setDisplayName(piiCategoryValidity.getDisplayName());
                try {
                    PIICategory piiCategory = consentManager.getPIICategoryByUuid(piiCategoryValidity.getUuid());
                    if (piiCategory == null) {
                        if (LOG.isDebugEnabled()) {
                            LOG.debug("Could not resolve element UUID for elementId: " + piiCategoryValidity.getUuid());
                        }
                    } else {
                        if (StringUtils.isNotBlank(piiCategory.getUuid())) {
                            elementDTO.setId(piiCategory.getUuid());
                        }
                    }
                } catch (ConsentManagementException e) {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("Could not resolve element UUID: " + piiCategoryValidity.getUuid(), e);
                    }
                }
                elementDTOs.add(elementDTO);
            }
        }
        dto.setElements(elementDTOs);
        return dto;
    }

    /**
     * Validates the current status of a consent receipt.
     *
     * @param consentId Consent receipt ID.
     * @return Response with ConsentValidateResponse.
     * @throws ConsentManagementException if validation fails.
     */
    public ConsentValidateResponse validateConsent(String consentId) {

        try {
            Receipt receipt = consentManager.getReceiptWithExtendedSchema(consentId);
            String status = consentManager.validateConsentStatus(consentId);

            ConsentValidateResponse validateResponse = new ConsentValidateResponse();
            validateResponse.setState(ConsentValidateResponse.StateEnum.fromValue(status));
            validateResponse.setExpiryTime(
                    receipt.getExpiryTime() != null ? receipt.getExpiryTime().getTime() : null);
            return validateResponse;
        } catch (ConsentManagementException e) {
            throw ConsentMgtEndpointUtil.handleConsentManagementException(e);
        }
    }

    private ConsentSummaryDTO toConsentSummaryDTO(Receipt receipt) {

        ConsentSummaryDTO dto = new ConsentSummaryDTO();
        dto.setId(receipt.getConsentReceiptId());
        dto.setSubjectId(receipt.getPiiPrincipalId());
        dto.setState(ConsentSummaryDTO.StateEnum.fromValue(receipt.getState()));
        dto.setTimestamp(receipt.getConsentTimestamp());
        dto.setExpiryTime(receipt.getExpiryTime() != null ? receipt.getExpiryTime().getTime() : null);
        if (receipt.getServices() != null && !receipt.getServices().isEmpty()) {
            dto.setServiceId(receipt.getServices().get(0).getService());
        }
        return dto;
    }

    private void populateRequestedAttributes(List<ConsentSummaryDTO> summaries, Set<String> attributes)
            throws ConsentManagementException {

        if (attributes.isEmpty() || summaries.isEmpty()) {
            return;
        }
        List<String> receiptIds = new ArrayList<>();
        for (ConsentSummaryDTO summary : summaries) {
            receiptIds.add(summary.getId());
        }
        Map<String, List<ConsentPurpose>> purposes = attributes.contains(ATTRIBUTE_PURPOSES)
                ? consentManager.listConsentPurposes(receiptIds) : Collections.emptyMap();
        Map<String, List<ConsentAuthorization>> authorizations = attributes.contains(ATTRIBUTE_AUTHORIZATIONS)
                ? consentManager.listConsentAuthorizations(receiptIds) : Collections.emptyMap();
        Map<String, Map<String, String>> properties = attributes.contains(ATTRIBUTE_PROPERTIES)
                ? consentManager.listReceiptProperties(receiptIds) : Collections.emptyMap();

        for (ConsentSummaryDTO summary : summaries) {
            if (attributes.contains(ATTRIBUTE_PURPOSES)) {
                summary.setPurposes(toConsentedPurposeSummaryDTOs(
                        purposes.getOrDefault(summary.getId(), Collections.emptyList())));
            }
            if (attributes.contains(ATTRIBUTE_AUTHORIZATIONS)) {
                summary.setAuthorizations(toAuthorizationDTOs(
                        authorizations.getOrDefault(summary.getId(), Collections.emptyList())));
            }
            if (attributes.contains(ATTRIBUTE_PROPERTIES)) {
                summary.setProperties(properties.getOrDefault(summary.getId(), new HashMap<>()));
            }
        }
    }

    private List<ConsentedPurposeSummaryDTO> toConsentedPurposeSummaryDTOs(List<ConsentPurpose> consentPurposes) {

        List<ConsentedPurposeSummaryDTO> purposeDTOs = new ArrayList<>();
        for (ConsentPurpose consentPurpose : consentPurposes) {
            ConsentedPurposeSummaryDTO purposeDTO = new ConsentedPurposeSummaryDTO();
            purposeDTO.setName(consentPurpose.getPurpose());
            purposeDTO.setId(consentPurpose.getUuid());
            purposeDTO.setType(consentPurpose.getGroupType());
            purposeDTO.setVersionId(consentPurpose.getPurposeVersionId());
            purposeDTO.setVersion(consentPurpose.getVersion());
            purposeDTOs.add(purposeDTO);
        }
        return purposeDTOs;
    }

    private Set<String> resolveAttributes(String attributes) throws ConsentManagementException {

        if (StringUtils.isBlank(attributes)) {
            return Collections.emptySet();
        }
        Set<String> resolved = new LinkedHashSet<>();
        for (String attribute : attributes.split(",")) {
            String resolvedAttribute = attribute.trim().toLowerCase(Locale.ROOT);
            if (StringUtils.isBlank(resolvedAttribute)) {
                continue;
            }
            if (!SUPPORTED_CONSENT_ATTRIBUTES.contains(resolvedAttribute)) {
                throw handleClientException(ERROR_CODE_INVALID_QUERY_PARAM, "attributes: " + attribute.trim());
            }
            resolved.add(resolvedAttribute);
        }
        return resolved;
    }

    private ConsentRelation resolveRelation(String relation) throws ConsentManagementException {

        try {
            return ConsentRelation.valueOf(relation.toUpperCase(Locale.ROOT));
        } catch (IllegalArgumentException e) {
            throw handleClientException(ERROR_CODE_INVALID_QUERY_PARAM, "relation: " + relation);
        }
    }

    private Integer validatedLimit(Integer limit) throws ConsentManagementException {

        limit = limit == null ? DEFAULT_LIMIT : limit;
        if (limit <= 0) {
            throw handleClientException(ERROR_CODE_INVALID_QUERY_PARAM, limit.toString());
        }
        int maximumItemPerPage = IdentityUtil.getMaximumItemPerPage();
        if (limit > maximumItemPerPage) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Given limit exceeds the maximum limit. Therefore the configured maximum limit: "
                        + maximumItemPerPage + " is set as the limit.");
            }
            return maximumItemPerPage;
        }
        return limit;
    }

    private PaginationLink buildPaginationLink(String url, String rel) {

        return new PaginationLink()
                .href(ConsentMgtEndpointUtil.buildURIForHeader(ConsentManagementConstants.V2_API_PATH_COMPONENT +
                        ConsentManagementConstants.CONSENTS_PATH + url).toString())
                .rel(rel);
    }

}
