package com.atlassian.bitbucket.internal.key.ssh.dao;

import com.atlassian.activeobjects.external.ActiveObjects;
import com.atlassian.bitbucket.ao.AbstractAoDao;
import com.atlassian.bitbucket.ssh.util.KeyUtils;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageRequest;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import net.java.ao.Query;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.security.PublicKey;
import java.util.Arrays;

import static com.atlassian.bitbucket.internal.key.ssh.dao.AoSshKey.*;

public class AoSshKeyDao extends AbstractAoDao implements SshKeyDao  {

    private static final Logger log = LoggerFactory.getLogger(AoSshKeyDao.class);

    public AoSshKeyDao(ActiveObjects ao) {
        super(ao);
    }

    @Nonnull
    @Override
    public AoSshKey create(ApplicationUser user, @Nonnull String keyText, String label) {
        PublicKey publicKey = KeyUtils.getPublicKey(keyText);

        ImmutableMap.Builder<String, Object> builder = new ImmutableMap.Builder<String, Object>();
        builder.put(COLUMN_KEY_TEXT, keyText);
        builder.put(COLUMN_KEY_MD5, KeyUtils.computeMD5(publicKey.getEncoded()));
        if (user != null) {
            builder.put(COLUMN_USER_ID, user.getId());
        }
        if (label != null) {
            builder.put(COLUMN_LABEL, StringUtils.left(label, LENGTH_LABEL));
        }

        return ao.create(AoSshKey.class, builder.build());
    }

    @Override
    public void delete(@Nonnull AoSshKey key) {
        ao.delete(key);
    }

    @Override
    public void deleteAllForUser(int userId) {
        log.debug("Fetching ssh keys for user with id={}", userId);
        AoSshKey[] keys = ao.find(AoSshKey.class, newUnorderedQuery(COLUMN_USER_ID + " = ?", userId));
        log.debug("There are {} ssh keys owned by user {} to delete", keys.length, userId);
        for (AoSshKey key : keys) {
            ao.delete(key);
            log.debug("Deleted key with id={}", key.getId());
        }
    }

    @Override
    public boolean existsForAnyUser(@Nonnull PublicKey key) {
        final byte[] remoteKey = key.getEncoded();
        final String md5 = KeyUtils.computeMD5(remoteKey);

        Query query = newUnorderedQuery(COLUMN_KEY_MD5 + " = ? and " + COLUMN_USER_ID + " is not null", md5);
        return Iterables.any(Arrays.asList(ao.find(AoSshKey.class, query)), keyMatchesPublicKey(remoteKey));
    }

    @Override
    public boolean existsForUser(int userId) {
        return ao.count(AoSshKey.class, newUnorderedQuery(COLUMN_USER_ID + " = ?", userId)) > 0;
    }

    @Nonnull
    @Override
    public Page<AoSshKey> findByUser(int userId, @Nonnull PageRequest pageRequest) {
        return pageQuery(AoSshKey.class, newQuery(COLUMN_USER_ID + " = ?", userId), pageRequest);
    }

    @Override
    public AoSshKey getById(int id) {
        return ao.get(AoSshKey.class, id);
    }

    @Override
    public AoSshKey getByPublicKey(@Nonnull PublicKey key) {
        final byte[] remoteKey = key.getEncoded();
        final String md5 = KeyUtils.computeMD5(remoteKey);

        return findMatchingKey(remoteKey, newUnorderedQuery(COLUMN_KEY_MD5 + " = ?", md5));
    }

    @Override
    public AoSshKey getByUserPublicKey(@Nonnull PublicKey key) {
        final byte[] remoteKey = key.getEncoded();
        final String md5 = KeyUtils.computeMD5(remoteKey);

        return findMatchingKey(remoteKey, newUnorderedQuery(COLUMN_KEY_MD5 + " = ? and " + COLUMN_USER_ID + " is not null", md5));
    }

    private AoSshKey findMatchingKey(byte[] remoteKey, Query query) {
        return Iterables.find(Arrays.asList(ao.find(AoSshKey.class, query)), keyMatchesPublicKey(remoteKey), null);
    }

    private Predicate<AoSshKey> keyMatchesPublicKey(final byte[] remoteKey) {
        return new Predicate<AoSshKey>() {

            @Override
            public boolean apply(AoSshKey key) {
                return Arrays.equals(remoteKey, key.toPublicKey().getEncoded());
            }
        };
    }

    private Query newQuery(String clause, Object... params) {
        return Query.select().from(AoSshKey.class).where(clause, params).order(COLUMN_ID);
    }

    private Query newUnorderedQuery(String clause, Object... params) {
        return Query.select().from(AoSshKey.class).where(clause, params);
    }
}
