/*
 * Copyright (C) 2017 FormKiQ Inc.
 *
 * 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 com.formkiq.equifax.service;

import static com.formkiq.forms.FormFinder.findValueByKey;
import static org.apache.commons.lang3.StringUtils.isEmpty;

import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.tuple.Pair;
import org.beanio.UnidentifiedRecordException;

import com.formkiq.equifax.segment.CERRSegment;
import com.formkiq.equifax.segment.ScorSegment;
import com.formkiq.equifax.segment.Segment;
import com.formkiq.equifax.segment.SegmentFactory;
import com.formkiq.forms.dto.FormJSON;
import com.formkiq.forms.dto.FormJSONValidator;
import com.formkiq.forms.dto.FormJSONValidatorRuleType;

/**
 * Equifax Response Processor.
 *
 */
public class EquifaxResponseProcessor {

    /** Regex Pattern for determining Approved amount. */
    private static final Pattern UNDER_PATTERN = Pattern
            .compile("(.*)(APPROVED FOR ITEMS UNDER)(.*)");

    /** Date Formatter for adding commas. */
    private final DecimalFormat formatter = new DecimalFormat("#,###");

    /** Format Error Map. */
    private Map<String, String> formatErrors =
            new HashMap<>();

    /** Processing Error Map. */
    private Map<String, String> processingErrors =
            new HashMap<>();

    /** Validity Error Map. */
    private Map<String, String> validityErrors =
            new HashMap<>();

    /**
     * Create Validation Rules based on Equifax Response.
     * @param form {@link FormJSON}
     * @param response {@link String}
     * @return {@link Map}
     */
    public Map<String, List<FormJSONValidator>> createValidators(
            final FormJSON form, final String response) {

        createFormatErrorMap();

        createProcessingErrorMap();

        createValidityErrorMap();

        Map<String, List<FormJSONValidator>> map = new HashMap<>();
        SegmentFactory factory = new SegmentFactory();

        Pair<ScorSegment, CERRSegment> segments = findScorSegment(factory,
                response);

        ScorSegment scor = segments.getLeft();
        CERRSegment err = segments.getRight();

        if (scor != null) {

            map = processScorSegment(scor, form);

        } else if (err != null) {

            String error = getErrorCode(err);
            addValidation(map, FormJSONValidatorRuleType.STOP, error);

        } else {

            String error = "Unable to process Equifax Request. "
                    + "Please try again later.";
            addValidation(map, FormJSONValidatorRuleType.STOP, error);
        }

        return map;
    }

    /**
     * Create Format Error Map.
     */
    private void createFormatErrorMap() {

        Map<String, String> f = new HashMap<>();

        f.put("021", "Segment Count Not Equal to Segments In");
        f.put("023", "Invalid Card Alert Request Code Used in the "
                + "IDNT Segment");
        f.put("024", "Invalid Deposit Exchange Code Used in the IDNT Segment");
        f.put("026", "Member Number/Security Code Invalid (IDNT)");
        f.put("027", "Segment Count Non-Numeric");
        f.put("028", "ECOA Inquiry Type Invalid");
        f.put("029", "Type of Processing Code Invalid");
        f.put("02A", "Number of Months to Count Inquiries Invalid (IDNT)");
        f.put("02B", "Number of Months to Count Maximum Delinquency "
                + "Invalid (IDNT)");
        f.put("02C", "Invalid Segment Identifier");
        f.put("02D", "This Code is not Presently Used");
        f.put("02E", "This Code is not Presently Used");
        f.put("02F", "Flash Format Error");
        f.put("022", "Maximum Number of Segments Exceeded");
        f.put("025", "AD Segment Does Not Follow IDNT Segment");

        this.formatErrors = f;
    }

