/*
 * Copyright (C) 2022 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 com.android.server.healthconnect.common.changelog;

import static com.android.healthfitness.flags.AconfigFlagHelper.isCloudBackupRestoreEnabled;
import static com.android.server.healthconnect.fitness.recordhelpers.RecordHelper.PRIMARY_COLUMN_NAME;
import static com.android.server.healthconnect.storage.utils.StorageUtils.DELIMITER;
import static com.android.server.healthconnect.storage.utils.StorageUtils.INTEGER;
import static com.android.server.healthconnect.storage.utils.StorageUtils.PRIMARY;
import static com.android.server.healthconnect.storage.utils.StorageUtils.TEXT_NOT_NULL;
import static com.android.server.healthconnect.storage.utils.StorageUtils.TEXT_NULL;
import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorInt;
import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorIntegerList;
import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorString;
import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorStringList;
import static com.android.server.healthconnect.storage.utils.WhereClauses.LogicalOperator.AND;

import android.content.ContentValues;
import android.database.Cursor;
import android.health.connect.changelog.ChangeLogTokenRequest;
import android.health.connect.datatypes.MedicalResource;
import android.health.connect.datatypes.RecordTypeIdentifier;
import android.util.Pair;

import com.android.healthfitness.flags.AconfigFlagHelper;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.healthconnect.storage.DatabaseHelper;
import com.android.server.healthconnect.storage.TransactionManager;
import com.android.server.healthconnect.storage.request.AlterTableRequest;
import com.android.server.healthconnect.storage.request.CreateTableRequest;
import com.android.server.healthconnect.storage.request.DeleteTableRequest;
import com.android.server.healthconnect.storage.request.ReadTableRequest;
import com.android.server.healthconnect.storage.request.UpsertTableRequest;
import com.android.server.healthconnect.storage.utils.StorageUtils;
import com.android.server.healthconnect.storage.utils.WhereClauses;

import java.time.Duration;
import java.time.Instant;
import java.util.List;

/**
 * A class to interact with the DB table that stores the information about the change log requests
 * i.e. {@code TABLE_NAME}
 *
 * <p>This class returns the row_id of the change_log_request_table as a token, that can later be
 * used to recreate the request.
 *
 * @hide
 */
public final class ChangeLogsRequestHelper extends DatabaseHelper {
    public static final String TABLE_NAME = "change_log_request_table";
    private static final Duration DEFAULT_CHANGE_LOG_RETENTION = Duration.ofDays(32);
    private static final Duration NEW_CHANGE_LOG_RETENTION = Duration.ofDays(90);
    private static final String PACKAGES_TO_FILTER_COLUMN_NAME = "packages_to_filter";
    private static final String RECORD_TYPES_COLUMN_NAME = "record_types";
    private static final String PACKAGE_NAME_COLUMN_NAME = "package_name";
    private static final String ROW_ID_CHANGE_LOGS_TABLE_COLUMN_NAME = "row_id_change_logs_table";

    /**
     * Public because it is used in {@link
     * com.android.server.healthconnect.storage.DatabaseUpgradeHelper} to check if the PHR change
     * logs upgrade has already been applied.
     */
    public static final String MEDICAL_RESOURCE_TYPES_COLUMN_NAME = "medical_resource_types";

    @VisibleForTesting public static final String TIME_COLUMN_NAME = "time";

    private final TransactionManager mTransactionManager;

    public ChangeLogsRequestHelper(
            TransactionManager transactionManager, DatabaseHelpers databaseHelpers) {
        super(databaseHelpers);
        mTransactionManager = transactionManager;
    }

    @Override
    protected String getMainTableName() {
        return TABLE_NAME;
    }

    public static CreateTableRequest getCreateTableRequest() {
        var columns =
                List.of(
                        new Pair<>(PRIMARY_COLUMN_NAME, PRIMARY),
                        new Pair<>(PACKAGES_TO_FILTER_COLUMN_NAME, TEXT_NOT_NULL),
                        new Pair<>(PACKAGE_NAME_COLUMN_NAME, TEXT_NOT_NULL),
                        new Pair<>(RECORD_TYPES_COLUMN_NAME, TEXT_NULL),
                        new Pair<>(ROW_ID_CHANGE_LOGS_TABLE_COLUMN_NAME, INTEGER),
                        new Pair<>(TIME_COLUMN_NAME, INTEGER));
        return new CreateTableRequest(TABLE_NAME, columns);
    }

    /** Adds the required columns for the PHR change logs feature. */
    public static AlterTableRequest getAlterTableRequestForPhrChangeLogs() {
        var columns = List.of(new Pair<>(MEDICAL_RESOURCE_TYPES_COLUMN_NAME, TEXT_NULL));
        return new AlterTableRequest(TABLE_NAME, columns);
    }

    public static Duration getChangeLogRetentionDuration() {
        return isCloudBackupRestoreEnabled()
                ? NEW_CHANGE_LOG_RETENTION
                : DEFAULT_CHANGE_LOG_RETENTION;
    }

