package io.delphiplatform.api.v3.view;

import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.DateTimeFormat.ISO;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.server.ResponseStatusException;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;

import io.delphiplatform.api.util.CollectionUtils;
import io.delphiplatform.api.util.DateUtils;
import io.delphiplatform.api.v3.constant.ApplicationConstants;
import io.delphiplatform.api.v3.constant.CountryCodeConstant;
import io.delphiplatform.api.v3.constant.DspConstants;
import io.delphiplatform.api.v3.model.AggBy;
import io.delphiplatform.api.v3.model.GroupByField;
import io.delphiplatform.api.v3.model.IncludeStreams;
import io.delphiplatform.api.v3.model.PeriodDaysWeeks;
import io.delphiplatform.api.v3.model.SortOrder;
import io.delphiplatform.api.v3.model.StreamDates;
import io.delphiplatform.api.v3.model.StreamsCountryCodeRankInfo;
import io.delphiplatform.api.v3.model.StreamsModel;
import io.delphiplatform.api.v3.model.StreamsNewListenersTopCountryResults;
import io.delphiplatform.api.v3.model.SubsetParam;
import io.delphiplatform.api.v3.model.TrackStreamsInsightsItem;
import io.delphiplatform.api.v3.view.interceptor.OnlyDeclaredParams;
import io.delphiplatform.api.v3.view.util.Params;
import lombok.NonNull;

import static io.delphiplatform.api.v3.constant.ApplicationConstants.COUNTRY_CODE_PATTERN;
import static io.delphiplatform.api.v3.constant.ApplicationConstants.MAX_PAGE_SIZE;

@Validated
public interface StreamsApi {

    String DEFAULT_TOP_ISRCS_REPORT_LIMIT = "5";

    CompletableFuture<ResponseEntity<StreamsModel>> getSearchData(@NonNull Params params);

    void validateParams(@NonNull Params params) throws ResponseStatusException;

    Params loadAdditionalParams(@NonNull Params params);

    ResponseEntity<StreamsNewListenersTopCountryResults> getStreamsNewListenersTopCountry(@NonNull Params params);