    /**
     * Create Processing Error Map.
     */
    private void createProcessingErrorMap() {

        Map<String, String> p = new HashMap<>();
        p.put("002", "No Record Found");
        p.put("008", "This Area of Records Temporarily Unavailable "
                + "- Please Try Later");
        p.put("009", "This Area of Records Temporarily Unavailable "
                + "- Please Try Later");
        p.put("00A", "Your Transaction Was Not Completed - Error Has "
                + "Occurred in Searching File");
        p.put("014", "Re-enter Your Entire Transaction - File Has Been "
                + "Changed Since You Inquired");
        p.put("015", "Insufficient Information given to Search for the "
                + "File Requested");
        p.put("01B", "Your NM Line Is Invalid");
        p.put("01C", "Your Address Line Is Invalid");
        p.put("01D", "Current and Former Address Both Out of Area");
        p.put("01E", "Error in Employment Line");
        p.put("021", "Error in Preparing Your Form - Re-Order Form - "
                + "Billing and Maintenance is OK");
        p.put("02B", "City or State Is Misspelled or Not In ACROPAC System");
        p.put("02C", "City or State Is Misspelled or Not In ACROPAC System");
        p.put("02D", "City or State Misspelled or Not in ACROPAC System");
        p.put("02E", "This Area of Records Temporarily Unavailable - "
                + "Please Try Later");
        p.put("031", "Program Error - Print Total Transaction and "
                + "Notify Automation Division");
        p.put("036", "Error in Preparing Your Form-Reorder Form Billing and "
                + "Maintenance is OK");
        p.put("037", "Error in Preparing Your Form-Reorder Form Billing and "
                + "Maintenance is OK");
        p.put("044", "Program Error on your Inquiry Notify Bureau rep and "
                + "try again later");
        p.put("045", "Program Error - Print Total Transaction and Notify "
                + "Automation Division");
        p.put("046", "City or Province is Misspelled or Not in ACROPAC System");
        p.put("047", "Unable to prepare form Print Transaction and Notify "
                + "Automation Division");
        p.put("048", "Program Error - Print Transaction and Notify Automation "
                + "Division");
        p.put("049", "Program Error - Print Transaction and Notify Automation "
                + "Division");
        p.put("04A", "Invalid Security Code Use Your Correct Code");
        p.put("04D", "File Combine not Allowed on this Record Please "
                + "Notify Your Supervisor");
        p.put("04E", "File Combine not Allowed on this Record Please "
                + "Notify Your Supervisor");
        p.put("04F", "File Combine not Allowed on this Record Please "
                + "Notify Your Supervisor");
        p.put("050", "File Combine not Allowed on this Record Please "
                + "Notify Your Supervisor");
        p.put("051", "No Record Found");
        p.put("06B", "Form Too Large too Fit in Queue Notify Systems Division");
        p.put("065", "Run Initque Due to PRINT/FWS/F250");
        p.put("067", "Exceeded Form Request Allowed for Oper-Form");
        p.put("076", "Contact Bureau Supervisor - File Exceeds Maximum Length");
        p.put("077", "Cancelled Member Number Contact Credit Bureau Sales "
                + "Department");
        p.put("078", "Security Violation Unauthorized Transaction Type");
        p.put("079", "Unable to Build Summary Output for This Record");
        p.put("07A", "Unable to Build Full File Output for This Record");
        p.put("07B", "Unable to Build Print Image Output for This Record");
        p.put("092", "City Not Part of the Equifax Automated Network");
        p.put("0BB", "Card Alert is Not Available or Had an Internal Error");
        p.put("0BD", "No Deposit Alert Exchange Record Found");

        this.processingErrors = p;
    }

