/*
 * Copyright (C) 2017 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.keychain.internal;

import android.content.ContentValues;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;

import java.util.ArrayList;
import java.util.List;

public class GrantsDatabase {
    private static final String TAG = "KeyChain";

    private static final String DATABASE_NAME = "grants.db";
    private static final int DATABASE_VERSION = 2;
    private static final String TABLE_GRANTS = "grants";
    private static final String GRANTS_ALIAS = "alias";
    private static final String GRANTS_GRANTEE_UID = "uid";

    private static final String SELECTION_COUNT_OF_MATCHING_GRANTS =
            "SELECT COUNT(*) FROM "
                    + TABLE_GRANTS
                    + " WHERE "
                    + GRANTS_GRANTEE_UID
                    + "=? AND "
                    + GRANTS_ALIAS
                    + "=?";

    private static final String SELECTION_GRANTEE_UIDS_FOR_ALIAS =
            "SELECT " + GRANTS_GRANTEE_UID + " FROM "
                    + TABLE_GRANTS
                    + " WHERE "
                    + GRANTS_ALIAS
                    + "=?";

    private static final String SELECT_GRANTS_BY_UID_AND_ALIAS =
            GRANTS_GRANTEE_UID + "=? AND " + GRANTS_ALIAS + "=?";

    private static final String SELECTION_GRANTS_BY_UID = GRANTS_GRANTEE_UID + "=?";

    private static final String SELECTION_GRANTS_BY_ALIAS = GRANTS_ALIAS + "=?";

    private static final String TABLE_SELECTABLE = "userselectable";
    private static final String SELECTABLE_IS_SELECTABLE = "is_selectable";
    private static final String COUNT_SELECTABILITY_FOR_ALIAS =
            "SELECT COUNT(*) FROM " + TABLE_SELECTABLE + " WHERE " + GRANTS_ALIAS + "=?";

    public DatabaseHelper mDatabaseHelper;

    private class DatabaseHelper extends SQLiteOpenHelper {
        private final ExistingKeysProvider mKeysProvider;

        public DatabaseHelper(Context context, ExistingKeysProvider keysProvider) {
            super(context, DATABASE_NAME, null /* CursorFactory */, DATABASE_VERSION);
            mKeysProvider = keysProvider;
        }

        void createSelectableTable(final SQLiteDatabase db) {
            // There are some broken V1 databases that actually have the 'userselectable'
            // already created. Only create it if it does not exist.
            db.execSQL(
                    "CREATE TABLE IF NOT EXISTS "
                            + TABLE_SELECTABLE
                            + " (  "
                            + GRANTS_ALIAS
                            + " STRING NOT NULL,  "
                            + SELECTABLE_IS_SELECTABLE
                            + " STRING NOT NULL,  "
                            + "UNIQUE ("
                            + GRANTS_ALIAS
                            + "))");
        }

        @Override
        public void onCreate(final SQLiteDatabase db) {
            Log.w(TAG, "Creating new DB.");
            db.execSQL(
                    "CREATE TABLE "
                            + TABLE_GRANTS
                            + " (  "
                            + GRANTS_ALIAS
                            + " STRING NOT NULL,  "
                            + GRANTS_GRANTEE_UID
                            + " INTEGER NOT NULL,  "
                            + "UNIQUE ("
                            + GRANTS_ALIAS
                            + ","
                            + GRANTS_GRANTEE_UID
                            + "))");

            createSelectableTable(db);
            markExistingKeysAsSelectable(db);
        }

        private void markExistingKeysAsSelectable(final SQLiteDatabase db) {
            for (String alias: mKeysProvider.getExistingKeyAliases()) {
                Log.w(TAG, "Existing alias: " + alias);
                if (!hasEntryInUserSelectableTable(db, alias)) {
                    Log.w(TAG, "Marking as selectable: " + alias);
                    markKeyAsSelectable(db, alias);
                }
            }

        }

        private void markKeyAsSelectable(final SQLiteDatabase db, final String alias) {
            final ContentValues values = new ContentValues();
            values.put(GRANTS_ALIAS, alias);
            values.put(SELECTABLE_IS_SELECTABLE, Boolean.toString(true));
            db.replace(TABLE_SELECTABLE, null, values);
        }

        private boolean hasEntryInUserSelectableTable(final SQLiteDatabase db, final String alias) {
            final long numMatches =
                    DatabaseUtils.longForQuery(
                            db,
                            COUNT_SELECTABILITY_FOR_ALIAS,
                            new String[] {alias});
            return numMatches > 0;
        }

        @Override
        public void onUpgrade(final SQLiteDatabase db, int oldVersion, final int newVersion) {
            Log.w(TAG, "upgrade from version " + oldVersion + " to version " + newVersion);

            if (oldVersion == 1) {
                // Version 1 of the database does not have the 'userselectable' table, meaning
                // upgraded keys could not be selected by users.
                // The upgrade from version 1 to 2 consists of creating the 'userselectable'
                // table and adding all existing keys as user-selectable ones into that table.
                oldVersion++;
                createSelectableTable(db);
                markExistingKeysAsSelectable(db);
            }
        }
    }

    public GrantsDatabase(Context context, ExistingKeysProvider keysProvider) {
        mDatabaseHelper = new DatabaseHelper(context, keysProvider);
    }

    public void destroy() {
        mDatabaseHelper.close();
        mDatabaseHelper = null;
    }

    boolean hasGrantInternal(final SQLiteDatabase db, final int uid, final String alias) {
        final long numMatches =
                DatabaseUtils.longForQuery(
                        db,
                        SELECTION_COUNT_OF_MATCHING_GRANTS,
                        new String[] {String.valueOf(uid), alias});
        return numMatches > 0;
    }

    public boolean hasGrant(final int uid, final String alias) {
        final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
        return hasGrantInternal(db, uid, alias);
    }

    public void setGrant(final int uid, final String alias, final boolean value) {
        final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
        if (value) {
            if (!hasGrantInternal(db, uid, alias)) {
                final ContentValues values = new ContentValues();
                values.put(GRANTS_ALIAS, alias);
                values.put(GRANTS_GRANTEE_UID, uid);
                db.insert(TABLE_GRANTS, GRANTS_ALIAS, values);
            }
        } else {
            db.delete(
                    TABLE_GRANTS,
                    SELECT_GRANTS_BY_UID_AND_ALIAS,
                    new String[] {String.valueOf(uid), alias});
        }
    }

    public int[] getGrants(String alias) {
        final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
        try (Cursor cursor =
                     db.query(
                             TABLE_GRANTS,
                             new String[] {GRANTS_GRANTEE_UID},
                             SELECTION_GRANTS_BY_ALIAS,
                             new String[] {alias},
                             null /* group by */,
                             null /* having */,
                             null /* order by */)) {
            final List<Integer> result = new ArrayList<>();
            while (cursor.moveToNext()) {
                result.add(cursor.getInt(0));
            }
            return result.stream().mapToInt(Integer::intValue).toArray();
        }
    }

    public void removeAliasInformation(String alias) {
        final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
        db.delete(TABLE_GRANTS, SELECTION_GRANTS_BY_ALIAS, new String[] {alias});
        db.delete(TABLE_SELECTABLE, SELECTION_GRANTS_BY_ALIAS, new String[] {alias});
    }

    public void removeAllAliasesInformation() {
        final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
        db.delete(TABLE_GRANTS, null /* whereClause */, null /* whereArgs */);
        db.delete(TABLE_SELECTABLE, null /* whereClause */, null /* whereArgs */);
    }

    public void purgeOldGrants(PackageManager pm) {
        final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
        db.beginTransaction();
        try (Cursor cursor = db.query(
                TABLE_GRANTS,
                new String[] {GRANTS_GRANTEE_UID}, null, null, GRANTS_GRANTEE_UID, null, null)) {
            while ((cursor != null) && (cursor.moveToNext())) {
                final int uid = cursor.getInt(0);
                final boolean packageExists = pm.getPackagesForUid(uid) != null;
                if (packageExists) {
                    continue;
                }
                Log.d(TAG, String.format(
                        "deleting grants for UID %d because its package is no longer installed",
                        uid));
                db.delete(
                        TABLE_GRANTS,
                        SELECTION_GRANTS_BY_UID,
                        new String[] {Integer.toString(uid)});
            }
            db.setTransactionSuccessful();
        }

        db.endTransaction();
    }

    public void setIsUserSelectable(final String alias, final boolean userSelectable) {
        final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
        final ContentValues values = new ContentValues();
        values.put(GRANTS_ALIAS, alias);
        values.put(SELECTABLE_IS_SELECTABLE, Boolean.toString(userSelectable));

        db.replace(TABLE_SELECTABLE, null, values);
    }

    public boolean isUserSelectable(final String alias) {
        final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
        try (Cursor res =
                db.query(
                        TABLE_SELECTABLE,
                        new String[] {SELECTABLE_IS_SELECTABLE},
                        SELECTION_GRANTS_BY_ALIAS,
                        new String[] {alias},
                        null /* group by */,
                        null /* having */,
                        null /* order by */)) {
            if (res == null || !res.moveToNext()) {
                return false;
            }

            boolean isSelectable = Boolean.parseBoolean(res.getString(0));
            if (res.getCount() > 1) {
                // BUG! Should not have more than one result for any given alias.
                Log.w(TAG, String.format("Have more than one result for alias %s", alias));
            }
            return isSelectable;
        }
    }
}
