/*
 * 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 android.os.Build.VERSION_CODES.TIRAMISU;
import static com.android.safetycenter.data.SafetyCenterIssueDeduplicator.DeduplicationInfo;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static java.util.Collections.emptySet;
import android.annotation.UserIdInt;
import android.content.Context;
import android.safetycenter.SafetySourceData;
import android.safetycenter.SafetySourceIssue;
import android.safetycenter.config.SafetySource;
import android.safetycenter.config.SafetySourcesGroup;
import android.util.SparseArray;
import androidx.annotation.RequiresApi;
import com.android.modules.utils.build.SdkLevel;
import com.android.permission.util.UserUtils;
import com.android.safetycenter.SafetyCenterConfigReader;
import com.android.safetycenter.SafetySourceIssueInfo;
import com.android.safetycenter.SafetySourceKey;
import com.android.safetycenter.SafetySources;
import com.android.safetycenter.UserProfileGroup;
import com.android.safetycenter.internaldata.SafetyCenterIssueKey;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import javax.annotation.concurrent.NotThreadSafe;
/**
 * Contains issue related data.
 *
 * 
Responsible for generating lists of issues and deduplication of issues.
 */
@RequiresApi(TIRAMISU)
@NotThreadSafe
final class SafetyCenterIssueRepository {
    private static final SafetySourceIssuesInfoBySeverityDescending
            SAFETY_SOURCE_ISSUES_INFO_BY_SEVERITY_DESCENDING =
                    new SafetySourceIssuesInfoBySeverityDescending();
    private static final DeduplicationInfo EMPTY_DEDUP_INFO =
            new DeduplicationInfo(emptyList(), emptyList(), emptyMap());
    private final Context mContext;
    private final SafetySourceDataRepository mSafetySourceDataRepository;
    private final SafetyCenterConfigReader mSafetyCenterConfigReader;
    private final SafetyCenterIssueDismissalRepository mSafetyCenterIssueDismissalRepository;
    private final SafetyCenterIssueDeduplicator mSafetyCenterIssueDeduplicator;
    private final SparseArray mUserIdToDedupInfo = new SparseArray<>();
    SafetyCenterIssueRepository(
            Context context,
            SafetySourceDataRepository safetySourceDataRepository,
            SafetyCenterConfigReader safetyCenterConfigReader,
            SafetyCenterIssueDismissalRepository safetyCenterIssueDismissalRepository,
            SafetyCenterIssueDeduplicator safetyCenterIssueDeduplicator) {
        mContext = context;
        mSafetySourceDataRepository = safetySourceDataRepository;
        mSafetyCenterConfigReader = safetyCenterConfigReader;
        mSafetyCenterIssueDismissalRepository = safetyCenterIssueDismissalRepository;
        mSafetyCenterIssueDeduplicator = safetyCenterIssueDeduplicator;
    }
    /**
     * Updates the class as per the current state of issues. Should be called after any state update
     * that can affect issues.
     */
    void updateIssues(UserProfileGroup userProfileGroup) {
        updateIssues(userProfileGroup.getProfileParentUserId(), /* isManagedProfile= */ false);
        int[] managedProfileUserIds = userProfileGroup.getManagedProfilesUserIds();
        for (int i = 0; i < managedProfileUserIds.length; i++) {
            updateIssues(managedProfileUserIds[i], /* isManagedProfile= */ true);
        }
    }
    /**
     * Updates the class as per the current state of issues. Should be called after any state update
     * that can affect issues.
     */
    void updateIssues(@UserIdInt int userId) {
        updateIssues(userId, UserUtils.isManagedProfile(userId, mContext));
    }
    private void updateIssues(@UserIdInt int userId, boolean isManagedProfile) {
        List issues =
                getAllStoredIssuesFromRawSourceData(userId, isManagedProfile);
        issues.sort(SAFETY_SOURCE_ISSUES_INFO_BY_SEVERITY_DESCENDING);
        mUserIdToDedupInfo.put(userId, produceDedupInfo(issues));
    }
    private DeduplicationInfo produceDedupInfo(List issues) {
        if (SdkLevel.isAtLeastU()) {
            return mSafetyCenterIssueDeduplicator.deduplicateIssues(issues);
        }
        return new DeduplicationInfo(issues, emptyList(), emptyMap());
    }
    /**
     * Fetches a list of issues related to the given {@link UserProfileGroup}.
     *
     * Issues in the list are sorted in descending order and deduplicated (if applicable, only on
     * Android U+).
     *
     * 
Only includes issues related to active/running {@code userId}s in the given {@link
     * UserProfileGroup}.
     */
    List getIssuesDedupedSortedDescFor(UserProfileGroup userProfileGroup) {
        List issuesInfo = getIssuesFor(userProfileGroup);
        issuesInfo.sort(SAFETY_SOURCE_ISSUES_INFO_BY_SEVERITY_DESCENDING);
        return issuesInfo;
    }
    /**
     * Counts the total number of issues from loggable sources, in the given {@link
     * UserProfileGroup}.
     *
     * Only includes issues related to active/running {@code userId}s in the given {@link
     * UserProfileGroup}.
     */
    int countLoggableIssuesFor(UserProfileGroup userProfileGroup) {
        List relevantIssues = getIssuesFor(userProfileGroup);
        int issueCount = 0;
        for (int i = 0; i < relevantIssues.size(); i++) {
            SafetySourceIssueInfo safetySourceIssueInfo = relevantIssues.get(i);
            if (SafetySources.isLoggable(safetySourceIssueInfo.getSafetySource())) {
                issueCount++;
            }
        }
        return issueCount;
    }
    /** Gets a list of all issues for the given {@code userId}. */
    List getIssuesForUser(@UserIdInt int userId) {
        return filterOutHiddenIssues(
                mUserIdToDedupInfo.get(userId, EMPTY_DEDUP_INFO).getDeduplicatedIssues());
    }
    /**
     * Returns a set of {@link SafetySourcesGroup} IDs that the given {@link SafetyCenterIssueKey}
     * is mapped to, or an empty list if no such mapping is configured.
     */
    Set getGroupMappingFor(SafetyCenterIssueKey issueKey) {
        return mUserIdToDedupInfo
                .get(issueKey.getUserId(), EMPTY_DEDUP_INFO)
                .getIssueToGroupMapping()
                .getOrDefault(issueKey, emptySet());
    }
    /**
     * Returns the list of issues for the given {@code userId} which were removed from the given
     * list of issues in the most recent {@link SafetyCenterIssueDeduplicator#deduplicateIssues}
     * call. These issues were removed because they were duplicates of other issues.
     *
     * If this method is called before any calls to {@link
     * SafetyCenterIssueDeduplicator#deduplicateIssues} then an empty list is returned.
     */
    List getLatestDuplicates(@UserIdInt int userId) {
        return mUserIdToDedupInfo.get(userId, EMPTY_DEDUP_INFO).getFilteredOutDuplicateIssues();
    }
    private List filterOutHiddenIssues(List issues) {
        List result = new ArrayList<>();
        for (int i = 0; i < issues.size(); i++) {
            SafetySourceIssueInfo issueInfo = issues.get(i);
            if (!mSafetyCenterIssueDismissalRepository.isIssueHidden(
                    issueInfo.getSafetyCenterIssueKey())) {
                result.add(issueInfo);
            }
        }
        return result;
    }
    private List getAllStoredIssuesFromRawSourceData(
            @UserIdInt int userId, boolean isManagedProfile) {
        List allIssuesInfo = new ArrayList<>();
        List safetySourcesGroups =
                mSafetyCenterConfigReader.getSafetySourcesGroups();
        for (int j = 0; j < safetySourcesGroups.size(); j++) {
            addSafetySourceIssuesInfo(
                    allIssuesInfo, safetySourcesGroups.get(j), userId, isManagedProfile);
        }
        return allIssuesInfo;
    }
    private void addSafetySourceIssuesInfo(
            List issuesInfo,
            SafetySourcesGroup safetySourcesGroup,
            @UserIdInt int userId,
            boolean isManagedProfile) {
        List safetySources = safetySourcesGroup.getSafetySources();
        for (int i = 0; i < safetySources.size(); i++) {
            SafetySource safetySource = safetySources.get(i);
            if (!SafetySources.isExternal(safetySource)) {
                continue;
            }
            if (isManagedProfile && !SafetySources.supportsManagedProfiles(safetySource)) {
                continue;
            }
            addSafetySourceIssuesInfo(issuesInfo, safetySource, safetySourcesGroup, userId);
        }
    }
    private void addSafetySourceIssuesInfo(
            List issuesInfo,
            SafetySource safetySource,
            SafetySourcesGroup safetySourcesGroup,
            @UserIdInt int userId) {
        SafetySourceKey key = SafetySourceKey.of(safetySource.getId(), userId);
        SafetySourceData safetySourceData = mSafetySourceDataRepository.getSafetySourceData(key);
        if (safetySourceData == null) {
            return;
        }
        List safetySourceIssues = safetySourceData.getIssues();
        for (int i = 0; i < safetySourceIssues.size(); i++) {
            SafetySourceIssue safetySourceIssue = safetySourceIssues.get(i);
            SafetySourceIssueInfo safetySourceIssueInfo =
                    new SafetySourceIssueInfo(
                            safetySourceIssue, safetySource, safetySourcesGroup, userId);
            issuesInfo.add(safetySourceIssueInfo);
        }
    }
    /**
     * Only includes issues related to active/running {@code userId}s in the given {@link
     * UserProfileGroup}.
     */
    private List getIssuesFor(UserProfileGroup userProfileGroup) {
        List issues =
                new ArrayList<>(getIssuesForUser(userProfileGroup.getProfileParentUserId()));
        int[] managedRunningProfileUserIds = userProfileGroup.getManagedRunningProfilesUserIds();
        for (int i = 0; i < managedRunningProfileUserIds.length; i++) {
            issues.addAll(getIssuesForUser(managedRunningProfileUserIds[i]));
        }
        return issues;
    }
    /** A comparator to order {@link SafetySourceIssueInfo} by severity level descending. */
    private static final class SafetySourceIssuesInfoBySeverityDescending
            implements Comparator {
        private SafetySourceIssuesInfoBySeverityDescending() {}
        @Override
        public int compare(SafetySourceIssueInfo left, SafetySourceIssueInfo right) {
            return Integer.compare(
                    right.getSafetySourceIssue().getSeverityLevel(),
                    left.getSafetySourceIssue().getSeverityLevel());
        }
    }
    /** Dumps state for debugging purposes. */
    void dump(PrintWriter fout) {
        fout.println("ISSUE REPOSITORY");
        for (int i = 0; i < mUserIdToDedupInfo.size(); i++) {
            fout.println();
            fout.println("\tUSER ID=" + mUserIdToDedupInfo.keyAt(i));
            fout.println("\tDEDUPLICATION INFO=" + mUserIdToDedupInfo.valueAt(i));
        }
        fout.println();
    }
    /** Clears all the data from the repository. */
    void clear() {
        mUserIdToDedupInfo.clear();
    }
    /** Clears all data related to the given {@code userId}. */
    void clearForUser(@UserIdInt int userId) {
        mUserIdToDedupInfo.delete(userId);
    }
}