package com.pharmacopoeia.service;

import com.pharmacopoeia.entity.Brand;
import com.pharmacopoeia.entity.BrandRecommendRule;
import com.pharmacopoeia.repository.BrandRecommendRuleRepository;
import com.pharmacopoeia.repository.BrandRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;

@Service
public class BrandRecommendService {

    private final BrandRecommendRuleRepository ruleRepository;
    private final BrandRepository brandRepository;

    public BrandRecommendService(BrandRecommendRuleRepository ruleRepository,
                                  BrandRepository brandRepository) {
        this.ruleRepository = ruleRepository;
        this.brandRepository = brandRepository;
    }

    // ==================== field constants ====================

    private static final List<String> BRAND_CSV_COLS = List.of(
            "name", "function_indication", "usage_dosage", "contraindication",
            "ingredients", "properties", "specification", "adverse_reactions",
            "precautions", "execution_standard", "storage", "jump_url", "description", "sort_order"
    );
    private static final String BRANDS_CSV_HEADER = String.join(",", BRAND_CSV_COLS);
    private static final String RULES_CSV_HEADER = "keyword,brand_id,brand_name,tier,is_active";

    // non-name, non-sort_order columns
    private static final List<String> BRAND_TEXT_COLS = BRAND_CSV_COLS.stream()
            .filter(c -> !"name".equals(c) && !"sort_order".equals(c))
            .toList();

    // ==================== tier matching ====================

    public List<Map<String, Object>> match(List<Map<String, Object>> sources, String fullAnswer) {
        List<BrandRecommendRule> rules = ruleRepository.findByIsActiveTrueOrderByTierAscKeywordAsc();
        return rules.isEmpty() ? List.of() : doMatch(rules, sources, fullAnswer);
    }

    private List<Map<String, Object>> doMatch(List<BrandRecommendRule> rules,
                                               List<Map<String, Object>> sources, String fullAnswer) {
        Map<Integer, List<BrandRecommendRule>> byTier = rules.stream()
                .collect(Collectors.groupingBy(BrandRecommendRule::getTier, LinkedHashMap::new, Collectors.toList()));

        Set<String> texts = sources.stream()
                .map(s -> (String) s.getOrDefault("name", ""))
                .filter(n -> !n.isEmpty())
                .collect(Collectors.toCollection(HashSet::new));
        Optional.ofNullable(fullAnswer).filter(a -> !a.isEmpty()).ifPresent(texts::add);

        return byTier.values().stream()
                .map(tierRules -> findMatches(tierRules, texts))
                .filter(m -> !m.isEmpty())
                .findFirst()
                .orElse(List.of());
    }

    private List<Map<String, Object>> findMatches(List<BrandRecommendRule> rules, Set<String> targets) {
        Set<Integer> brandIds = rules.stream().map(BrandRecommendRule::getBrandId).collect(Collectors.toSet());
        Map<Integer, Brand> brandMap = brandRepository.findAllById(brandIds).stream()
                .collect(Collectors.toMap(Brand::getId, b -> b));

        Set<Integer> seenRuleIds = new HashSet<>();
        Set<Integer> seenBrandIds = new HashSet<>();
        List<Map<String, Object>> result = new ArrayList<>();
        for (BrandRecommendRule rule : rules) {
            String kwLower = rule.getKeyword().toLowerCase();
            boolean hit = targets.stream().anyMatch(t -> t.toLowerCase().contains(kwLower));
            if (hit && seenRuleIds.add(rule.getId()) && seenBrandIds.add(rule.getBrandId())) {
                result.add(buildBrandResult(rule, brandMap.get(rule.getBrandId())));
            }
        }
        return result;
    }

