/*
 * 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 com.android.safetycenter.data;

import static com.android.permission.PermissionStatsLog.SAFETY_SOURCE_STATE_COLLECTED__SOURCE_STATE__DATA_PROVIDED;
import static com.android.permission.PermissionStatsLog.SAFETY_SOURCE_STATE_COLLECTED__SOURCE_STATE__NO_DATA_PROVIDED;
import static com.android.permission.PermissionStatsLog.SAFETY_SOURCE_STATE_COLLECTED__SOURCE_STATE__REFRESH_TIMEOUT;
import static com.android.permission.PermissionStatsLog.SAFETY_SOURCE_STATE_COLLECTED__SOURCE_STATE__SOURCE_CLEARED;
import static com.android.permission.PermissionStatsLog.SAFETY_SOURCE_STATE_COLLECTED__SOURCE_STATE__SOURCE_ERROR;

import android.annotation.UptimeMillisLong;
import android.annotation.UserIdInt;
import android.os.SystemClock;
import android.safetycenter.SafetyCenterData;
import android.safetycenter.SafetyEvent;
import android.safetycenter.SafetySourceData;
import android.safetycenter.SafetySourceErrorDetails;
import android.safetycenter.SafetySourceIssue;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;

import androidx.annotation.Nullable;

import com.android.safetycenter.SafetySourceKey;
import com.android.safetycenter.internaldata.SafetyCenterIssueActionId;
import com.android.safetycenter.internaldata.SafetyCenterIssueKey;
import com.android.safetycenter.logging.SafetyCenterStatsdLogger;

import java.io.PrintWriter;
import java.util.List;
import java.util.Objects;

import javax.annotation.concurrent.NotThreadSafe;

/**
 * Repository for {@link SafetySourceData} and other data managed by Safety Center including {@link
 * SafetySourceErrorDetails}.
 *
 * <p>This class isn't thread safe. Thread safety must be handled by the caller.
 */
@NotThreadSafe
final class SafetySourceDataRepository {

    private static final String TAG = "SafetySourceDataRepo";

    private final ArrayMap<SafetySourceKey, SafetySourceData> mSafetySourceData = new ArrayMap<>();
    private final ArraySet<SafetySourceKey> mSafetySourceErrors = new ArraySet<>();
    private final ArrayMap<SafetySourceKey, Long> mSafetySourceLastUpdated = new ArrayMap<>();
    private final ArrayMap<SafetySourceKey, Integer> mSourceStates = new ArrayMap<>();

    private final SafetyCenterInFlightIssueActionRepository
            mSafetyCenterInFlightIssueActionRepository;
    private final SafetyCenterIssueDismissalRepository mSafetyCenterIssueDismissalRepository;

    SafetySourceDataRepository(
            SafetyCenterInFlightIssueActionRepository safetyCenterInFlightIssueActionRepository,
            SafetyCenterIssueDismissalRepository safetyCenterIssueDismissalRepository) {
        mSafetyCenterInFlightIssueActionRepository = safetyCenterInFlightIssueActionRepository;
        mSafetyCenterIssueDismissalRepository = safetyCenterIssueDismissalRepository;
    }

    /**
     * Sets the latest {@link SafetySourceData} for the given {@link SafetySourceKey}, and returns
     * {@code true} if this caused any changes which would alter {@link SafetyCenterData}.
     *
     * <p>This method does not perform any validation, {@link
     * SafetyCenterDataManager#setSafetySourceData(SafetySourceData, String, SafetyEvent, String,
     * int)} should be called wherever validation is required.
     *
     * <p>Setting a {@code null} {@link SafetySourceData} evicts the current {@link
     * SafetySourceData} entry and clears the {@link SafetyCenterIssueDismissalRepository} for the
     * source.
     *
     * <p>This method may modify the {@link SafetyCenterIssueDismissalRepository}.
     */
    boolean setSafetySourceData(
            SafetySourceKey safetySourceKey, @Nullable SafetySourceData safetySourceData) {
        boolean sourceDataDiffers =
                !Objects.equals(safetySourceData, mSafetySourceData.get(safetySourceKey));
        boolean removedSourceError = mSafetySourceErrors.remove(safetySourceKey);

        if (sourceDataDiffers) {
            setSafetySourceDataInternal(safetySourceKey, safetySourceData);
        }

        setLastUpdatedNow(safetySourceKey);
        return sourceDataDiffers || removedSourceError;
    }

    private void setSafetySourceDataInternal(SafetySourceKey key, @Nullable SafetySourceData data) {
        ArraySet<String> issueIds = new ArraySet<>();
        if (data == null) {
            mSafetySourceData.remove(key);
            mSourceStates.put(key, SAFETY_SOURCE_STATE_COLLECTED__SOURCE_STATE__SOURCE_CLEARED);
        } else {
            mSafetySourceData.put(key, data);
            for (int i = 0; i < data.getIssues().size(); i++) {
                issueIds.add(data.getIssues().get(i).getId());
            }
            mSourceStates.put(key, SAFETY_SOURCE_STATE_COLLECTED__SOURCE_STATE__DATA_PROVIDED);
        }
        mSafetyCenterIssueDismissalRepository.updateIssuesForSource(
                issueIds, key.getSourceId(), key.getUserId());
    }

