/*
 * Copyright (C) 2024 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.ondevicepersonalization.services.data.user;

import android.annotation.NonNull;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;

import com.android.internal.annotations.VisibleForTesting;
import com.android.odp.module.common.Clock;
import com.android.odp.module.common.MonotonicClock;
import com.android.ondevicepersonalization.internal.util.LoggerFactory;
import com.android.ondevicepersonalization.services.data.OnDevicePersonalizationDbHelper;
import com.android.ondevicepersonalization.services.fbs.AppInfo;
import com.android.ondevicepersonalization.services.fbs.AppInfoList;

import com.google.common.primitives.Ints;
import com.google.flatbuffers.FlatBufferBuilder;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

public class UserDataDao {
    private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
    private static final String TAG = UserDataDao.class.getSimpleName();
    private static volatile UserDataDao sSingleton;
    private final OnDevicePersonalizationDbHelper mDbHelper;
    private final Clock mClock;

    private UserDataDao(@NonNull OnDevicePersonalizationDbHelper dbHelper, Clock clock) {
        this.mDbHelper = dbHelper;
        this.mClock = clock;
    }

    /** Returns an instance of the EventsDao given a context. */
    public static UserDataDao getInstance(@NonNull Context context) {
        if (sSingleton == null) {
            synchronized (UserDataDao.class) {
                if (sSingleton == null) {
                    OnDevicePersonalizationDbHelper dbHelper =
                            OnDevicePersonalizationDbHelper.getInstance(context);
                    sSingleton = new UserDataDao(dbHelper, MonotonicClock.getInstance());
                }
            }
        }
        return sSingleton;
    }

    /** Returns an instance of the EventsDao given a context. This is used for testing only. */
    @VisibleForTesting
    public static UserDataDao getInstanceForTest(@NonNull Context context, Clock clock) {
        synchronized (UserDataDao.class) {
            if (sSingleton == null) {
                OnDevicePersonalizationDbHelper dbHelper =
                        OnDevicePersonalizationDbHelper.getInstanceForTest(context);
                sSingleton = new UserDataDao(dbHelper, clock);
            }
            return sSingleton;
        }
    }

    /** Returns an instance of the EventsDao given a context. This is used for testing only. */
    @VisibleForTesting
    public static UserDataDao getInstanceForTest(@NonNull Context context) {
        synchronized (UserDataDao.class) {
            if (sSingleton == null) {
                OnDevicePersonalizationDbHelper dbHelper =
                        OnDevicePersonalizationDbHelper.getInstanceForTest(context);
                sSingleton = new UserDataDao(dbHelper, MonotonicClock.getInstance());
            }
            return sSingleton;
        }
    }

    /** Inserts or replaces an entry in AppInstall table. */
    public boolean insertAppInstall(Map<String, Long> appInstallList) {
        SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
        if (db == null) {
            return false;
        }

        byte[] appInfoList = convertAppInstallFromMapToBytes(appInstallList);
        ContentValues values = new ContentValues();
        values.put(UserDataContract.AppInstall.APP_LIST, appInfoList);
        values.put(UserDataContract.AppInstall.CREATION_TIME, mClock.currentTimeMillis());
        long jobId =
                db.insertWithOnConflict(
                        UserDataContract.AppInstall.TABLE_NAME,
                        null,
                        values,
                        SQLiteDatabase.CONFLICT_REPLACE);
        return jobId != -1;
    }

    /** Gets the app installed map . */
    @NonNull
    public Map<String, Long> getAppInstallMap() {
        Map<String, Long> appInstallMap = new HashMap<>();

        SQLiteDatabase db = mDbHelper.safeGetReadableDatabase();
        if (db == null) {
            return appInstallMap;
        }
        String[] projection = {UserDataContract.AppInstall.APP_LIST};
        String orderBy = UserDataContract.AppInstall.CREATION_TIME + " DESC";
        try (Cursor cursor =
                db.query(
                        UserDataContract.AppInstall.TABLE_NAME,
                        projection,
                        null,
                        null,
                        /* groupBy= */ null,
                        /* having= */ null,
                        /* orderBy= */ orderBy)) {
            if (cursor.moveToNext()) {
                byte[] blob =
                        cursor.getBlob(
                                cursor.getColumnIndexOrThrow(UserDataContract.AppInstall.APP_LIST));
                cursor.close();
                return convertAppInstallFromBytesToMap(blob);
            }
        }
        return appInstallMap;
    }

    /** Deletes all entries in AppInstall table. */
    public boolean deleteAllAppInstallTable() {
        SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
        if (db == null) {
            return false;
        }
        boolean success = false;
        db.beginTransaction();
        try {
            db.delete(UserDataContract.AppInstall.TABLE_NAME, null, null);
            success = true;
            db.setTransactionSuccessful();
        } catch (SQLiteException e) {
            // TODO(b/337481657): add logging for db failure.
            sLogger.e(e, TAG + ": Failed to perform delete all on AppInstall table.");
        } finally {
            db.endTransaction();
        }
        return success;
    }

    private byte[] convertAppInstallFromMapToBytes(Map<String, Long> appInstallMap) {
        FlatBufferBuilder builder = new FlatBufferBuilder();
        ArrayList<Integer> entryOffsets = new ArrayList<>();
        int offset = 0;
        for (String packageName : appInstallMap.keySet()) {
            long updateTime = appInstallMap.get(packageName);
            offset = builder.createString(packageName);
            offset = AppInfo.createAppInfo(builder, offset, updateTime);
            entryOffsets.add(offset);
        }
        offset = AppInfoList.createAppInfoListVector(builder, Ints.toArray(entryOffsets));
        AppInfoList.startAppInfoList(builder);
        AppInfoList.addAppInfoList(builder, offset);
        offset = AppInfoList.endAppInfoList(builder);
        builder.finish(offset);
        return builder.sizedByteArray();
    }

    private Map<String, Long> convertAppInstallFromBytesToMap(byte[] blob) {
        HashMap<String, Long> appInstallMap = new HashMap<>();
        AppInfoList appInfoList = AppInfoList.getRootAsAppInfoList(ByteBuffer.wrap(blob));
        for (int i = 0; i < appInfoList.appInfoListLength(); i++) {
            AppInfo appInfo = appInfoList.appInfoList(i);
            appInstallMap.put(appInfo.name(), appInfo.updateTime());
        }
        return appInstallMap;
    }
}