    private Map<String, Object> buildBrandResult(BrandRecommendRule rule, Brand brand) {
        Map<String, Object> m = new LinkedHashMap<>();
        m.put("brand_id", rule.getBrandId());
        m.put("matched_keyword", rule.getKeyword());
        m.put("tier", rule.getTier());
        if (brand != null) {
            m.put("brand_name", brand.getName());
            m.put("description", nullToEmpty(brand.getDescription()));
            m.put("function", nullToEmpty(brand.getFunctionIndication()));
            m.put("usage_dosage", nullToEmpty(brand.getUsageDosage()));
            m.put("contraindication", nullToEmpty(brand.getContraindication()));
            m.put("ingredients", nullToEmpty(brand.getIngredients()));
            m.put("properties", nullToEmpty(brand.getProperties()));
            m.put("specification", nullToEmpty(brand.getSpecification()));
            m.put("adverse_reactions", nullToEmpty(brand.getAdverseReactions()));
            m.put("precautions", nullToEmpty(brand.getPrecautions()));
            m.put("execution_standard", nullToEmpty(brand.getExecutionStandard()));
            m.put("storage", nullToEmpty(brand.getStorage()));
            m.put("jump_url", nullToEmpty(brand.getJumpUrl()));
        }
        return m;
    }

    // ==================== brand CRUD ====================

    public Map<String, Object> listBrands(String keyword, int page, int pageSize) {
        List<Brand> all = brandRepository.findAllByOrderBySortOrderAsc();
        Optional.ofNullable(keyword)
                .filter(k -> !k.isBlank())
                .ifPresent(k -> {
                    String kw = k.toLowerCase();
                    all.removeIf(b -> !b.getName().toLowerCase().contains(kw));
                });
        return paginate(all, page, pageSize);
    }

    @Transactional
    public Map<String, Object> createBrand(Map<String, Object> body) {
        String name = require(body);
        if (brandRepository.existsByNameAndIdNot(name.trim(), null)) {
            throw new IllegalArgumentException("brand name already exists");
        }

        Brand brand = Brand.builder()
                .name(name.trim())
                .functionIndication(str(body, "function_indication"))
                .usageDosage(str(body, "usage_dosage"))
                .contraindication(str(body, "contraindication"))
                .ingredients(str(body, "ingredients"))
                .properties(str(body, "properties"))
                .specification(str(body, "specification"))
                .adverseReactions(str(body, "adverse_reactions"))
                .precautions(str(body, "precautions"))
                .executionStandard(str(body, "execution_standard"))
                .storage(str(body, "storage"))
                .jumpUrl(str(body, "jump_url"))
                .description(str(body, "description"))
                .sortOrder(intVal(body))
                .isActive(true)
                .build();
        brand = brandRepository.save(brand);
        return Map.of("ok", true, "id", brand.getId(), "message", "created");
    }

    @Transactional
    public Map<String, Object> updateBrand(int id, Map<String, Object> body) {
        Brand b = brandRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("brand not found"));

        Optional.ofNullable(body.get("name"))
                .map(Object::toString).map(String::trim).filter(n -> !n.isBlank())
                .ifPresent(n -> {
                    if (brandRepository.existsByNameAndIdNot(n, id)) {
                        throw new IllegalArgumentException("brand name already exists");
                    }
                    b.setName(n);
                });

        BRAND_TEXT_COLS.forEach(key -> applyStr(body, key, fieldSetter(b, key)));

        Optional.ofNullable(body.get("sort_order"))
                .filter(v -> v instanceof Number)
                .ifPresent(v -> b.setSortOrder(((Number) v).intValue()));
        Optional.ofNullable(body.get("is_active"))
                .filter(v -> v instanceof Boolean)
                .ifPresent(v -> b.setIsActive((Boolean) v));