    /**
     * Returns the latest {@link SafetySourceData} that was set by {@link #setSafetySourceData} for
     * the given {@link SafetySourceKey}.
     *
     * <p>This method does not perform any validation, {@link
     * SafetyCenterDataManager#getSafetySourceData(String, String, int)} should be called wherever
     * validation is required.
     *
     * <p>Returns {@code null} if it was never set since boot, or if the entry was evicted using
     * {@link #setSafetySourceData} with a {@code null} value.
     */
    @Nullable
    SafetySourceData getSafetySourceData(SafetySourceKey safetySourceKey) {
        return mSafetySourceData.get(safetySourceKey);
    }

    /** Returns {@code true} if the given source has an error. */
    boolean sourceHasError(SafetySourceKey safetySourceKey) {
        return mSafetySourceErrors.contains(safetySourceKey);
    }

    /**
     * Returns whether the repository has the given {@link SafetySourceData} for the given {@link
     * SafetySourceKey}.
     */
    boolean sourceHasData(
            SafetySourceKey safetySourceKey, @Nullable SafetySourceData safetySourceData) {
        if (mSafetySourceErrors.contains(safetySourceKey)) {
            // Any error will cause the SafetySourceData to be discarded in favor of an error
            // message, so it can't possibly match the SafetySourceData passed in parameter.
            return false;
        }
        return Objects.equals(safetySourceData, mSafetySourceData.get(safetySourceKey));
    }

    /**
     * Reports the given {@link SafetySourceErrorDetails} for the given {@link SafetySourceKey}, and
     * returns {@code true} if this changed the repository's data.
     *
     * <p>This method does not perform any validation, {@link
     * SafetyCenterDataManager#reportSafetySourceError(SafetySourceErrorDetails, String, String,
     * int)} should be called wherever validation is required.
     */
    boolean reportSafetySourceError(
            SafetySourceKey safetySourceKey, SafetySourceErrorDetails safetySourceErrorDetails) {
        SafetyEvent safetyEvent = safetySourceErrorDetails.getSafetyEvent();
        Log.w(
                TAG,
                "Error reported from source: " + safetySourceKey + ", for event: " + safetyEvent);

        int safetyEventType = safetyEvent.getType();
        if (safetyEventType == SafetyEvent.SAFETY_EVENT_TYPE_RESOLVING_ACTION_FAILED
                || safetyEventType == SafetyEvent.SAFETY_EVENT_TYPE_RESOLVING_ACTION_SUCCEEDED) {
            return false;
        }

        mSourceStates.put(
                safetySourceKey, SAFETY_SOURCE_STATE_COLLECTED__SOURCE_STATE__SOURCE_ERROR);
        return setSafetySourceError(safetySourceKey);
    }

    /**
     * Marks the given {@link SafetySourceKey} as having timed out during a refresh, and returns
     * {@code true} if it caused a change to the stored data.
     *
     * @param setError whether we should clear the data associated with the source and set an error
     */
    boolean markSafetySourceRefreshTimedOut(SafetySourceKey sourceKey, boolean setError) {
        mSourceStates.put(sourceKey, SAFETY_SOURCE_STATE_COLLECTED__SOURCE_STATE__REFRESH_TIMEOUT);
        if (!setError) {
            return false;
        }
        return setSafetySourceError(sourceKey);
    }

    /**
     * Marks the given {@link SafetySourceKey} as being in an error state and returns {@code true}
     * if this changed the repository's data.
     */
    private boolean setSafetySourceError(SafetySourceKey safetySourceKey) {
        setLastUpdatedNow(safetySourceKey);
        boolean removingSafetySourceDataChangedSafetyCenterData =
                mSafetySourceData.remove(safetySourceKey) != null;
        boolean addingSafetySourceErrorChangedSafetyCenterData =
                mSafetySourceErrors.add(safetySourceKey);
        return removingSafetySourceDataChangedSafetyCenterData
                || addingSafetySourceErrorChangedSafetyCenterData;
    }

    /**
     * Returns the {@link SafetySourceIssue} associated with the given {@link SafetyCenterIssueKey}.
     *
     * <p>Returns {@code null} if there is no such {@link SafetySourceIssue}.
     */
    @Nullable
    SafetySourceIssue getSafetySourceIssue(SafetyCenterIssueKey safetyCenterIssueKey) {
        SafetySourceKey key =
                SafetySourceKey.of(
                        safetyCenterIssueKey.getSafetySourceId(), safetyCenterIssueKey.getUserId());
        SafetySourceData safetySourceData = mSafetySourceData.get(key);
        if (safetySourceData == null) {
            return null;
        }
        List<SafetySourceIssue> safetySourceIssues = safetySourceData.getIssues();

        SafetySourceIssue targetIssue = null;
        for (int i = 0; i < safetySourceIssues.size(); i++) {
            SafetySourceIssue safetySourceIssue = safetySourceIssues.get(i);

            if (safetyCenterIssueKey.getSafetySourceIssueId().equals(safetySourceIssue.getId())) {
                targetIssue = safetySourceIssue;
                break;
            }
        }

        return targetIssue;
    }

