Subversion Repositories SmartDukaan

Rev

Blame | Last modification | View Log | RSS feed

package com.smartdukaan.cron.offercircular;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import com.spice.profitmandi.dao.repository.offers.CircularIngestRepository;

/**
 * Which divisions and products are in scope. Read from offers.oem_division and
 * offers.scope_rule - this is editable configuration, never constants in code, so
 * changing scope is a data change.
 *
 * A label with no division row is reported as UNKNOWN rather than silently skipped,
 * so a new OEM division in next month's circular is visible immediately.
 */
public final class ScopeConfig {

    private static final String INCLUDE_ONLY = "INCLUDE_ONLY";
    private static final String EXCLUDE = "EXCLUDE";

    private static final class Division {
        final boolean inScope;
        final int categoryId;
        Division(boolean inScope, int categoryId) {
            this.inScope = inScope;
            this.categoryId = categoryId;
        }
    }

    private static final class Rule {
        final String type;
        final Pattern pattern;
        Rule(String type, Pattern pattern) { this.type = type; this.pattern = pattern; }
    }

    /** Outcome for one row, carrying the brand/category to match products against. */
    public static final class Decision {
        private final boolean keep;
        private final String reason;
        private final String brand;
        private final int categoryId;
        private final List<Pattern> includeOnly;

        private Decision(boolean keep, String reason, String brand, int categoryId,
                         List<Pattern> includeOnly) {
            this.keep = keep;
            this.reason = reason;
            this.brand = brand;
            this.categoryId = categoryId;
            this.includeOnly = includeOnly;
        }
        public boolean isKeep() { return keep; }
        public String getReason() { return reason; }
        public String getBrand() { return brand; }
        public int getCategoryId() { return categoryId; }

        /** Drops entities that no INCLUDE_ONLY rule accepts (Apple -> iPhone only). */
        public List<String> filterEntities(List<String> entities) {
            if (includeOnly == null || includeOnly.isEmpty()) {
                return entities;
            }
            List<String> kept = new ArrayList<>();
            for (String entity : entities) {
                for (Pattern p : includeOnly) {
                    if (p.matcher(entity).find()) {
                        kept.add(entity);
                        break;
                    }
                }
            }
            return kept;
        }
    }

    private final Map<String, Division> divisions = new LinkedHashMap<>();
    private final Map<String, List<Rule>> rules = new LinkedHashMap<>();
    private final Map<String, String> brandByLabel = new LinkedHashMap<>();

    private ScopeConfig() { }

    public static ScopeConfig load(CircularIngestRepository repository) {
        ScopeConfig config = new ScopeConfig();
        for (Object[] row : repository.selectDivisions()) {
            String label = (String) row[0];
            boolean inScope = isTrue(row[1]);
            int categoryId = ((Number) row[2]).intValue();
            config.divisions.put(label, new Division(inScope, categoryId));
            String brand = row.length > 3 && row[3] != null ? (String) row[3] : "";
            if (!brand.isEmpty()) {
                config.brandByLabel.put(label, brand);
            }
        }
        for (Object[] row : repository.selectScopeRules()) {
            String label = row[0] == null ? "" : (String) row[0];
            config.rules.computeIfAbsent(label, k -> new ArrayList<>())
                    .add(new Rule((String) row[1],
                            Pattern.compile((String) row[2], Pattern.CASE_INSENSITIVE)));
        }
        return config;
    }

    /**
     * in_scope is TINYINT(1), which MySQL Connector/J maps to Boolean while Hibernate
     * may hand back a Number. Accept either rather than depend on driver settings.
     */
    private static boolean isTrue(Object value) {
        if (value instanceof Boolean) {
            return (Boolean) value;
        }
        if (value instanceof Number) {
            return ((Number) value).intValue() == 1;
        }
        return value != null && "1".equals(value.toString());
    }

    public Decision evaluate(String label, String products) {
        Division division = divisions.get(label);
        if (division == null) {
            return new Decision(false, "UNKNOWN division label: " + label, null, 0, null);
        }
        if (!division.inScope) {
            return new Decision(false, "out of scope by config: " + label, null, 0, null);
        }
        String haystack = products == null ? "" : products;
        List<Pattern> includeOnly = new ArrayList<>();
        // division-specific rules first, then global ones (label "")
        for (String scope : new String[]{label, ""}) {
            List<Rule> scoped = rules.get(scope);
            if (scoped == null) {
                continue;
            }
            for (Rule rule : scoped) {
                if (EXCLUDE.equals(rule.type) && rule.pattern.matcher(haystack).find()) {
                    return new Decision(false,
                            "EXCLUDE rule /" + rule.pattern.pattern() + "/ on "
                                    + (scope.isEmpty() ? "all" : scope), null, 0, null);
                }
            }
            List<Pattern> includes = new ArrayList<>();
            for (Rule rule : scoped) {
                if (INCLUDE_ONLY.equals(rule.type)) {
                    includes.add(rule.pattern);
                }
            }
            if (!includes.isEmpty()) {
                boolean any = false;
                for (Pattern p : includes) {
                    if (p.matcher(haystack).find()) {
                        any = true;
                        break;
                    }
                }
                if (!any) {
                    return new Decision(false, "no INCLUDE_ONLY rule matched on " + label,
                            null, 0, null);
                }
                includeOnly.addAll(includes);
            }
        }
        String brand = brandByLabel.get(label);
        return new Decision(true, null, brand == null ? label.split(" ")[0] : brand,
                division.categoryId, includeOnly);
    }
}