        brandRepository.save(b);
        return Map.of("ok", true, "id", id, "message", "updated");
    }

    @Transactional
    public Map<String, Object> deleteBrand(int id) {
        brandRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("brand not found"));
        brandRepository.deleteById(id);
        return Map.of("ok", true, "message", "deleted");
    }

    public Map<String, Object> getBrand(int id) {
        Brand b = brandRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("brand not found"));

        Map<String, Object> m = new LinkedHashMap<>();
        m.put("id", b.getId());
        m.put("name", b.getName());
        BRAND_CSV_COLS.stream().filter(c -> !"name".equals(c))
                .forEach(c -> m.put(c, nullToEmpty(brandField(b, c))));
        m.put("is_active", b.getIsActive());
        m.put("created_at", b.getCreatedAt());
        m.put("updated_at", b.getUpdatedAt());
        return m;
    }

    // ==================== rule CRUD ====================

    public Map<String, Object> listRules(String keyword, int page, int pageSize) {
        List<BrandRecommendRule> all = ruleRepository.findAllByOrderByTierAscKeywordAsc();
        Optional.ofNullable(keyword)
                .filter(k -> !k.isBlank())
                .ifPresent(k -> all.removeIf(r -> !r.getKeyword().contains(k)));

        Set<Integer> brandIds = all.stream().map(BrandRecommendRule::getBrandId).collect(Collectors.toSet());
        Map<Integer, String> brandNames = brandRepository.findAllById(brandIds).stream()
                .collect(Collectors.toMap(Brand::getId, Brand::getName));

        Map<String, Object> result = new HashMap<>(paginate(all, page, pageSize));
        @SuppressWarnings("unchecked")
        List<BrandRecommendRule> items = (List<BrandRecommendRule>) result.get("items");
        List<Map<String, Object>> enriched = items.stream().map(r -> {
            Map<String, Object> m = new LinkedHashMap<>();
            m.put("id", r.getId());
            m.put("keyword", r.getKeyword());
            m.put("brand_id", r.getBrandId());
            m.put("brand_name", brandNames.getOrDefault(r.getBrandId(), ""));
            m.put("tier", r.getTier());
            m.put("is_active", r.getIsActive());
            m.put("created_at", r.getCreatedAt());
            m.put("updated_at", r.getUpdatedAt());
            return m;
        }).collect(Collectors.toList());
        result.put("items", enriched);
        return result;
    }

    @Transactional
    public Map<String, Object> createRule(String keyword, int brandId, int tier) {
        Optional.ofNullable(keyword).filter(k -> !k.isBlank())
                .orElseThrow(() -> new IllegalArgumentException("keyword required"));
        Optional.of(brandId).filter(brandRepository::existsById)
                .orElseThrow(() -> new IllegalArgumentException("brand not found"));
        if (ruleRepository.existsByKeywordAndIdNot(keyword.trim(), null)) {
            throw new IllegalArgumentException("keyword already exists");
        }

        BrandRecommendRule rule = BrandRecommendRule.builder()
                .keyword(keyword.trim()).brandId(brandId).tier(tier).isActive(true).build();
        rule = ruleRepository.save(rule);
        return Map.of("ok", true, "id", rule.getId(), "message", "created");
    }

    @Transactional
    public Map<String, Object> updateRule(int id, String keyword, Integer brandId, Integer tier, Boolean isActive) {
        BrandRecommendRule rule = ruleRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("rule not found"));

        Optional.ofNullable(keyword).filter(k -> !k.isBlank()).ifPresent(k -> {
            if (ruleRepository.existsByKeywordAndIdNot(k.trim(), id)) {
                throw new IllegalArgumentException("keyword already exists");
            }
            rule.setKeyword(k.trim());
        });
        Optional.ofNullable(brandId)
                .filter(brandRepository::existsById)
                .ifPresent(rule::setBrandId);
        Optional.ofNullable(tier).ifPresent(rule::setTier);
        Optional.ofNullable(isActive).ifPresent(rule::setIsActive);

        ruleRepository.save(rule);
        return Map.of("ok", true, "id", id, "message", "updated");
    }

    @Transactional
    public Map<String, Object> deleteRule(int id) {
        ruleRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("rule not found"));
        ruleRepository.deleteById(id);
        return Map.of("ok", true, "message", "deleted");
    }

    @Transactional
    public Map<String, Object> toggleRule(int id, Boolean isActive) {
        BrandRecommendRule rule = ruleRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("rule not found"));
        boolean newState = isActive != null ? isActive : !Boolean.TRUE.equals(rule.getIsActive());
        rule.setIsActive(newState);
        ruleRepository.save(rule);
        return Map.of("ok", true, "id", id, "is_active", newState,
                "message", newState ? "enabled" : "disabled");
    }

    // ==================== CSV import/export ====================

    public Map<String, Object> importBrandsCsv(String csv) {
        List<String> lines = csvLines(csv);
        if (lines.isEmpty() || !lines.getFirst().equals(BRANDS_CSV_HEADER)) {
            throw new IllegalArgumentException("CSV format incorrect: expected header " + BRANDS_CSV_HEADER);
        }

        int success = 0, failed = 0;
        for (int i = 1; i < lines.size(); i++) {
            String[] cols = lines.get(i).split(",", BRAND_CSV_COLS.size());
            try {
                String name = cols[0].trim();
                if (name.isEmpty() || brandRepository.existsByNameAndIdNot(name, null)) { failed++; continue; }
                brandRepository.save(Brand.builder()
                        .name(name)
                        .functionIndication(col(cols, 1))
                        .usageDosage(col(cols, 2))
                        .contraindication(col(cols, 3))
                        .ingredients(col(cols, 4))
                        .properties(col(cols, 5))
                        .specification(col(cols, 6))
                        .adverseReactions(col(cols, 7))
                        .precautions(col(cols, 8))
                        .executionStandard(col(cols, 9))
                        .storage(col(cols, 10))
                        .jumpUrl(col(cols, 11))
                        .description(col(cols, 12))
                        .sortOrder(cols.length > 13 ? Integer.parseInt(cols[13].trim()) : 0)
                        .isActive(true)
                        .build());
                success++;
            } catch (Exception e) { failed++; }
        }
        return Map.of("ok", true, "success", success, "failed", failed);
    }

    public String exportBrandsCsv(Set<Integer> ids) {
        List<Brand> brands = ids.isEmpty() ? List.of() : brandRepository.findAllById(ids);
        StringBuilder sb = new StringBuilder(BRANDS_CSV_HEADER).append("\n");
        List<Function<Brand, String>> extractors = List.of(
                b -> esc(b.getName()),
                b -> esc(b.getFunctionIndication()),
                b -> esc(b.getUsageDosage()),
                b -> esc(b.getContraindication()),
                b -> esc(b.getIngredients()),
                b -> esc(b.getProperties()),
                b -> esc(b.getSpecification()),
                b -> esc(b.getAdverseReactions()),
                b -> esc(b.getPrecautions()),
                b -> esc(b.getExecutionStandard()),
                b -> esc(b.getStorage()),
                b -> esc(b.getJumpUrl()),
                b -> esc(b.getDescription()),
                b -> String.valueOf(b.getSortOrder())
        );
        brands.forEach(b -> sb.append(extractors.stream()
                .map(f -> f.apply(b)).collect(Collectors.joining(","))).append("\n"));
        return sb.toString();
    }

    public Map<String, Object> importRulesCsv(String csv) {
        List<String> lines = csvLines(csv);
        if (lines.isEmpty() || !lines.getFirst().equals(RULES_CSV_HEADER)) {
            throw new IllegalArgumentException("CSV format incorrect: expected header " + RULES_CSV_HEADER);
        }

        int success = 0, failed = 0;
        for (int i = 1; i < lines.size(); i++) {
            String[] cols = lines.get(i).split(",", 5);
            try {
                String kw = cols[0].trim();
                if (kw.isEmpty()) { failed++; continue; }

                int brandId = cols.length > 1 ? parseIntOrDefault(cols[1], 0) : 0;
                String brandName = cols.length > 2 ? cols[2].trim() : "";
                Brand matched = brandId > 0 && brandRepository.existsById(brandId)
                        ? null
                        : brandRepository.findByName(brandName);
                int resolvedId = matched != null ? matched.getId() : brandId;
                if (resolvedId <= 0 || !brandRepository.existsById(resolvedId)) { failed++; continue; }

                int tier = cols.length > 3 ? parseIntOrDefault(cols[3], 1) : 1;
                if (ruleRepository.existsByKeywordAndIdNot(kw, null)) { failed++; continue; }
                ruleRepository.save(BrandRecommendRule.builder()
                        .keyword(kw).brandId(resolvedId).tier(tier).isActive(true).build());
                success++;
            } catch (Exception e) { failed++; }
        }
        return Map.of("ok", true, "success", success, "failed", failed);
    }

    public String exportRulesCsv(Set<Integer> ids) {
        List<BrandRecommendRule> rules = ids.isEmpty() ? List.of() : ruleRepository.findAllById(ids);
        Set<Integer> brandIds = rules.stream().map(BrandRecommendRule::getBrandId).collect(Collectors.toSet());
        Map<Integer, String> brandNames = brandRepository.findAllById(brandIds).stream()
                .collect(Collectors.toMap(Brand::getId, Brand::getName));
        StringBuilder sb = new StringBuilder(RULES_CSV_HEADER).append("\n");
        rules.forEach(r -> sb.append(r.getKeyword()).append(",")
                .append(r.getBrandId()).append(",")
                .append(brandNames.getOrDefault(r.getBrandId(), "")).append(",")
                .append(r.getTier()).append(",")
                .append(r.getIsActive()).append("\n"));
        return sb.toString();
    }

    public byte[] exportBrandsCsvBytes(Set<Integer> ids) {
        return exportBrandsCsv(ids).getBytes(StandardCharsets.UTF_8);
    }

    public byte[] exportRulesCsvBytes(Set<Integer> ids) {
        return exportRulesCsv(ids).getBytes(StandardCharsets.UTF_8);
    }

    // ==================== helpers ====================

    private static <T> Map<String, Object> paginate(List<T> all, int page, int pageSize) {
        int total = all.size();
        int from = (page - 1) * pageSize;
        int to = Math.min(from + pageSize, total);
        List<T> items = from < total ? all.subList(from, to) : List.of();
        return Map.of(
            "items", items,
            "page", page,
            "page_size", pageSize,
            "total", total,
            "total_pages", (int) Math.ceil((double) total / pageSize)
        );
    }

    private static List<String> csvLines(String csv) {
        return Arrays.stream(csv.replace("\r\n", "\n").replace("\r", "\n").split("\n"))
                .map(String::trim)
                .filter(s -> !s.isEmpty() && !s.startsWith("#"))
                .collect(Collectors.toList());
    }

    private static String require(Map<String, Object> body) {
        String v = str(body, "name");
        if (v == null || v.isBlank()) {
            throw new IllegalArgumentException("brand name required");
        }
        return v;
    }

    private static int parseIntOrDefault(String s, int def) {
        try { return Integer.parseInt(s.trim()); } catch (NumberFormatException e) { return def; }
    }

    private static Consumer<String> fieldSetter(Brand b, String col) {
        return switch (col) {
            case "function_indication" -> b::setFunctionIndication;
            case "usage_dosage" -> b::setUsageDosage;
            case "contraindication" -> b::setContraindication;
            case "ingredients" -> b::setIngredients;
            case "properties" -> b::setProperties;
            case "specification" -> b::setSpecification;
            case "adverse_reactions" -> b::setAdverseReactions;
            case "precautions" -> b::setPrecautions;
            case "execution_standard" -> b::setExecutionStandard;
            case "storage" -> b::setStorage;
            case "jump_url" -> b::setJumpUrl;
            case "description" -> b::setDescription;
            default -> v -> {};
        };
    }

    private static String brandField(Brand b, String col) {
        return switch (col) {
            case "function_indication" -> b.getFunctionIndication();
            case "usage_dosage" -> b.getUsageDosage();
            case "contraindication" -> b.getContraindication();
            case "ingredients" -> b.getIngredients();
            case "properties" -> b.getProperties();
            case "specification" -> b.getSpecification();
            case "adverse_reactions" -> b.getAdverseReactions();
            case "precautions" -> b.getPrecautions();
            case "execution_standard" -> b.getExecutionStandard();
            case "storage" -> b.getStorage();
            case "jump_url" -> b.getJumpUrl();
            case "description" -> b.getDescription();
            case "sort_order" -> String.valueOf(b.getSortOrder());
            default -> "";
        };
    }

    private static String str(Map<String, Object> body, String key) {
        return Optional.ofNullable(body.get(key))
                .map(Object::toString).map(String::trim)
                .filter(s -> !s.isEmpty())
                .orElse(null);
    }

    private static String nullToEmpty(String s) { return s != null ? s : ""; }

    private static void applyStr(Map<String, Object> body, String key, Consumer<String> setter) {
        Optional.ofNullable(body.get(key))
                .ifPresent(v -> {
                    String s = v.toString().trim();
                    if (!s.isEmpty()) {
                        setter.accept(s);
                    }
                });
    }

    private static String col(String[] cols, int idx) {
        return idx < cols.length ? Optional.of(cols[idx].trim()).filter(s -> !s.isEmpty()).orElse(null) : null;
    }

    private static String esc(String s) {
        return (s == null || s.isEmpty()) ? "" : "\"" + s.replace("\"", "\"\"") + "\"";
    }

    private static int intVal(Map<String, Object> body) {
        return Optional.ofNullable(body.get("sort_order"))
                .filter(v -> v instanceof Number)
                .map(v -> ((Number) v).intValue())
                .orElse(0);
    }
}
