/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * 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 android.adservices.adselection;

import static android.adservices.adselection.AdSelectionOutcome.UNSET_AD_SELECTION_ID;
import static android.adservices.adselection.AdSelectionOutcome.UNSET_AD_SELECTION_ID_MESSAGE;

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.view.InputEvent;

import com.android.internal.util.Preconditions;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.Objects;

/**
 * Request object wrapping the required arguments needed to report an ad event.
 */
public class ReportEventRequest {
    public static final int FLAG_REPORTING_DESTINATION_SELLER = 1 << 0;
    public static final int FLAG_REPORTING_DESTINATION_BUYER = 1 << 1;
    private static final int UNSET_REPORTING_DESTINATIONS = 0;
    private static final String UNSET_REPORTING_DESTINATIONS_MESSAGE =
            "Reporting destinations bitfield not set.";
    private static final String INVALID_REPORTING_DESTINATIONS_MESSAGE =
            "Invalid reporting destinations bitfield!";

    /** @hide */
    public static final long REPORT_EVENT_MAX_INTERACTION_DATA_SIZE_B = 64 * 1024; // 64 KB

    private static final String EVENT_DATA_SIZE_MAX_EXCEEDED =
            "Event data should not exceed " + REPORT_EVENT_MAX_INTERACTION_DATA_SIZE_B + " bytes";

    private final long mAdSelectionId;
    @NonNull private final String mEventKey;
    @Nullable private final InputEvent mInputEvent;
    @NonNull private final String mEventData;
    @ReportingDestination private final int mReportingDestinations; // buyer, seller, or both

    private ReportEventRequest(@NonNull Builder builder) {
        Objects.requireNonNull(builder);

        Preconditions.checkArgument(
                builder.mAdSelectionId != UNSET_AD_SELECTION_ID, UNSET_AD_SELECTION_ID_MESSAGE);
        Preconditions.checkArgument(
                builder.mReportingDestinations != UNSET_REPORTING_DESTINATIONS,
                UNSET_REPORTING_DESTINATIONS_MESSAGE);
        Preconditions.checkArgument(
                isValidDestination(builder.mReportingDestinations),
                INVALID_REPORTING_DESTINATIONS_MESSAGE);
        Preconditions.checkArgument(
                builder.mEventData.getBytes(StandardCharsets.UTF_8).length
                        <= REPORT_EVENT_MAX_INTERACTION_DATA_SIZE_B,
                EVENT_DATA_SIZE_MAX_EXCEEDED);

        this.mAdSelectionId = builder.mAdSelectionId;
        this.mEventKey = builder.mEventKey;
        this.mInputEvent = builder.mInputEvent;
        this.mEventData = builder.mEventData;
        this.mReportingDestinations = builder.mReportingDestinations;
    }

    /**
     * Returns the adSelectionId, the primary identifier of an ad selection process.
     */
    public long getAdSelectionId() {
        return mAdSelectionId;
    }

    /**
     * Returns the event key, the type of ad event to be reported.
     *
     * <p>This field will be used to fetch the {@code reportingUri} associated with the {@code
     * eventKey} registered in {@code registerAdBeacon} after ad selection.
     *
     * <p>This field should be an exact match to the {@code eventKey} registered in {@code
     * registerAdBeacon}. Specific details about {@code registerAdBeacon} can be found at the
     * documentation of {@link AdSelectionManager#reportImpression}
     *
     * <p>The event key (when inspecting its byte array with {@link String#getBytes()}) in {@code
     * UTF-8} format should not exceed 40 bytes. Any key exceeding this limit will not be registered
     * during the {@code registerAdBeacon} call.
     */
    @NonNull
    public String getKey() {
        return mEventKey;
    }

    /**
     * Returns the input event associated with the user interaction.
     *
     * <p>This field is either {@code null}, representing a <em>view</em> event, or has an {@link
     * InputEvent} object, representing a <em>click</em> event.
     */
    @Nullable
    public InputEvent getInputEvent() {
        return mInputEvent;
    }

    /**
     * Returns the ad event data.
     *
     * <p>After ad selection, this data is generated by the caller. The caller can then call {@link
     * AdSelectionManager#reportEvent}. This data will be attached in a POST request to the {@code
     * reportingUri} registered in {@code registerAdBeacon}.
     *
     * <p>The size of {@link String#getBytes()} in {@code UTF-8} format should be below 64KB.
     */
    @NonNull
    public String getData() {
        return mEventData;
    }

