/*
 * Copyright 2015 Christian Basler
 *
 * 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 ch.dissem.bitmessage.repository;

import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.payload.Pubkey;
import ch.dissem.bitmessage.entity.payload.V3Pubkey;
import ch.dissem.bitmessage.entity.payload.V4Pubkey;
import ch.dissem.bitmessage.entity.valueobject.PrivateKey;
import ch.dissem.bitmessage.factory.Factory;
import ch.dissem.bitmessage.ports.AddressRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.sql.*;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

public class JdbcAddressRepository extends JdbcHelper implements AddressRepository {
    private static final Logger LOG = LoggerFactory.getLogger(JdbcAddressRepository.class);

    public JdbcAddressRepository(JdbcConfig config) {
        super(config);
    }

    @Override
    public BitmessageAddress findContact(byte[] ripeOrTag) {
        for (BitmessageAddress address : find("public_key is null")) {
            if (address.getVersion() > 3) {
                if (Arrays.equals(ripeOrTag, address.getTag())) return address;
            } else {
                if (Arrays.equals(ripeOrTag, address.getRipe())) return address;
            }
        }
        return null;
    }

    @Override
    public BitmessageAddress findIdentity(byte[] ripeOrTag) {
        for (BitmessageAddress address : find("private_key is not null")) {
            if (address.getVersion() > 3) {
                if (Arrays.equals(ripeOrTag, address.getTag())) return address;
            } else {
                if (Arrays.equals(ripeOrTag, address.getRipe())) return address;
            }
        }
        return null;
    }

    @Override
    public List<BitmessageAddress> getIdentities() {
        return find("private_key IS NOT NULL");
    }

    @Override
    public List<BitmessageAddress> getSubscriptions() {
        return find("subscribed = '1'");
    }

    @Override
    public List<BitmessageAddress> getContacts() {
        return find("private_key IS NULL");
    }

    private List<BitmessageAddress> find(String where) {
        List<BitmessageAddress> result = new LinkedList<>();
        try (Connection connection = config.getConnection()) {
            Statement stmt = connection.createStatement();
            ResultSet rs = stmt.executeQuery("SELECT address, alias, public_key, private_key, subscribed FROM Address WHERE " + where);
            while (rs.next()) {
                BitmessageAddress address;
                Blob privateKeyBlob = rs.getBlob("private_key");
                if (privateKeyBlob != null) {
                    PrivateKey privateKey = PrivateKey.read(privateKeyBlob.getBinaryStream());
                    address = new BitmessageAddress(privateKey);
                } else {
                    address = new BitmessageAddress(rs.getString("address"));
                    Blob publicKeyBlob = rs.getBlob("public_key");
                    if (publicKeyBlob != null) {
                        Pubkey pubkey = Factory.readPubkey(address.getVersion(), address.getStream(),
                                publicKeyBlob.getBinaryStream(), (int) publicKeyBlob.length(), false);
                        if (address.getVersion() == 4 && pubkey instanceof V3Pubkey) {
                            pubkey = new V4Pubkey((V3Pubkey) pubkey);
                        }
                        address.setPubkey(pubkey);
                    }
                }
                address.setAlias(rs.getString("alias"));
                address.setSubscribed(rs.getBoolean("subscribed"));

                result.add(address);
            }
        } catch (IOException | SQLException e) {
            LOG.error(e.getMessage(), e);
        }
        return result;
    }

    private boolean exists(BitmessageAddress address) {
        try (Connection connection = config.getConnection()) {
            Statement stmt = connection.createStatement();
            ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM Address WHERE address='" + address.getAddress() + "'");
            rs.next();
            return rs.getInt(1) > 0;
        } catch (SQLException e) {
            LOG.error(e.getMessage(), e);
        }
        return false;
    }

    @Override
    public void save(BitmessageAddress address) {
        try {
            if (exists(address)) {
                update(address);
            } else {
                insert(address);
            }
        } catch (IOException | SQLException e) {
            LOG.error(e.getMessage(), e);
        }
    }

    private void update(BitmessageAddress address) throws IOException, SQLException {
        try (Connection connection = config.getConnection()) {
            PreparedStatement ps = connection.prepareStatement(
                    "UPDATE Address SET alias=?, public_key=?, private_key=?, subscribed=? WHERE address=?");
            ps.setString(1, address.getAlias());
            writePubkey(ps, 2, address.getPubkey());
            writeBlob(ps, 3, address.getPrivateKey());
            ps.setBoolean(4, address.isSubscribed());
            ps.setString(5, address.getAddress());
            ps.executeUpdate();
        }
    }

    private void insert(BitmessageAddress address) throws IOException, SQLException {
        try (Connection connection = config.getConnection()) {
            PreparedStatement ps = connection.prepareStatement(
                    "INSERT INTO Address (address, alias, public_key, private_key, subscribed) VALUES (?, ?, ?, ?, ?)");
            ps.setString(1, address.getAddress());
            ps.setString(2, address.getAlias());
            writePubkey(ps, 3, address.getPubkey());
            writeBlob(ps, 4, address.getPrivateKey());
            ps.setBoolean(5, address.isSubscribed());
            ps.executeUpdate();
        }
    }

    protected void writePubkey(PreparedStatement ps, int parameterIndex, Pubkey data) throws SQLException, IOException {
        if (data != null) {
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            data.writeUnencrypted(out);
            ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
            ps.setBlob(parameterIndex, in);
        } else {
            ps.setBlob(parameterIndex, (Blob) null);
        }
    }

    @Override
    public void remove(BitmessageAddress address) {
        try (Connection connection = config.getConnection()) {
            Statement stmt = connection.createStatement();
            stmt.executeUpdate("DELETE FROM Address WHERE address = '" + address.getAddress() + "'");
        } catch (SQLException e) {
            LOG.error(e.getMessage(), e);
        }
    }

    @Override
    public BitmessageAddress getAddress(String address) {
        List<BitmessageAddress> result = find("address = '" + address + "'");
        if (result.size() > 0) return result.get(0);
        return null;
    }
}