    /**
     * Validity Error Map.
     */
    private void createValidityErrorMap() {

        Map<String, String> v = new HashMap<>();
        v.put("101", "Invalid Member Number");
        v.put("102", "Invalid Teleprocessing Type");
        v.put("103", "Invalid Company Code");
        v.put("104", "Invalid Input/Output Code");
        v.put("105", "Invalid Telephone Number Supplied");
        v.put("106", "Invalid Business Telephone Number Supplied");
        v.put("107", "Invalid Customer Code for Card Alert");
        v.put("1F1", "Line One Duplicate Lines");
        v.put("1F2", "Line One Unacceptable Foreign Bureau Line");
        v.put("1F3", "Line One Invalid Line Mnemonic");
        v.put("1F4", "Line One Longer Than 80 Bytes");
        v.put("201", "Invalid Last Name");
        v.put("202", "Invalid First Name");
        v.put("203", "Invalid Middle Name");
        v.put("204", "Invalid Suffix");
        v.put("205", "Invalid Spouse Name");
        v.put("2F1", "Line Two Duplicate Lines");
        v.put("2F2", "Line Two Unacceptable Foreign Bureau Line");
        v.put("2F3", "Line Two Invalid Line Mnemonic");
        v.put("2F4", "Line Two Longer Than 80 Bytes");
        v.put("401", "Invalid Subject Social Insurance Number");
        v.put("402", "Invalid Subject Birth Date");
        v.put("403", "Invalid Subject Age");
        v.put("404", "***This Code Presently Not Used***");
        v.put("405", "Invalid Spouse Birth Date");
        v.put("406", "Invalid Spouse Social Insurance Number");
        v.put("407", "Invalid Number of Dependents");
        v.put("408", "Invalid Marital Status");
        v.put("4F1", "Line Four Duplicate Lines");
        v.put("4F2", "Line Four Unacceptable Foreign Bureau Line");
        v.put("4F3", "Line Four Invalid Line Mnemonic");
        v.put("4F4", "Line Four Longer Than 80 Bytes");
        v.put("601", "Invalid Occupation");
        v.put("602", "Invalid Name of Employer");
        v.put("701", "Invalid Account Number Mnemonic Entered");
        v.put("702", "Invalid Account Number Reference Entered");
        v.put("7F1", "Line Seven Duplicate Lines");
        v.put("7F2", "Line Seven Unacceptable Foreign Bureau Line");
        v.put("7F3", "Line Seven Invalid Line Mnemonic");
        v.put("7F4", "Line Seven Longer Than 80 Bytes");
        v.put("301", "Invalid Street Number");
        v.put("302", "Invalid Street Name");
        v.put("303", "***This Code Presently Not Used***");
        v.put("304", "Invalid City Name");
        v.put("305", "Invalid Province Code");
        v.put("306", "Invalid Postal Code");
        v.put("307", "Invalid Date Since");
        v.put("308", "Invalid R-O-B Code");
        v.put("3F1", "Line Three Duplicate Lines");
        v.put("3F2", "Line Three Unacceptable Foreign Bureau Line");
        v.put("3F3", "Line Three Invalid Line Mnemonic");
        v.put("3F4", "Line Three Longer Than 80 Bytes");
        v.put("501", "Invalid Former Address Street Number");
        v.put("502", "Invalid Former Address Street Name");
        v.put("503", "***This Code Presently Not Used***");
        v.put("504", "Invalid Former Address City Name");
        v.put("505", "Invalid Former Address Province Code");
        v.put("506", "Invalid Former Address Postal Code");
        v.put("507", "Invalid Former Address Date Since");
        v.put("508", "Invalid Former Address R-O-B Code");
        v.put("5F1", "Line Five Duplicate Lines");
        v.put("5F2", "Line Five Unacceptable Foreign Bureau Line");
        v.put("5F3", "Line Five Invalid Line Mnemonic");
        v.put("5F4", "Line Five Longer Than 80 Bytes");

        this.validityErrors = v;
    }

    /**
     * Process SCOR segment and update Response.
     * @param scorSegment {@link ScorSegment}
     * @param form {@link FormJSON}
     * @return {@link String} error message
     */
    private Map<String, List<FormJSONValidator>> processScorSegment(
            final ScorSegment scorSegment, final FormJSON form) {

        final int group = 3;
        Map<String, List<FormJSONValidator>> map = new HashMap<>();

        String narrative = scorSegment.getDecisionNarrative();
        Matcher matcher = UNDER_PATTERN.matcher(narrative);

        if (matcher.matches()) {

            try {

                String s = matcher.group(group).trim();
                String[] strs = s.split("[ \\$]");

                int max = Integer.parseInt(strs[1]);
                FormJSONValidator v1 = new FormJSONValidator();
                v1.setRule(FormJSONValidatorRuleType.MAX);
                v1.setValue(strs[1]);
                v1.setMessage("Approved for a maximum of $"
                        + this.formatter.format(max));

                FormJSONValidator v2 = new FormJSONValidator();
                v2.setRule(FormJSONValidatorRuleType.MESSAGE);
                v2.setMessage("Approved for a maximum of $"
                        + this.formatter.format(max));

                // SubTotal
                // TODO remove & change to something that can be references by
                // other forms
                map.put("$st", Arrays.asList(v1));
                map.put("*", Arrays.asList(v2));

                Locale locale = new Locale("en", "US");
                NumberFormat currencyFormatter
                    = NumberFormat.getCurrencyInstance(locale);

                String money = currencyFormatter.format(max);
                if (money.endsWith(".00")) {
                    int centsIndex = money.lastIndexOf(".00");
                    if (centsIndex != -1) {
                        money = money.substring(1, centsIndex);
                    }
                }

                findValueByKey(form, "approvedamount").get().setValue(money);

            } catch (NumberFormatException e) {

                addValidation(map, FormJSONValidatorRuleType.STOP,
                        "Unable to process Equifax Request. "
                                + "Please try again later.");
            }

        } else {

            addValidation(map, FormJSONValidatorRuleType.STOP,
                    "Application is not approved");
        }

        return map;
    }