    public String getToken(
            long latestChangeLogRowId, String packageName, ChangeLogTokenRequest request) {
        ContentValues contentValues = new ContentValues();

        // Store package names here as a package name and not as {@link AppInfoHelper.AppInfo#mId}
        // as ID might not be available right now but might become available when the actual request
        // for this token comes
        contentValues.put(
                PACKAGES_TO_FILTER_COLUMN_NAME,
                String.join(DELIMITER, request.getPackageNamesToFilter()));
        contentValues.put(
                RECORD_TYPES_COLUMN_NAME,
                StorageUtils.flattenIntCollection(request.getRecordTypeIds()));
        contentValues.put(PACKAGE_NAME_COLUMN_NAME, packageName);
        contentValues.put(ROW_ID_CHANGE_LOGS_TABLE_COLUMN_NAME, latestChangeLogRowId);
        contentValues.put(TIME_COLUMN_NAME, Instant.now().toEpochMilli());
        if (AconfigFlagHelper.isPhrChangeLogsEnabled()) {
            contentValues.put(
                    MEDICAL_RESOURCE_TYPES_COLUMN_NAME,
                    StorageUtils.flattenIntCollection(request.getMedicalResourceTypes()));
        }

        return String.valueOf(
                mTransactionManager.insertOrThrowOnConflict(
                        new UpsertTableRequest(TABLE_NAME, contentValues)));
    }

    public TokenRequest getRequest(String packageName, String token) {
        if (token.isBlank()) {
            throw new IllegalArgumentException("Invalid token");
        }

        ReadTableRequest readTableRequest =
                new ReadTableRequest(TABLE_NAME)
                        .setWhereClause(
                                new WhereClauses(AND)
                                        .addWhereEqualsClause(PRIMARY_COLUMN_NAME, token)
                                        .addWhereEqualsClause(
                                                PACKAGE_NAME_COLUMN_NAME, packageName));
        try (Cursor cursor = mTransactionManager.read(readTableRequest)) {
            if (!cursor.moveToFirst()) {
                throw new IllegalArgumentException("Invalid token");
            }

            return new TokenRequest(
                    getCursorStringList(cursor, PACKAGES_TO_FILTER_COLUMN_NAME, DELIMITER),
                    getCursorIntegerList(cursor, RECORD_TYPES_COLUMN_NAME, DELIMITER),
                    AconfigFlagHelper.isPhrChangeLogsEnabled()
                            ? getCursorIntegerList(
                                    cursor, MEDICAL_RESOURCE_TYPES_COLUMN_NAME, DELIMITER)
                            : List.of(),
                    getCursorString(cursor, PACKAGE_NAME_COLUMN_NAME),
                    getCursorInt(cursor, ROW_ID_CHANGE_LOGS_TABLE_COLUMN_NAME));
        }
    }

    public String getNextPageToken(TokenRequest changeLogTokenRequest, long nextRowId) {
        ContentValues contentValues = new ContentValues();
        contentValues.put(
                PACKAGES_TO_FILTER_COLUMN_NAME,
                String.join(DELIMITER, changeLogTokenRequest.getPackageNamesToFilter()));
        contentValues.put(
                RECORD_TYPES_COLUMN_NAME,
                StorageUtils.flattenIntCollection(changeLogTokenRequest.getRecordTypes()));
        contentValues.put(
                PACKAGE_NAME_COLUMN_NAME, changeLogTokenRequest.getRequestingPackageName());
        contentValues.put(ROW_ID_CHANGE_LOGS_TABLE_COLUMN_NAME, nextRowId);
        if (AconfigFlagHelper.isPhrChangeLogsEnabled()) {
            contentValues.put(
                    MEDICAL_RESOURCE_TYPES_COLUMN_NAME,
                    StorageUtils.flattenIntCollection(
                            changeLogTokenRequest.getMedicalResourceTypes()));
        }

        return String.valueOf(
                mTransactionManager.insertOrThrowOnConflict(
                        new UpsertTableRequest(TABLE_NAME, contentValues)));
    }

    public static DeleteTableRequest getDeleteRequestForAutoDelete() {
        return new DeleteTableRequest(TABLE_NAME)
                .setTimeFilter(
                        TIME_COLUMN_NAME,
                        Instant.EPOCH.toEpochMilli(),
                        Instant.now().minus(getChangeLogRetentionDuration()).toEpochMilli());
    }

    /** A class to represent the request corresponding to a token */
    public static final class TokenRequest {
        private final List<String> mPackageNamesToFilter;
        private final List<@RecordTypeIdentifier.RecordType Integer> mRecordTypes;
        private final List<@MedicalResource.MedicalResourceType Integer> mMedicalResourceTypes;
        private final String mRequestingPackageName;
        private final long mRowIdChangeLogs;

        /**
         * @param requestingPackageName contributing package name
         * @param packageNamesToFilter package names to filter
         * @param recordTypes records to filter
         * @param rowIdChangeLogs row id of change log table after which the logs are to be fetched
         */
        public TokenRequest(
                List<String> packageNamesToFilter,
                List<@RecordTypeIdentifier.RecordType Integer> recordTypes,
                List<@MedicalResource.MedicalResourceType Integer> medicalResourceTypes,
                String requestingPackageName,
                long rowIdChangeLogs) {
            mPackageNamesToFilter = packageNamesToFilter;
            mRecordTypes = recordTypes;
            mMedicalResourceTypes = medicalResourceTypes;
            mRequestingPackageName = requestingPackageName;
            mRowIdChangeLogs = rowIdChangeLogs;
        }

        public long getRowIdChangeLogs() {
            return mRowIdChangeLogs;
        }

        public String getRequestingPackageName() {
            return mRequestingPackageName;
        }

        public List<String> getPackageNamesToFilter() {
            return mPackageNamesToFilter;
        }

        public List<@RecordTypeIdentifier.RecordType Integer> getRecordTypes() {
            return mRecordTypes;
        }

        public List<@MedicalResource.MedicalResourceType Integer> getMedicalResourceTypes() {
            return mMedicalResourceTypes;
        }
    }
}
