package ru.yandex.wmtools.common.service;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.jdbc.core.simple.ParameterizedRowMapper;

import ru.yandex.wmtools.common.data.multidbpager.MultiDbPager;
import ru.yandex.wmtools.common.data.partition.IPartition;
import ru.yandex.wmtools.common.data.partition.SimplePartition;
import ru.yandex.wmtools.common.error.InternalException;
import ru.yandex.wmtools.common.error.InternalProblem;
import ru.yandex.wmtools.common.util.HttpConnector;
import ru.yandex.wmtools.common.util.IServiceJdbcTemplate;
import ru.yandex.wmtools.common.util.ServiceTransactionTemplate;

/**
 * This class, generally, is a super-class for most services.
 * It inits JDBC connection, that is needed in most of services.
 *
 * User: baton
 * @author Andrey Mima (amima@yandex-team.ru)
 */
public abstract class AbstractDbService implements IService {
    private static final Logger log = LoggerFactory.getLogger(AbstractDbService.class);

    private static final int INFINITE_TOTAL_COUNT = 1000000001;

    private JdbcConnectionWrapperService jdbcConnectionWrapperService;

    /**
     * Returns JdbcTemplate object, that operates with master DB.
     * If it is unavailable, operates with reserve DB (slave).
     *
     * @param partition Information to choose database and partition number.
     * @return Master JdbcTemplate.
     */
    public IServiceJdbcTemplate getJdbcTemplate(final IPartition partition) {
        return jdbcConnectionWrapperService.getJdbcTemplate(partition);
    }

    public IServiceJdbcTemplate getSimpleJdbcTemplate() {
        return getJdbcTemplate(new SimplePartition());
    }

    public ServiceTransactionTemplate getServiceTransactionTemplate(final IPartition partition) {
        return jdbcConnectionWrapperService.getServiceTransactionTemplate(partition);
    }

    public ServiceTransactionTemplate getSimpleServiceTransactionTemplate() {
        return jdbcConnectionWrapperService.getServiceTransactionTemplate(new SimplePartition());
    }

    public int getDatabaseCount() {
        return jdbcConnectionWrapperService.getDatabaseCount();
    }

    public <T> List<T> multiDbPageableSelect(final String countSql, final String selectSql, final ParameterizedRowMapper<T> mapper, final int dbCount, final IPartition partitionExample, final MultiDbPager<T> pager, final Object... params) throws InternalException {
        int[] dbIndex = pager.getCurrentDbIndexArray(dbCount);

        @SuppressWarnings({"unchecked"})
        List<T>[] tList = new List[dbCount];

        int totalCount = 0;
        int selectedCount = 0;
        for (int i = 0; i < dbCount; i++) {
            String formatStr = " ORDER BY " + pager.getSortStrategy().getOrderByStr() + " ";
            if (!pager.isShowAll()) {
                formatStr += " LIMIT " + dbIndex[i] + ", " + (pager.getPageSize() + 1) + " ";
            }

            String queryStr = String.format(selectSql, formatStr);
            tList[i] = getJdbcTemplate(partitionExample.getIPartitionForDb(i)).query(queryStr, mapper, params);

            selectedCount += tList[i].size();

            if (countSql != null) {
                totalCount += getJdbcTemplate(partitionExample.getIPartitionForDb(i)).queryForLong(countSql);
            }
        }

        if (countSql != null) {
            pager.setTotalCount(totalCount);
        } else {
            pager.setTotalCount(INFINITE_TOTAL_COUNT);
        }

        List<T> res = new ArrayList<T>();

        int[] selectedDbIndex = new int[dbCount];
        for (int i = 0; i < (pager.isShowAll() ? selectedCount : Math.min(pager.getPageSize(), selectedCount)); i++) {
            Integer best = null;
            for (int j = 0; j < dbCount; j++) {
                if (selectedDbIndex[j] >= tList[j].size()) {
                    continue;
                }
                T last = tList[j].get(selectedDbIndex[j]);
                if ((best == null) || (pager.getSortStrategy().getComparator().compare(tList[best].get(selectedDbIndex[best]), last) > 0)) {
                    best = j;
                }
            }
            res.add(tList[best].get(selectedDbIndex[best]));
            selectedDbIndex[best]++;
            dbIndex[best]++;
        }
        pager.setNextDbIndexArray(dbIndex);

        return res;
    }


    protected String getXmlWithoutDeclaration(String address) throws InternalException {
        log.debug("Requesting info from: '" + address + "'");
        InputStream is;
        try {
            is = new HttpConnector.RequestBuilder(new URL(address))
                    .method(HttpConnector.HttpMethod.GET)
                    .okStatusRequired(true)
                    .execute().getContent();
        } catch (IOException e) {
            throw new InternalException(InternalProblem.CONNECTION_PROBLEM, "IO exception while requesting " + address, e);
        }

        StringBuilder sb = new StringBuilder();
        byte[] read = new byte[1024];

        while (!Thread.interrupted()) {
            int bytesRead;
            try {
                bytesRead = is.read(read);
            } catch (IOException e) {
                throw new InternalException(InternalProblem.CONNECTION_PROBLEM, "IO exception while getting results from " + address, e);
            }
            if (bytesRead < 0) {
                break;
            }

            sb.append(new String(read, 0, bytesRead));
        }

        String res = sb.toString().trim();

        return res.replaceAll("^<\\?.*\\?>", "");
    }

    /**
     * Выводит в лог число активных и неактивных соединений в пуле для базы данных с заданным индексом
     * @param dbIndex индекс БД (0-8)
     */
    public void logConnections(int dbIndex) {
        jdbcConnectionWrapperService.logConnections(dbIndex);
    }

    public void logUserDbConnections() {
        logConnections(getDatabaseCount()-1);
    }

    @Required
    public void setJdbcConnectionWrapperService(JdbcConnectionWrapperService jdbcConnectionWrapperService) {
        this.jdbcConnectionWrapperService = jdbcConnectionWrapperService;
    }
}