    /**
     * Add Stop Workflow Validation.
     * @param map {@link Map}
     * @param rule {@link FormJSONValidatorRuleType}
     * @param message {@link String}
     */
    private void addValidation(final Map<String, List<FormJSONValidator>> map,
            final FormJSONValidatorRuleType rule, final String message) {
        FormJSONValidator v = new FormJSONValidator();
        v.setRule(rule);
        v.setMessage(message);
        map.put("*", Arrays.asList(v));
    }

    /**
     * Find the Scor Segment.
     * @param factory {@link SegmentFactory}
     * @param response {@link String}
     * @return {@link Pair}
     */
    private Pair<ScorSegment, CERRSegment> findScorSegment(
            final SegmentFactory factory, final String response) {

        CERRSegment err = null;
        ScorSegment scor = null;

        try {

            List<Segment> segments = factory.readSegments(response);
            scor = (ScorSegment) findSegment(segments, ScorSegment.class);
            err = (CERRSegment) findSegment(segments, CERRSegment.class);

        } catch (UnidentifiedRecordException e) {

            err = null;
            scor = findScorSegmentSecondCheck(factory, response);
        }

        return Pair.of(scor, err);
    }

    /**
     * Finds the segment.
     * @param segments List<Segment>
     * @param clazz Class
     * @return ScorSegment
     */
    private Segment findSegment(final List<Segment> segments,
            final Class<?> clazz) {

        for (Segment segment : segments) {
            if (segment.getClass().equals(clazz)) {
                return segment;
            }
        }

        return null;
    }

    /**
     * Secondary Check to find SCOR segment in string.
     * @param factory {@link SegmentFactory}
     * @param responseData {@link String}
     * @return {@link ScorSegment}
     */
    private ScorSegment findScorSegmentSecondCheck(final SegmentFactory factory,
            final String responseData) {

        ScorSegment scor = null;

        try {

            if (!isEmpty(responseData)) {

                int pos = responseData.indexOf("##SCOR");
                if (pos > -1) {

                    pos += 2;
                    String substring = responseData.substring(pos,
                            pos + ScorSegment.LENGTH);

                    List<Segment> segments = factory
                            .readSegments(substring);

                    scor = (ScorSegment) findSegment(segments,
                            ScorSegment.class);
                }
            }

        } catch (Exception e) {
            scor = null;
        }

        return scor;
    }

    /**
     * Determine error code.
     * @param err {@link CERRSegment}
     * @return {@link String}
     */
    private String getErrorCode(final CERRSegment err) {

        if (err.getNumberOfProcessingErrors() > 0) {

            String code = this.processingErrors.get(
                    err.getProcessingErrorCode1());

            if (isEmpty(code)) {
                code = "Application is not approved PE "
                        + err.getProcessingErrorCode1();
            }

            return code;
        }

        if (err.getNumberOfFormatErrors() > 0) {

            String code = this.formatErrors.get(
                    err.getFormatErrorCode1());

            if (isEmpty(code)) {
                code = "Application is not approved FE "
                        + err.getFormatErrorCode1();
            }

            return code;
        }

        if (err.getNumberOfValidityErrors() > 0) {

            String code = this.validityErrors.get(
                    err.getValidityErrorCode1());

            if (isEmpty(code)) {
                code = "Application is not approved VC "
                        + err.getValidityErrorCode1();
            }

            return code;
        }

        return "Application is not approved";
    }
}