    /**
     * Returns the {@link SafetySourceIssue.Action} associated with the given {@link
     * SafetyCenterIssueActionId}.
     *
     * <p>Returns {@code null} if there is no associated {@link SafetySourceIssue}.
     *
     * <p>Returns {@code null} if the {@link SafetySourceIssue.Action} is currently in flight.
     */
    @Nullable
    SafetySourceIssue.Action getSafetySourceIssueAction(
            SafetyCenterIssueActionId safetyCenterIssueActionId) {
        SafetySourceIssue safetySourceIssue =
                getSafetySourceIssue(safetyCenterIssueActionId.getSafetyCenterIssueKey());

        if (safetySourceIssue == null) {
            return null;
        }

        return mSafetyCenterInFlightIssueActionRepository.getSafetySourceIssueAction(
                safetyCenterIssueActionId, safetySourceIssue);
    }

    /**
     * Returns the elapsed realtime millis of when the data of the given {@link SafetySourceKey} was
     * last updated, or {@code 0L} if no update has occurred.
     *
     * @see SystemClock#elapsedRealtime()
     */
    @UptimeMillisLong
    long getSafetySourceLastUpdated(SafetySourceKey sourceKey) {
        Long lastUpdated = mSafetySourceLastUpdated.get(sourceKey);
        if (lastUpdated != null) {
            return lastUpdated;
        } else {
            return 0L;
        }
    }

    private void setLastUpdatedNow(SafetySourceKey sourceKey) {
        mSafetySourceLastUpdated.put(sourceKey, SystemClock.elapsedRealtime());
    }

    /**
     * Returns the current {@link SafetyCenterStatsdLogger.SourceState} of the given {@link
     * SafetySourceKey}.
     */
    @SafetyCenterStatsdLogger.SourceState
    int getSourceState(SafetySourceKey sourceKey) {
        Integer sourceState = mSourceStates.get(sourceKey);
        if (sourceState != null) {
            return sourceState;
        } else {
            return SAFETY_SOURCE_STATE_COLLECTED__SOURCE_STATE__NO_DATA_PROVIDED;
        }
    }

    /** Clears all data for all users. */
    void clear() {
        mSafetySourceData.clear();
        mSafetySourceErrors.clear();
        mSafetySourceLastUpdated.clear();
        mSourceStates.clear();
    }

    /** Clears all data for the given user. */
    void clearForUser(@UserIdInt int userId) {
        // Loop in reverse index order to be able to remove entries while iterating.
        for (int i = mSafetySourceData.size() - 1; i >= 0; i--) {
            SafetySourceKey sourceKey = mSafetySourceData.keyAt(i);
            if (sourceKey.getUserId() == userId) {
                mSafetySourceData.removeAt(i);
            }
        }
        for (int i = mSafetySourceErrors.size() - 1; i >= 0; i--) {
            SafetySourceKey sourceKey = mSafetySourceErrors.valueAt(i);
            if (sourceKey.getUserId() == userId) {
                mSafetySourceErrors.removeAt(i);
            }
        }
        for (int i = mSafetySourceLastUpdated.size() - 1; i >= 0; i--) {
            SafetySourceKey sourceKey = mSafetySourceLastUpdated.keyAt(i);
            if (sourceKey.getUserId() == userId) {
                mSafetySourceLastUpdated.removeAt(i);
            }
        }
        for (int i = mSourceStates.size() - 1; i >= 0; i--) {
            SafetySourceKey sourceKey = mSourceStates.keyAt(i);
            if (sourceKey.getUserId() == userId) {
                mSourceStates.removeAt(i);
            }
        }
    }

    /** Dumps state for debugging purposes. */
    void dump(PrintWriter fout) {
        dumpArrayMap(fout, mSafetySourceData, "SOURCE DATA");
        int errorCount = mSafetySourceErrors.size();
        fout.println("SOURCE ERRORS (" + errorCount + ")");
        for (int i = 0; i < errorCount; i++) {
            SafetySourceKey key = mSafetySourceErrors.valueAt(i);
            fout.println("\t[" + i + "] " + key);
        }
        fout.println();
        dumpArrayMap(fout, mSafetySourceLastUpdated, "LAST UPDATED");
        dumpArrayMap(fout, mSourceStates, "SOURCE STATES");
    }

    private static <K, V> void dumpArrayMap(PrintWriter fout, ArrayMap<K, V> map, String label) {
        int count = map.size();
        fout.println(label + " (" + count + ")");
        for (int i = 0; i < count; i++) {
            fout.println("\t[" + i + "] " + map.keyAt(i) + " -> " + map.valueAt(i));
        }
        fout.println();
    }
}