    /**
     * GET /streams : Search for Streams Resource route for getting a list of &#x60;Streams&#x60; with filtering based on provided parameters
     *
     * @param startDate         The earliest date to include in the query range. Data is start_date inclusive. (required)
     * @param endDate           The latest date to include in the query. Data returned is end_date inclusive. (required)
     * @param artistId          (optional)
     * @param isrc              Array of International Standard Recording Code (ISRC) numbers (optional, default to new ArrayList&lt;&gt;())
     * @param countryCode       Typically a lower-case two letter code for the country (optional, default to new ArrayList&lt;&gt;())
     * @param dsp               Slug name of a DSP (optional, default to new ArrayList&lt;&gt;())
     * @param playlistId        (optional, default to new ArrayList&lt;&gt;())
     * @param include           Include optional data like demographics. (optional)
     * @param aggBy             Specific term to aggregate the data (e.g. streams) on (optional)
     * @param groupByFields     Specific time-based term to group the data by (optional)
     * @param sumUpCountryCodes this param is a hack to preserve backwards-compatibility: normally we would just want user to pass
     *                          GroupByField.COUNTRY for all non-summing requests, but this isn't possible right now
     * @param subset            A subset of data to query without specifying a single item. For example, to query for a track in all playlists
     *                          without providing a &#x60;playlist_id&#x60;. Currently providing &#x60;subset&#x3D;playlists&#x60; requires
     *                          providing parameter &#x60;isrc&#x60; as well. Response items will also be aggregated by &#x60;playlist_id&#x60;
     *                          (in addition to ISRC). (optional)
     * @param limit             The maximum number of &#x60;items&#x60; to return in a single request. (optional)
     * @param offset            The number of &#x60;items&#x60; to offset for pagination. (optional)
     * @param limitRange        Limit of the range to query in days to return in a single request. Format: `days:28` (optional)
     * @param cursor            Cursor to query, containing start date in format `date:2020-01-01` to return in a single request. (optional)
     * @param sortBy            Field name by which to sort the data. (optional)
     * @param sortOrder         Direction to sort the data. Default: &#x60;desc&#x60; if &#x60;sort_by&#x60; provided. (optional, default to
     *                          desc)
     * @return &#x60;Streams&#x60; response object (status code 200) or 400 Invalid Input Response (status code 400) or 401 Unauthorized
     * (AuthError) Response (status code 401) or 500 Internal Error Response (status code 500)
     * <p>
     */
    @OnlyDeclaredParams
    @RequestMapping(value = "/streams",
        produces = {"application/json"},
        method = {RequestMethod.GET, RequestMethod.POST})
    default CompletableFuture<ResponseEntity<StreamsModel>> streamsSearch(
        @NotNull
        @RequestParam(value = "start_date")
        @DateTimeFormat(iso = ISO.DATE) LocalDate startDate,
        @NotNull
        @RequestParam(value = "end_date")
        @DateTimeFormat(iso = ISO.DATE) LocalDate endDate,
        @Pattern(regexp = ApplicationConstants.GRAS_ID_PATTERN)
        @RequestParam(value = "artist_id", required = false) String artistId,
        @RequestParam(value = "isrc", required = false)
            List<@Pattern(regexp = ApplicationConstants.ISRC_PATTERN) String> isrc,
        @RequestParam(value = "country_code", required = false) Set<String> countryCode,
        @RequestParam(value = "dsp", required = false) List<String> dsp,
        @RequestParam(value = "playlist_id", required = false) List<String> playlistId,
        @RequestParam(value = "include", required = false) Set<IncludeStreams> include,
        @RequestParam(value = "agg_by", required = false) AggBy aggBy,
        @RequestParam(value = "group_by", required = false) Set<GroupByField> groupByFields,
        //this param is just an extension of 'group_by', see comment
        @RequestParam(value = "sum_up_country_codes", required = false) Boolean sumUpCountryCodes,
        @RequestParam(value = "subset", required = false) SubsetParam subset,
        @Min(0) @Max(ApplicationConstants.MAX_BIGTABLE_PAGE_SIZE)
        @RequestParam(value = "limit", required = false) Integer limit,
        @Min(0) @RequestParam(value = "offset", required = false) Integer offset,
        @Pattern(regexp = ApplicationConstants.PAGE_LIMIT_RANGE_PATTERN)
        @RequestParam(value = "limit_range", required = false) String limitRange,
        @Pattern(regexp = ApplicationConstants.PAGE_CURSOR_PATTERN)
        @RequestParam(value = "cursor", required = false) String cursor,
        @RequestParam(value = "sort_by", required = false) String sortBy,
        @RequestParam(value = "sort_order", required = false, defaultValue = "desc") SortOrder sortOrder
    ) {
        Set<GroupByField> groupByFieldsSet = CollectionUtils.isNotEmpty(groupByFields) ? groupByFields : new HashSet<>();

        if (!Boolean.TRUE.equals(sumUpCountryCodes)) {
            groupByFieldsSet.add(GroupByField.COUNTRY);
        }

        Params params = Params.builder()
            .startDate(startDate)
            .endDate(endDate)
            .artistId(artistId)
            .isrc(isrc)
            .countryCode(countryCode)
            .dsp(dsp)
            .playlistId(playlistId)
            .includeStreams(include)
            .aggBy(aggBy)
            .groupByFields(groupByFieldsSet)
            .subset(subset)
            .limit(limit)
            .offset(offset)
            .limitRange(limitRange)
            .cursor(cursor)
            .sortBy(sortBy)
            .sortOrder(sortOrder)
            .build();
        validateParams(params);
        final Params loadedParams = loadAdditionalParams(params);
        return getSearchData(loadedParams);
    }