    /**
     * Returns the bitfield of reporting destinations to report to (buyer, seller, or both).
     *
     * <p>To create this bitfield, place an {@code |} bitwise operator between each {@code
     * reportingDestination} to be reported to. For example to only report to buyer, set the
     * reportingDestinations field to {@link #FLAG_REPORTING_DESTINATION_BUYER} To only report to
     * seller, set the reportingDestinations field to {@link #FLAG_REPORTING_DESTINATION_SELLER} To
     * report to both buyers and sellers, set the reportingDestinations field to {@link
     * #FLAG_REPORTING_DESTINATION_BUYER} | {@link #FLAG_REPORTING_DESTINATION_SELLER}
     */
    @ReportingDestination
    public int getReportingDestinations() {
        return mReportingDestinations;
    }

    /** @hide */
    @IntDef(
            flag = true,
            prefix = {"FLAG_REPORTING_DESTINATION"},
            value = {FLAG_REPORTING_DESTINATION_SELLER, FLAG_REPORTING_DESTINATION_BUYER})
    @Retention(RetentionPolicy.SOURCE)
    public @interface ReportingDestination {}

    private static boolean isValidDestination(@ReportingDestination int reportingDestinations) {
        return 0 < reportingDestinations
                && reportingDestinations
                        <= (FLAG_REPORTING_DESTINATION_SELLER | FLAG_REPORTING_DESTINATION_BUYER);
    }

    /** Builder for {@link ReportEventRequest} objects. */
    public static final class Builder {

        private long mAdSelectionId;
        @NonNull private String mEventKey;
        @Nullable private InputEvent mInputEvent;
        @NonNull private String mEventData;
        @ReportingDestination private int mReportingDestinations; // buyer, seller, or both

        public Builder(
                long adSelectionId,
                @NonNull String eventKey,
                @NonNull String eventData,
                @ReportingDestination int reportingDestinations) {
            Objects.requireNonNull(eventKey);
            Objects.requireNonNull(eventData);

            Preconditions.checkArgument(
                    adSelectionId != UNSET_AD_SELECTION_ID, UNSET_AD_SELECTION_ID_MESSAGE);
            Preconditions.checkArgument(
                    reportingDestinations != UNSET_REPORTING_DESTINATIONS,
                    UNSET_REPORTING_DESTINATIONS_MESSAGE);
            Preconditions.checkArgument(
                    isValidDestination(reportingDestinations),
                    INVALID_REPORTING_DESTINATIONS_MESSAGE);
            Preconditions.checkArgument(
                    eventData.getBytes(StandardCharsets.UTF_8).length
                            <= REPORT_EVENT_MAX_INTERACTION_DATA_SIZE_B,
                    EVENT_DATA_SIZE_MAX_EXCEEDED);

            this.mAdSelectionId = adSelectionId;
            this.mEventKey = eventKey;
            this.mEventData = eventData;
            this.mReportingDestinations = reportingDestinations;
        }

        /**
         * Sets the ad selection ID with which the rendered ad's events are associated.
         *
         * <p>See {@link #getAdSelectionId()} for more information.
         */
        @NonNull
        public Builder setAdSelectionId(long adSelectionId) {
            Preconditions.checkArgument(
                    adSelectionId != UNSET_AD_SELECTION_ID, UNSET_AD_SELECTION_ID_MESSAGE);
            mAdSelectionId = adSelectionId;
            return this;
        }

        /**
         * Sets the event key, the type of ad event to be reported.
         *
         * <p>See {@link #getKey()} for more information.
         */
        @NonNull
        public Builder setKey(@NonNull String eventKey) {
            Objects.requireNonNull(eventKey);

            mEventKey = eventKey;
            return this;
        }

        /**
         * Sets the input event associated with the user interaction.
         *
         * <p>See {@link #getInputEvent()} for more information.
         */
        @NonNull
        public Builder setInputEvent(@Nullable InputEvent inputEvent) {
            mInputEvent = inputEvent;
            return this;
        }

        /**
         * Sets the ad event data.
         *
         * <p>See {@link #getData()} for more information.
         */
        @NonNull
        public Builder setData(@NonNull String eventData) {
            Objects.requireNonNull(eventData);

            mEventData = eventData;
            return this;
        }

        /**
         * Sets the bitfield of reporting destinations to report to (buyer, seller, or both).
         *
         * <p>See {@link #getReportingDestinations()} for more information.
         */
        @NonNull
        public Builder setReportingDestinations(@ReportingDestination int reportingDestinations) {
            Preconditions.checkArgument(
                    isValidDestination(reportingDestinations),
                    INVALID_REPORTING_DESTINATIONS_MESSAGE);

            mReportingDestinations = reportingDestinations;
            return this;
        }

        /** Builds the {@link ReportEventRequest} object. */
        @NonNull
        public ReportEventRequest build() {
            return new ReportEventRequest(this);
        }
    }
}
