/*
 * Copyright (C) 2017 Schürmann & Breitmoser GbR
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.sufficientlysecure.keychain.remote;


import java.security.AccessControlException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import androidx.annotation.NonNull;
import android.widget.Toast;

import org.sufficientlysecure.keychain.BuildConfig;
import org.sufficientlysecure.keychain.daos.ApiAppDao;
import org.sufficientlysecure.keychain.daos.DatabaseNotifyManager;
import org.sufficientlysecure.keychain.daos.UserIdDao;
import org.sufficientlysecure.keychain.model.UserPacket.UidStatus;
import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing.VerificationStatus;
import org.sufficientlysecure.keychain.provider.KeychainExternalContract;
import org.sufficientlysecure.keychain.provider.KeychainExternalContract.AutocryptStatus;
import org.sufficientlysecure.keychain.remote.AutocryptInteractor.AutocryptRecommendationResult;
import org.sufficientlysecure.keychain.remote.AutocryptInteractor.AutocryptState;
import timber.log.Timber;


public class KeychainExternalProvider extends ContentProvider {
    private static final int EMAIL_STATUS = 101;

    private static final int AUTOCRYPT_STATUS = 201;
    private static final int AUTOCRYPT_STATUS_INTERNAL = 202;


    private UriMatcher uriMatcher;
    private ApiPermissionHelper apiPermissionHelper;


    protected UriMatcher buildUriMatcher() {
        final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);

        String authority = KeychainExternalContract.CONTENT_AUTHORITY_EXTERNAL;

        matcher.addURI(authority, KeychainExternalContract.BASE_EMAIL_STATUS, EMAIL_STATUS);
        matcher.addURI(authority, KeychainExternalContract.BASE_AUTOCRYPT_STATUS, AUTOCRYPT_STATUS);
        matcher.addURI(authority, KeychainExternalContract.BASE_AUTOCRYPT_STATUS + "/*", AUTOCRYPT_STATUS_INTERNAL);

        return matcher;
    }

    @Override
    public boolean onCreate() {
        uriMatcher = buildUriMatcher();

        Context context = getContext();
        if (context == null) {
            throw new NullPointerException("Context can't be null during onCreate!");
        }

        apiPermissionHelper = new ApiPermissionHelper(context, ApiAppDao.getInstance(getContext()));
        return true;
    }

    @Override
    public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs,
                        String sortOrder) {
        Timber.v("query(uri=" + uri + ", proj=" + Arrays.toString(projection) + ")");
        Context context = getContext();
        if (context == null) {
            throw new IllegalStateException();
        }

        String callingPackageName = apiPermissionHelper.getCurrentCallingPackage();

        int match = uriMatcher.match(uri);
        switch (match) {
            case EMAIL_STATUS: {
                Toast.makeText(context, "This API is no longer supported by OpenKeychain!", Toast.LENGTH_SHORT).show();
                return new MatrixCursor(projection);
            }

            case AUTOCRYPT_STATUS_INTERNAL:
                if (!BuildConfig.APPLICATION_ID.equals(callingPackageName)) {
                    throw new AccessControlException("This URI can only be called internally!");
                }

                // override package name to use any external
                 callingPackageName = uri.getLastPathSegment();

            case AUTOCRYPT_STATUS: {
                boolean callerIsAllowed = (match == AUTOCRYPT_STATUS_INTERNAL) || apiPermissionHelper.isAllowedIgnoreErrors();
                if (!callerIsAllowed) {
                    throw new AccessControlException("An application must register before use of KeychainExternalProvider!");
                }

                if (projection == null) {
                    throw new IllegalArgumentException("Please provide a projection!");
                }

                List<String> plist = Arrays.asList(projection);
                boolean isWildcardSelector = selectionArgs.length == 1 && selectionArgs[0].contains("%");
                boolean queriesUidResult = plist.contains(AutocryptStatus.UID_KEY_STATUS) ||
                        plist.contains(AutocryptStatus.UID_ADDRESS) ||
                        plist.contains(AutocryptStatus.UID_MASTER_KEY_ID) ||
                        plist.contains(AutocryptStatus.UID_CANDIDATES);
                boolean queriesAutocryptResult = plist.contains(AutocryptStatus.AUTOCRYPT_PEER_STATE) ||
                        plist.contains(AutocryptStatus.AUTOCRYPT_MASTER_KEY_ID) ||
                        plist.contains(AutocryptStatus.AUTOCRYPT_KEY_STATUS);
                if (isWildcardSelector && queriesAutocryptResult) {
                    throw new UnsupportedOperationException("Cannot wildcard-query autocrypt results!");
                }

                Map<String, UidStatus> uidStatuses = queriesUidResult ?
                        loadUidStatusMap(selectionArgs, isWildcardSelector) : Collections.emptyMap();
                Map<String, AutocryptRecommendationResult> autocryptStates = queriesAutocryptResult ?
                        loadAutocryptRecommendationMap(selectionArgs, callingPackageName) : Collections.emptyMap();

                MatrixCursor cursor =
                        mapResultsToProjectedMatrixCursor(projection, selectionArgs, uidStatuses, autocryptStates);

                uri = DatabaseNotifyManager.getNotifyUriAllKeys();
                cursor.setNotificationUri(context.getContentResolver(), uri);

                return cursor;
            }

            default: {
                throw new IllegalArgumentException("Unknown URI " + uri + " (" + match + ")");
            }
        }
    }

    @NonNull
    private MatrixCursor mapResultsToProjectedMatrixCursor(String[] projection, String[] selectionArgs,
            Map<String, UidStatus> uidStatuses, Map<String, AutocryptRecommendationResult> autocryptStates) {
        MatrixCursor cursor = new MatrixCursor(projection);
        for (String selectionArg : selectionArgs) {
            AutocryptRecommendationResult autocryptResult = autocryptStates.get(selectionArg);
            UidStatus uidStatus = uidStatuses.get(selectionArg);

            Object[] row = new Object[projection.length];
            for (int i = 0; i < projection.length; i++) {
                if (AutocryptStatus.ADDRESS.equals(projection[i]) || AutocryptStatus._ID.equals(projection[i])) {
                    row[i] = selectionArg;
                } else {
                    row[i] = columnNameToRowContent(projection[i], autocryptResult, uidStatus);
                }
            }
            cursor.addRow(row);
        }
        return cursor;
    }

    private Object columnNameToRowContent(
            String columnName, AutocryptRecommendationResult autocryptResult, UidStatus uidStatus) {
        switch (columnName) {
            case AutocryptStatus.UID_KEY_STATUS: {
                if (uidStatus == null) {
                    return null;
                }
                return uidStatus.keyStatus() == VerificationStatus.VERIFIED_SECRET ?
                        KeychainExternalContract.KEY_STATUS_VERIFIED :
                        KeychainExternalContract.KEY_STATUS_UNVERIFIED;
            }
            case AutocryptStatus.UID_ADDRESS:
                if (uidStatus == null) {
                    return null;
                }
                return uidStatus.user_id();

            case AutocryptStatus.UID_MASTER_KEY_ID:
                if (uidStatus == null) {
                    return null;
                }
                return uidStatus.master_key_id();

            case AutocryptStatus.UID_CANDIDATES:
                if (uidStatus == null) {
                    return null;
                }
                return uidStatus.candidates();

            case AutocryptStatus.AUTOCRYPT_PEER_STATE:
                if (autocryptResult == null) {
                    return null;
                }
                return getPeerStateValue(autocryptResult.autocryptState);

            case AutocryptStatus.AUTOCRYPT_KEY_STATUS:
                if (autocryptResult == null) {
                    return null;
                }
                return autocryptResult.isVerified ?
                        KeychainExternalContract.KEY_STATUS_VERIFIED : KeychainExternalContract.KEY_STATUS_UNVERIFIED;

            case AutocryptStatus.AUTOCRYPT_MASTER_KEY_ID:
                if (autocryptResult == null) {
                    return null;
                }
                return autocryptResult.masterKeyId;

            default:
                throw new IllegalArgumentException("Unhandled case " + columnName);
        }
    }

    private Map<String, UidStatus> loadUidStatusMap(String[] selectionArgs, boolean isWildcardSelector) {
        UserIdDao userIdDao = UserIdDao.getInstance(getContext());
        if (isWildcardSelector) {
            UidStatus uidStatus = userIdDao.getUidStatusByEmailLike(selectionArgs[0]);
            return Collections.singletonMap(selectionArgs[0], uidStatus);
        } else {
            return userIdDao.getUidStatusByEmail(selectionArgs);
        }
    }

    private Map<String, AutocryptRecommendationResult> loadAutocryptRecommendationMap(
            String[] selectionArgs, String callingPackageName) {
        AutocryptInteractor autocryptInteractor = AutocryptInteractor.getInstance(getContext(), callingPackageName);
        return autocryptInteractor.determineAutocryptRecommendations(selectionArgs);
    }

    private int getPeerStateValue(AutocryptState autocryptState) {
        switch (autocryptState) {
            case DISABLE: return AutocryptStatus.AUTOCRYPT_PEER_DISABLED;
            case DISCOURAGED_OLD: return AutocryptStatus.AUTOCRYPT_PEER_DISCOURAGED_OLD;
            case DISCOURAGED_GOSSIP: return AutocryptStatus.AUTOCRYPT_PEER_GOSSIP;
            case AVAILABLE: return AutocryptStatus.AUTOCRYPT_PEER_AVAILABLE;
            case MUTUAL: return AutocryptStatus.AUTOCRYPT_PEER_MUTUAL;
        }
        throw new IllegalStateException("Unhandled case!");
    }

    @Override
    public String getType(@NonNull Uri uri) {
        throw new UnsupportedOperationException("Unknown uri: " + uri);
    }

    @Override
    public Uri insert(@NonNull Uri uri, ContentValues values) {
        throw new UnsupportedOperationException();
    }

    @Override
    public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
        throw new UnsupportedOperationException();
    }

    @Override
    public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        throw new UnsupportedOperationException();
    }

}
