/**
 * Copyright © 2016-2026 The Thingsboard Authors
 *
 * 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.thingsboard.server.controller;

import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.edqs.EdqsState;
import org.thingsboard.server.common.data.edqs.ToCoreEdqsRequest;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.AlarmCountQuery;
import org.thingsboard.server.common.data.query.AlarmData;
import org.thingsboard.server.common.data.query.AlarmDataQuery;
import org.thingsboard.server.common.data.query.AvailableEntityKeys;
import org.thingsboard.server.common.data.query.AvailableEntityKeysV2;
import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataPageLink;
import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.common.data.query.EntityFilter;
import org.thingsboard.server.common.msg.edqs.EdqsService;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.query.EntityQueryService;
import org.thingsboard.server.service.security.permission.Operation;

import java.util.Set;

import static org.thingsboard.server.controller.ControllerConstants.ALARM_DATA_QUERY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_COUNT_QUERY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_DATA_QUERY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;

@RestController
@TbCoreComponent
@RequestMapping("/api")
@RequiredArgsConstructor
public class EntityQueryController extends BaseController {

    private final EntityQueryService entityQueryService;
    private final EdqsService edqsService;

    private static final int MAX_PAGE_SIZE = 100;

    @ApiOperation(value = "Count Entities by Query", notes = ENTITY_COUNT_QUERY_DESCRIPTION)
    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
    @PostMapping("/entitiesQuery/count")
    public long countEntitiesByQuery(
            @Parameter(description = "A JSON value representing the entity count query. See API call notes above for more details.")
            @RequestBody EntityCountQuery query) throws ThingsboardException {
        checkNotNull(query);
        resolveQuery(query);
        return entityQueryService.countEntitiesByQuery(getCurrentUser(), query);
    }

    @ApiOperation(value = "Find Entity Data by Query", notes = ENTITY_DATA_QUERY_DESCRIPTION)
    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
    @PostMapping("/entitiesQuery/find")
    public PageData<EntityData> findEntityDataByQuery(
            @Parameter(description = "A JSON value representing the entity data query. See API call notes above for more details.")
            @RequestBody EntityDataQuery query) throws ThingsboardException {
        checkNotNull(query);
        resolveQuery(query);
        return entityQueryService.findEntityDataByQuery(getCurrentUser(), query);
    }

    @ApiOperation(value = "Find Alarms by Query", notes = ALARM_DATA_QUERY_DESCRIPTION)
    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
    @PostMapping("/alarmsQuery/find")
    public PageData<AlarmData> findAlarmDataByQuery(
            @Parameter(description = "A JSON value representing the alarm data query. See API call notes above for more details.")
            @RequestBody AlarmDataQuery query) throws ThingsboardException {
        checkNotNull(query);
        checkNotNull(query.getPageLink());
        UserId assigneeId = query.getPageLink().getAssigneeId();
        if (assigneeId != null) {
            checkUserId(assigneeId, Operation.READ);
        }
        resolveQuery(query);
        return entityQueryService.findAlarmDataByQuery(getCurrentUser(), query);
    }

    @ApiOperation(value = "Count Alarms by Query (countAlarmsByQuery)", notes = "Returns the number of alarms that match the query definition.")
    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
    @PostMapping("/alarmsQuery/count")
    public long countAlarmsByQuery(@Parameter(description = "A JSON value representing the alarm count query.")
                                   @RequestBody AlarmCountQuery query) throws ThingsboardException {
        checkNotNull(query);
        UserId assigneeId = query.getAssigneeId();
        if (assigneeId != null) {
            checkUserId(assigneeId, Operation.READ);
        }
        resolveQuery(query);
        return entityQueryService.countAlarmsByQuery(getCurrentUser(), query);
    }

    @Deprecated(forRemoval = true)
    @ApiOperation(
            value = "Find Available Entity Keys by Query (deprecated)",
            notes = """
                    **Deprecated.** Use the V2 endpoint (`POST /api/v2/entitiesQuery/find/keys`) instead.\n
                    Returns unique time series and/or attribute key names from entities matching the query.\n
                    Executes the Entity Data Query to find up to 100 entities, then fetches and aggregates all distinct key names.\n
                    Primarily used for UI features like autocomplete suggestions.""" + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH
    )
    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
    @PostMapping("/entitiesQuery/find/keys")
    public DeferredResult<AvailableEntityKeys> findAvailableEntityKeysByQuery(
            @Parameter(description = "Entity data query to find entities. Page size is capped at 100.")
            @RequestBody EntityDataQuery query,

            @Parameter(description = """
                    When true, includes unique time series key names in the response.
                    When false, the 'timeseries' list will be empty.""")
            @RequestParam("timeseries") boolean includeTimeseries,

            @Parameter(description = """
                    When true, includes unique attribute key names in the response.
                    When false, the 'attribute' list will be empty. Use 'scope' parameter to filter by attribute scope.""")
            @RequestParam("attributes") boolean includeAttributes,

            @Parameter(description = """
                    Filters attribute keys by scope. Only applies when 'attributes' is true.
                    If not specified, returns attribute keys from all scopes.""",
                    schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}))
            @RequestParam(value = "scope", required = false) AttributeScope scope
    ) throws ThingsboardException {
        resolveQuery(query);
        EntityDataPageLink pageLink = query.getPageLink();
        if (pageLink.getPageSize() > MAX_PAGE_SIZE) {
            pageLink.setPageSize(MAX_PAGE_SIZE);
        }
        return wrapFuture(entityQueryService.getKeysByQuery(getCurrentUser(), getTenantId(), query, includeTimeseries, includeAttributes, scope));
    }

    @ApiOperation(
            value = "Find Available Entity Keys By Query",
            notes = """
                    Discovers unique time series and/or attribute key names available on entities that match the given query.
                    Works in two steps: first, the request body (an Entity Data Query) is executed to find matching entities
                    (page size is capped at 100); then, all distinct key names are collected from those entities.\n
                    Optionally, each key can include a sample — the most recent value (by timestamp) for that key
                    across all matched entities."""
                    + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH
    )
    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
    @PostMapping("/v2/entitiesQuery/find/keys")
    public DeferredResult<AvailableEntityKeysV2> findAvailableEntityKeysByQueryV2(
            @Parameter(description = "Entity data query to find entities. Page size is capped at 100.")
            @RequestBody EntityDataQuery query,

            @Parameter(description = """
                    When true, includes unique time series keys in the response.
                    When false, the 'timeseries' field is omitted. At least one of 'includeTimeseries' or 'includeAttributes' must be true.""")
            @RequestParam(defaultValue = "true") boolean includeTimeseries,

            @Parameter(description = """
                    When true, includes unique attribute keys in the response.
                    When false, the 'attributes' field is omitted. At least one of 'includeTimeseries' or 'includeAttributes' must be true.""")
            @RequestParam(defaultValue = "true") boolean includeAttributes,

            @Parameter(description = """
                    Filters attribute keys by scope. Only applies when 'includeAttributes' is true.
                    When not specified, scopes are auto-determined: all three scopes (server, client, shared) for device entities,
                    server scope only for other entity types.""",
                    schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}))
            @RequestParam(required = false) Set<AttributeScope> scopes,

            @Parameter(description = """
                    When true, each key entry includes a 'sample' object with the most recent value and timestamp.
                    When false, only key names are returned (sample is omitted from JSON).""")
            @RequestParam(defaultValue = "false") boolean includeSamples
    ) throws ThingsboardException {
        resolveQuery(query);
        EntityDataPageLink pageLink = query.getPageLink();
        if (pageLink.getPageSize() > MAX_PAGE_SIZE) {
            pageLink.setPageSize(MAX_PAGE_SIZE);
        }
        return wrapFuture(entityQueryService.findAvailableEntityKeysByQuery(
                getCurrentUser(), query,
                includeTimeseries, includeAttributes, scopes, includeSamples));
    }

    @PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
    @PostMapping("/edqs/system/request")
    public void processSystemEdqsRequest(@RequestBody ToCoreEdqsRequest request) {
        edqsService.processSystemRequest(request);
    }

    @PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
    @GetMapping("/edqs/state")
    public EdqsState getEdqsState() {
        return edqsService.getState();
    }

    private void resolveQuery(EntityCountQuery query) throws ThingsboardException {
        if (query.getEntityFilter() != null) {
            var user = getCurrentUser();
            var customerId = user.getCustomerId();
            var ownerId = customerId != null && !customerId.isNullUid() ? customerId : getTenantId();
            EntityFilter.resolveEntityFilter(query.getEntityFilter(), getTenantId(), user.getId(), ownerId);
        }
    }

}