    /**
     * @param isrc      Array of International Standard Recording Code (ISRC) numbers (required)
     * @param dsp       Slug name of a DSP (optional, default to new all dsps)
     * @param limit     The maximum number of &#x60;items&#x60; to return in a single request. (optional)
     * @param offset    The number of &#x60;items&#x60; to offset for pagination. (optional)
     * @param sortBy    Field name by which to sort the data. (optional)
     * @param sortOrder Direction to sort the data. Default: &#x60;desc&#x60; if &#x60;sort_by&#x60; provided. (optional, default to desc)
     * @return &#x60;StreamDates&#x60; response object (status code 200) or 400 Invalid Input Response (status code 400) or 401 Unauthorized
     * (AuthError) Response (status code 401) or 500 Internal Error Response (status code 500)
     * <p>
     */
    @OnlyDeclaredParams
    @RequestMapping(value = "/stream-dates",
        produces = {"application/json"},
        method = RequestMethod.GET)
    default CompletableFuture<ResponseEntity<StreamDates>> streamDates(
        @RequestParam(value = "isrc") List<@Pattern(regexp = ApplicationConstants.ISRC_PATTERN) String> isrc,
        @RequestParam(value = "dsp", required = false) List<String> dsp,
        @Min(0) @Max(ApplicationConstants.MAX_BIGTABLE_PAGE_SIZE)
        @RequestParam(value = "limit", required = false) Integer limit,
        @Min(0) @RequestParam(value = "offset", required = false) Integer offset,
        @RequestParam(value = "sort_by", required = false) String sortBy,
        @RequestParam(value = "sort_order", required = false, defaultValue = "desc") SortOrder sortOrder
    ) {
        return getStreamDates(Params.builder()
            .isrc(isrc)
            .dsp(dsp)
            .limit(limit)
            .offset(offset)
            .sortBy(sortBy)
            .sortOrder(sortOrder)
            .countryCode(Set.of(CountryCodeConstant.WORLDWIDE))
            .startDate(ApplicationConstants.MIN_STREAMS_DATE)
            .endDate(DateUtils.getCurrentDate())
            .build());
    }

    CompletableFuture<ResponseEntity<StreamDates>> getStreamDates(Params params);

    @OnlyDeclaredParams
    @GetMapping(value = "/streams/project", produces = {MediaType.APPLICATION_JSON_VALUE})
    default CompletableFuture<ResponseEntity<StreamsModel>> getStreamsAggByProjects(
        @RequestParam(value = "start_date")
        @DateTimeFormat(iso = ISO.DATE) LocalDate startDate,
        @RequestParam(value = "end_date")
        @DateTimeFormat(iso = ISO.DATE) LocalDate endDate,
        @RequestParam(value = "project_number")
            Set<@Pattern(regexp = ApplicationConstants.GRAS_ID_PATTERN) String> projectNumbers,
        @RequestParam(value = "country_code", required = false) Set<String> countryCode,
        @RequestParam(value = "dsp", required = false) List<String> dsp,
        @RequestParam(value = "include", required = false) Set<IncludeStreams> include,
        @RequestParam(value = "group_by", required = false) Set<GroupByField> groupByFields,
        @Min(0) @Max(ApplicationConstants.MAX_BIGTABLE_PAGE_SIZE)
        @RequestParam(value = "limit", required = false) Integer limit,
        @Min(0) @RequestParam(value = "offset", required = false) Integer offset,
        @Pattern(regexp = ApplicationConstants.PAGE_LIMIT_RANGE_PATTERN)
        @RequestParam(value = "limit_range", required = false) String limitRange,
        @Pattern(regexp = ApplicationConstants.PAGE_CURSOR_PATTERN)
        @RequestParam(value = "cursor", required = false) String cursor,
        @RequestParam(value = "sort_by", required = false) String sortBy,
        @RequestParam(value = "sort_order", required = false, defaultValue = "desc") SortOrder sortOrder) {
        Set<GroupByField> groupByFieldsSet = CollectionUtils.isNotEmpty(groupByFields) ? groupByFields : new HashSet<>();
        groupByFieldsSet.add(GroupByField.COUNTRY);

        Params params = Params.builder()
            .startDate(startDate)
            .endDate(endDate)
            .projectNumbers(projectNumbers)
            .countryCode(countryCode)
            .dsp(dsp)
            .includeStreams(include)
            .groupByFields(groupByFieldsSet)
            .limit(limit)
            .offset(offset)
            .limitRange(limitRange)
            .cursor(cursor)
            .sortBy(sortBy)
            .sortOrder(sortOrder)
            .build();
        return getSearchData(params);
    }

