
package com.formkiq.equifax.service;

import static com.formkiq.equifax.service.EquifaxCreditCheckRequestType.CACHED;
import static com.formkiq.equifax.service.EquifaxCreditCheckRequestType.ERROR;
import static com.formkiq.equifax.service.EquifaxCreditCheckRequestType.NEW;
import static com.formkiq.forms.FormFieldMapper.extractLabelAndValue;
import static com.formkiq.forms.FormFinder.findValueByKey;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

import javax.persistence.EntityManager;
import javax.persistence.Query;

import org.apache.commons.lang3.math.NumberUtils;

import com.formkiq.forms.DateService;
import com.formkiq.forms.dto.FormJSON;
import com.formkiq.forms.dto.FormJSONField;

/**
 * Equifax Services Implementation.
 *
 */
public class EquifaxServiceImpl implements EquifaxService {

    /** {@link EntityManager}. */
    private EntityManager em;

    /**
     * constructor.
     * @param entityManager {@link EntityManager}
     */
    public EquifaxServiceImpl(final EntityManager entityManager) {
        this.em = entityManager;
    }

    @Override
    public List<EquifaxCreditCheck> findAll(final Date start, final Date end) {

        String sql = "select e.request, e.response, e.request_type, "
                + " e.url, e.requester, e.inserted_date "
                + "from equifax_credit_checks e "
                + " where e.inserted_date between :start and :end"
                + " order by inserted_date desc";

        Calendar c = Calendar.getInstance();
        c.setTime(end);
        c.add(Calendar.DAY_OF_MONTH, 1);

        Query query = this.em.createNativeQuery(sql)
                .setParameter("start", DateService.removeTime(start))
                .setParameter("end", DateService.removeTime(c.getTime()));

        return transform(query);
    }

    @Override
    public EquifaxCreditCheck getCachedResponse(final String request,
            final Date date) {

        String sql = "select e.request, e.response, e.request_type, "
                + " e.url, e.requester, e.inserted_date "
                + "from equifax_credit_checks e "
                + " where e.request=:request and e.inserted_date >= :date "
                + " order by inserted_date desc";

        Query query = this.em.createNativeQuery(sql)
                .setParameter("request", request)
                .setParameter("date", date);

        List<EquifaxCreditCheck> results = transform(query);
        return !results.isEmpty() ? results.get(0) : null;
    }

    /**
     * Get Equifax Credit Check URL.
     * @param form {@link FormJSON}
     * @return {@link String}
     */
    private String getCreditCheckUrl(final FormJSON form) {
        String host = extractLabelAndValue(
                findValueByKey(form, "host").get().getValue()).getRight();
        return host + "/sts/processinquiry.asp";
    }

    /**
     * Get Request Cache.
     * @param form {@link FormJSON}
     * @return int
     */
    private int getRequestCache(final FormJSON form) {
        int cache = 0;
        Optional<FormJSONField> op = findValueByKey(form, "requestcache");
        if (op.isPresent()) {
            cache = NumberUtils.toInt(op.get().getValue(), 0);
        }

        return cache;
    }

    /**
     * Save Equifax Response.
     * @param cc {@link EquifaxCreditCheck}
     */
    private void saveResponse(final EquifaxCreditCheck cc) {

        EquifaxServiceImpl.this.em
                .createNativeQuery("insert into equifax_credit_checks"
                        + "(equifax_credit_check_id, url, request, response, "
                        + "request_type,requester, inserted_date) "
                        + "values (:id,:url,:req,:resp,:type,:requester,:date)")
                .setParameter("id", UUID.fromString(cc.getCreditcheckid()))
                .setParameter("url", cc.getUrl())
                .setParameter("req", cc.getRequest())
                .setParameter("resp", cc.getResponse())
                .setParameter("type", cc.getRequesttype().name())
                .setParameter("requester", cc.getRequester())
                .setParameter("date", new Date()).executeUpdate();
    }

    @Override
    public EquifaxCreditCheck sendCreditCheck(final FormJSON form,
            final String request, final String requestor) {

        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.DAY_OF_MONTH, -1 * getRequestCache(form));

        EquifaxCreditCheck resp = getCachedResponse(request,
                calendar.getTime());

        if (resp == null) {

            EquifaxCreditCheckRequestType status = NEW;
            String url = getCreditCheckUrl(form);

            resp = new EquifaxCreditCheck();
            resp.setRequest(request);
            resp.setUrl(url);

            try {
                String response = sendToEquifax(url, request);
                resp.setResponse(response);
            } catch (Exception e) {
                resp.setResponse(e.getMessage());
                status = ERROR;
            }

            resp.setRequesttype(status);

        } else {
            resp.setRequesttype(CACHED);
        }

        resp.setRequester(requestor);
        resp.setRequest(request);
        resp.setInserteddate(new Date());
        resp.setCreditcheckid(UUID.randomUUID().toString());

        saveResponse(resp);

        return resp;
    }

    /**
     * Sends request to Equifax.
     * @param server {@link String}
     * @param send {@link String}
     * @return {@link String}
     * @throws IOException IOException
     */
    public String sendToEquifax(final String server, final String send)
            throws IOException {

        String params = "InputSegments=" + URLEncoder.encode(send, "UTF-8")
            + "&cmdSubmit=Submit";

        byte[] postData = params.getBytes(StandardCharsets.UTF_8);
        int postDataLength = postData.length;

        URL url = new URL(server);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setDoOutput(true);
        conn.setInstanceFollowRedirects(false);
        conn.setRequestMethod("POST");
        conn.setRequestProperty("Content-Type",
                "application/x-www-form-urlencoded");

        conn.setRequestProperty("charset", "utf-8");
        conn.setRequestProperty("Content-Length",
                Integer.toString(postDataLength));
        conn.setUseCaches(false);

        Reader in = null;
        DataOutputStream wr = null;

        try {
            wr = new DataOutputStream(conn.getOutputStream());
            wr.write(postData);

            StringBuilder sb = new StringBuilder();
            in = new BufferedReader(
                    new InputStreamReader(conn.getInputStream(), "UTF-8"));

            for (int c = in.read(); c != -1; c = in.read()) {
                sb.append((char) c);
            }

            String receive = sb.toString();

            return receive;

        } finally {

            if (in != null) {
                in.close();
            }

            if (wr != null) {
                wr.close();
            }
        }
    }

    /**
     * Transforms Database query to {@link EquifaxCreditCheck}.
     * @param vals Object[]
     * @return {@link EquifaxCreditCheck}
     */
    private EquifaxCreditCheck transform(final Object[] vals) {
        int i = 0;

        EquifaxCreditCheck response = new EquifaxCreditCheck();

        response.setRequest(vals[i++].toString());
        response.setResponse(vals[i++].toString());
        response.setRequesttype(EquifaxCreditCheckRequestType
                .valueOf(vals[i++].toString()));
        response.setUrl(vals[i++].toString());
        response.setRequester(vals[i++].toString());
        response.setInserteddate((Date) vals[i++]);

        return response;
    }

    /**
     * Transform {@link Query} to {@link List} of {@link EquifaxCreditCheck}.
     * @param query {@link Query}
     * @return {@link List} of {@link EquifaxCreditCheck}
     */
    @SuppressWarnings("unchecked")
    private List<EquifaxCreditCheck> transform(final Query query) {
        List<Object[]> responses = query.getResultList();
        List<EquifaxCreditCheck> results = new ArrayList<>(responses.size());

        for (Object[] vals : responses) {
            EquifaxCreditCheck response = transform(vals);
            results.add(response);
        }
        return results;
    }
}