    @OnlyDeclaredParams
    @GetMapping(value = "/streams/product-family", produces = {MediaType.APPLICATION_JSON_VALUE})
    default CompletableFuture<ResponseEntity<StreamsModel>> getStreamsAggByProductFamily(
        @RequestParam(value = "start_date")
        @DateTimeFormat(iso = ISO.DATE) LocalDate startDate,
        @RequestParam(value = "end_date")
        @DateTimeFormat(iso = ISO.DATE) LocalDate endDate,
        @RequestParam(value = "product_family_id") Set<String> productFamilyIds,
        @RequestParam(value = "country_code", required = false) Set<String> countryCode,
        @RequestParam(value = "dsp", required = false) List<String> dsp,
        @RequestParam(value = "include", required = false) Set<IncludeStreams> include,
        @RequestParam(value = "group_by", required = false) Set<GroupByField> groupByFields,
        @Min(0) @Max(ApplicationConstants.MAX_BIGTABLE_PAGE_SIZE)
        @RequestParam(value = "limit", required = false) Integer limit,
        @Min(0) @RequestParam(value = "offset", required = false) Integer offset,
        @Pattern(regexp = ApplicationConstants.PAGE_LIMIT_RANGE_PATTERN)
        @RequestParam(value = "limit_range", required = false) String limitRange,
        @Pattern(regexp = ApplicationConstants.PAGE_CURSOR_PATTERN)
        @RequestParam(value = "cursor", required = false) String cursor,
        @RequestParam(value = "sort_by", required = false) String sortBy,
        @RequestParam(value = "sort_order", required = false, defaultValue = "desc") SortOrder sortOrder) {
        Set<GroupByField> groupByFieldsSet = CollectionUtils.isNotEmpty(groupByFields) ? groupByFields : new HashSet<>();
        groupByFieldsSet.add(GroupByField.COUNTRY);

        Params params = Params.builder()
            .startDate(startDate)
            .endDate(endDate)
            .productFamilyIds(productFamilyIds)
            .countryCode(countryCode)
            .dsp(dsp)
            .includeStreams(include)
            .groupByFields(groupByFieldsSet)
            .limit(limit)
            .offset(offset)
            .limitRange(limitRange)
            .cursor(cursor)
            .sortBy(sortBy)
            .sortOrder(sortOrder)
            .build();
        return getSearchData(params);
    }

    @OnlyDeclaredParams
    @GetMapping(value = "/streams/new-listeners", produces = {MediaType.APPLICATION_JSON_VALUE})
    default CompletableFuture<ResponseEntity<StreamsModel>> getStreamsNewListeners(
        @RequestParam(value = "start_date")
        @DateTimeFormat(iso = ISO.DATE) LocalDate startDate,
        @RequestParam(value = "end_date")
        @DateTimeFormat(iso = ISO.DATE) LocalDate endDate,
        @RequestParam(value = "isrc")
            List<@Pattern(regexp = ApplicationConstants.ISRC_PATTERN) String> isrc,
        @RequestParam(value = "country_code", required = false) Set<@Pattern(regexp = COUNTRY_CODE_PATTERN) String> countryCode,
        @RequestParam(value = "dsp", required = false, defaultValue = DspConstants.SPOTIFY) List<String> dsp,
        @RequestParam(value = "include", required = false) Set<IncludeStreams> include,
        @RequestParam(value = "group_by", required = false) Set<GroupByField> groupByFields,
        @RequestParam(value = "subset", required = false) SubsetParam subset,
        @RequestParam(value = "playlist_id", required = false) List<String> playlistId,
        @Min(0) @Max(ApplicationConstants.MAX_BIGTABLE_PAGE_SIZE)
        @RequestParam(value = "limit", required = false) Integer limit,
        @Min(0) @RequestParam(value = "offset", required = false) Integer offset,
        @Pattern(regexp = ApplicationConstants.PAGE_LIMIT_RANGE_PATTERN)
        @RequestParam(value = "limit_range", required = false) String limitRange,
        @Pattern(regexp = ApplicationConstants.PAGE_CURSOR_PATTERN)
        @RequestParam(value = "cursor", required = false) String cursor,
        @RequestParam(value = "sort_by", required = false) String sortBy,
        @RequestParam(value = "sort_order", required = false, defaultValue = "desc") SortOrder sortOrder) {
        Set<GroupByField> groupByFieldsSet = CollectionUtils.isNotEmpty(groupByFields) ? groupByFields : new HashSet<>();
        groupByFieldsSet.add(GroupByField.COUNTRY);

        Params params = Params.builder()
            .startDate(startDate)
            .endDate(endDate)
            .isrc(isrc)
            .countryCode(countryCode)
            .dsp(dsp)
            .includeStreams(include)
            .groupByFields(groupByFieldsSet)
            .subset(subset)
            .playlistId(playlistId)
            .limit(limit)
            .offset(offset)
            .limitRange(limitRange)
            .isNewListeners(true)
            .cursor(cursor)
            .sortBy(sortBy)
            .sortOrder(sortOrder)
            .build();
        return getSearchData(params);
    }

    @OnlyDeclaredParams
    @RequestMapping(value = "/streams/new-listeners/top/country",
        produces = {"application/json"},
        method = RequestMethod.GET)
    default ResponseEntity<StreamsNewListenersTopCountryResults> getStreamsNewListenersTopCountry(
        @Pattern(regexp = COUNTRY_CODE_PATTERN)
        @RequestParam(value = "country_code") String countryCode,
        @RequestParam(value = "period_days") PeriodDaysWeeks periodDays,
        @RequestParam(value = "dsp", required = false, defaultValue = DspConstants.SPOTIFY) String dsp,
        @RequestParam(value = "date", required = false)
        @DateTimeFormat(iso = ISO.DATE) LocalDate reportDate,
        @Min(0) @Max(MAX_PAGE_SIZE) @RequestParam(value = "limit", required = false, defaultValue = DEFAULT_TOP_ISRCS_REPORT_LIMIT)
            Integer reportLimit
    ) {
        return getStreamsNewListenersTopCountry(Params.builder()
            .countryCode(Collections.singleton(countryCode))
            .periodDays(Collections.singleton(periodDays.getIntegerValue()))
            .dsp(dsp == null ? Collections.emptyList() : Collections.singletonList(dsp))
            .date(reportDate)
            .limit(reportLimit)
            .build());
    }

    @OnlyDeclaredParams
    @RequestMapping(value = "/streams/insights",
        produces = {"application/json"},
        method = RequestMethod.GET)
    default ResponseEntity<Map<String, List<TrackStreamsInsightsItem>>> getStreamsInsights(
        @RequestParam(value = "isrc") @Pattern(regexp = ApplicationConstants.ISRC_PATTERN) String isrc,
        @RequestParam(value = "country_code", required = false) Set<String> countryCode,
        @RequestParam(value = "dsp", required = false) List<String> dsp) {
        return getStreamsInsights(Params.builder()
            .isrc(List.of(isrc))
            .countryCode(countryCode)
            .dsp(dsp)
            .build());
    }

    ResponseEntity<Map<String, List<TrackStreamsInsightsItem>>> getStreamsInsights(Params params);

    @OnlyDeclaredParams
    @RequestMapping(value = "/streams/country-code/rank",
        produces = {"application/json"},
        method = RequestMethod.GET)
    default ResponseEntity<Map<String, List<StreamsCountryCodeRankInfo>>> getStreamsCountryCodeRank(
        @RequestParam(value = "dsp", required = false) List<String> dsp,
        @RequestParam(value = "country_code", required = false) Set<String> countryCode
    ) {
        return getStreamsCountryCodeRank(Params.builder()
            .countryCode(CollectionUtils.isEmpty(countryCode) ? Collections.emptySet() : new HashSet<>(countryCode))
            .dsp(CollectionUtils.isEmpty(dsp) ? Collections.emptyList() : new ArrayList<>(dsp))
            .build());
    }

    ResponseEntity<Map<String, List<StreamsCountryCodeRankInfo>>> getStreamsCountryCodeRank(Params params);
}
