package kst4contest.view.map;

import kst4contest.locatorUtils.Location;
import kst4contest.model.AirPlaneReflectionInfo;
import kst4contest.model.Band;
import kst4contest.model.ChatMember;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.EnumMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;

/**
 * Builds immutable map snapshots from the currently visible chat members.
 *
 * The aggregation key is callSignRaw because the map shall show exactly one marker
 * per base callsign, even if the same station exists in multiple chat categories.
 */
public final class MapCallsignRawSnapshotBuilder {

    private static final Pattern TOKEN_SPLIT_PATTERN = Pattern.compile("[^A-Z0-9]+");

    public List<MapCallsignRawSnapshot> buildSnapshots(Collection<ChatMember> visibleChatMembers,
                                                       ChatMember selectedChatMember,
                                                       EnumSet<Band> selectedBands) {

        if (visibleChatMembers == null || visibleChatMembers.isEmpty()) {
            return List.of();
        }

        String selectedCallsignRaw = normalizeCallsignRaw(
                selectedChatMember == null ? null : selectedChatMember.getCallSignRaw()
        );

        Map<String, List<ChatMember>> groupedByCallsignRaw = new LinkedHashMap<>();

        for (ChatMember chatMember : visibleChatMembers) {
            if (chatMember == null) {
                continue;
            }

            String callSignRaw = normalizeCallsignRaw(chatMember.getCallSignRaw());
            if (callSignRaw.isBlank()) {
                continue;
            }

            groupedByCallsignRaw
                    .computeIfAbsent(callSignRaw, ignored -> new ArrayList<>())
                    .add(chatMember);
        }

        List<MapCallsignRawSnapshot> snapshots = new ArrayList<>(groupedByCallsignRaw.size());

        for (Map.Entry<String, List<ChatMember>> entry : groupedByCallsignRaw.entrySet()) {
            String callSignRaw = entry.getKey();
            List<ChatMember> variants = entry.getValue();

            ChatMember representative = chooseRepresentative(variants);
            if (representative == null) {
                continue;
            }

            String locator6 = findBestLocator6(variants);
            if (locator6.isBlank()) {
                continue;
            }

            Location location = new Location(locator6);

            LinkedHashMap<String, String> frequenciesByBand = collectLastKnownFrequenciesByBand(variants);
            EnumSet<Band> sureBands = collectSureBands(variants, frequenciesByBand);
            String bandSummary = buildBandSummary(sureBands);
            boolean offersSelectedBand = hasAnySelectedBand(sureBands, selectedBands);

            boolean warningToMyDirection = variants.stream().anyMatch(ChatMember::isInAngleAndRange);
            boolean worked = variants.stream().anyMatch(this::isWorkedAtAnyBand);
            boolean selected = callSignRaw.equals(selectedCallsignRaw);

            double qrbKm = representative.getQrb() != null ? representative.getQrb() : 0.0;
            double qtfDeg = representative.getQTFdirection() != null ? representative.getQTFdirection() : 0.0;

            int reachableAirplanes = variants.stream()
                    .map(ChatMember::getAirPlaneReflectInfo)
                    .mapToInt(this::extractReachableAirplanes)
                    .max()
                    .orElse(0);

            snapshots.add(new MapCallsignRawSnapshot(
                    callSignRaw,
                    bestDisplayCallsign(variants, representative),
                    locator6,
                    location.getLatitude().toDegrees(),
                    location.getLongitude().toDegrees(),
                    bandSummary,
                    frequenciesByBand,
                    offersSelectedBand,
                    warningToMyDirection,
                    worked,
                    selected,
                    qrbKm,
                    qtfDeg,
                    reachableAirplanes,
                    representative.getActivityTimeLastInEpoch()
            ));
        }

        snapshots.sort(Comparator.comparing(
                MapCallsignRawSnapshot::displayCallSign,
                String.CASE_INSENSITIVE_ORDER
        ));

        return List.copyOf(snapshots);
    }

    private ChatMember chooseRepresentative(List<ChatMember> variants) {
        return variants.stream()
                .filter(chatMember -> chatMember != null && normalizeLocator6(chatMember.getQra()).length() == 6)
                .max(Comparator.comparingLong(ChatMember::getActivityTimeLastInEpoch))
                .orElseGet(() -> variants.stream()
                        .filter(chatMember -> chatMember != null)
                        .max(Comparator.comparingLong(ChatMember::getActivityTimeLastInEpoch))
                        .orElse(null));
    }

    private String bestDisplayCallsign(List<ChatMember> variants, ChatMember representative) {
        if (representative != null && representative.getCallSign() != null && !representative.getCallSign().isBlank()) {
            return representative.getCallSign().trim().toUpperCase(Locale.ROOT);
        }

        for (ChatMember variant : variants) {
            if (variant != null && variant.getCallSign() != null && !variant.getCallSign().isBlank()) {
                return variant.getCallSign().trim().toUpperCase(Locale.ROOT);
            }
        }

        return representative == null ? "" : normalizeCallsignRaw(representative.getCallSignRaw());
    }

    private String findBestLocator6(List<ChatMember> variants) {
        return variants.stream()
                .sorted(Comparator.comparingLong(ChatMember::getActivityTimeLastInEpoch).reversed())
                .map(ChatMember::getQra)
                .map(this::normalizeLocator6)
                .filter(locator -> locator.length() == 6)
                .findFirst()
                .orElse("");
    }

    private LinkedHashMap<String, String> collectLastKnownFrequenciesByBand(List<ChatMember> variants) {

        Map<Band, FrequencyCandidate> latestByBand = new EnumMap<>(Band.class);

        for (ChatMember variant : variants) {
            if (variant == null) {
                continue;
            }

            for (Map.Entry<Band, ChatMember.ActiveFrequencyInfo> bandEntry : variant.getKnownActiveBands().entrySet()) {
                Band band = bandEntry.getKey();
                ChatMember.ActiveFrequencyInfo activeFrequencyInfo = bandEntry.getValue();

                if (band == null || activeFrequencyInfo == null) {
                    continue;
                }

                FrequencyCandidate previous = latestByBand.get(band);
                if (previous == null || activeFrequencyInfo.timestampEpoch > previous.timestampEpochMs()) {
                    latestByBand.put(band, new FrequencyCandidate(
                            band,
                            formatFrequency(activeFrequencyInfo.frequency),
                            activeFrequencyInfo.timestampEpoch
                    ));
                }
            }

            addFallbackCurrentFrequencyIfUseful(variant, latestByBand);
        }

        LinkedHashMap<String, String> ordered = new LinkedHashMap<>();
        latestByBand.entrySet().stream()
                .sorted(Map.Entry.comparingByKey())
                .forEach(entry -> ordered.put(toBandDisplayLabel(entry.getKey()), entry.getValue().formattedFrequency()));

        return ordered;
    }

    private EnumSet<Band> collectSureBands(List<ChatMember> variants,
                                           LinkedHashMap<String, String> frequenciesByBand) {
        EnumSet<Band> sureBands = EnumSet.noneOf(Band.class);

        if (frequenciesByBand != null) {
            for (String label : frequenciesByBand.keySet()) {
                Band mappedBand = bandFromDisplayLabel(label);
                if (mappedBand != null) {
                    sureBands.add(mappedBand);
                }
            }
        }

        for (ChatMember variant : variants) {
            if (variant == null || variant.getName() == null || variant.getName().isBlank()) {
                continue;
            }
            sureBands.addAll(detectBandsFromStationName(variant.getName()));
        }

        return sureBands;
    }

    private EnumSet<Band> detectBandsFromStationName(String stationName) {
        EnumSet<Band> detectedBands = EnumSet.noneOf(Band.class);
        if (stationName == null || stationName.isBlank()) {
            return detectedBands;
        }

        String normalized = stationName.toUpperCase(Locale.ROOT);
        String[] tokens = TOKEN_SPLIT_PATTERN.split(normalized);
        for (String token : tokens) {
            if (token == null || token.isBlank()) {
                continue;
            }

            switch (token) {
                case "2", "2M", "144", "144MHZ" -> detectedBands.add(Band.B_144);
                case "70", "70CM", "432", "432MHZ" -> detectedBands.add(Band.B_432);
                case "23", "23CM", "1296", "1296MHZ" -> detectedBands.add(Band.B_1296);
                case "13", "13CM", "2300", "2320", "2320MHZ" -> detectedBands.add(Band.B_2320);
                case "9", "9CM", "3400", "3400MHZ" -> detectedBands.add(Band.B_3400);
                case "6", "6CM", "5600", "5760", "5760MHZ" -> detectedBands.add(Band.B_5760);
                case "3", "3CM", "10G", "10GHZ", "10368", "10368MHZ" -> detectedBands.add(Band.B_10G);
                case "24G", "24GHZ", "24048", "24048MHZ" -> detectedBands.add(Band.B_24G);
                default -> {
                }
            }
        }

        return detectedBands;
    }

    private String buildBandSummary(EnumSet<Band> sureBands) {
        if (sureBands == null || sureBands.isEmpty()) {
            return "";
        }

        List<String> labels = sureBands.stream()
                .sorted()
                .map(this::toBandDisplayLabel)
                .toList();
        return String.join(", ", labels);
    }

    private boolean hasAnySelectedBand(EnumSet<Band> sureBands, EnumSet<Band> selectedBands) {
        if (sureBands == null || sureBands.isEmpty() || selectedBands == null || selectedBands.isEmpty()) {
            return false;
        }
        for (Band band : selectedBands) {
            if (sureBands.contains(band)) {
                return true;
            }
        }
        return false;
    }

    private Band bandFromDisplayLabel(String label) {
        if (label == null || label.isBlank()) {
            return null;
        }

        return switch (label) {
            case "144" -> Band.B_144;
            case "432" -> Band.B_432;
            case "1296" -> Band.B_1296;
            case "2320" -> Band.B_2320;
            case "3400" -> Band.B_3400;
            case "5760" -> Band.B_5760;
            case "10368" -> Band.B_10G;
            case "24048" -> Band.B_24G;
            default -> null;
        };
    }

    /**
     * Fallback for stations where the current displayed QRG exists but the
     * knownActiveBands history has not yet been filled.
     *
     * This parsing is intentionally tolerant so strings like "144.300 MHz"
     * can still be used.
     */
    private void addFallbackCurrentFrequencyIfUseful(ChatMember variant, Map<Band, FrequencyCandidate> latestByBand) {
        if (variant == null || variant.getFrequency() == null || variant.getFrequency().getValue() == null) {
            return;
        }

        String rawFrequency = variant.getFrequency().getValue().trim();
        if (rawFrequency.isBlank()) {
            return;
        }

        double parsedFrequencyMHz = PathGeometryUtils.tryParseFrequencyMHz(rawFrequency);
        if (!Double.isFinite(parsedFrequencyMHz) || parsedFrequencyMHz <= 0.0) {
            return;
        }

        Band detectedBand = Band.fromFrequency(parsedFrequencyMHz);
        if (detectedBand == null) {
            return;
        }

        FrequencyCandidate previous = latestByBand.get(detectedBand);
        if (previous != null && previous.timestampEpochMs() >= variant.getActivityTimeLastInEpoch()) {
            return;
        }

        latestByBand.put(detectedBand, new FrequencyCandidate(
                detectedBand,
                formatFrequency(parsedFrequencyMHz),
                variant.getActivityTimeLastInEpoch()
        ));
    }

    private boolean isWorkedAtAnyBand(ChatMember member) {
        return member.isWorked()
                || member.isWorked50()
                || member.isWorked70()
                || member.isWorked144()
                || member.isWorked432()
                || member.isWorked1240()
                || member.isWorked2300()
                || member.isWorked3400()
                || member.isWorked5600()
                || member.isWorked10G()
                || member.isWorked24G()
                || member.isWorked47G()
                || member.isWorked76G();
    }

    private int extractReachableAirplanes(AirPlaneReflectionInfo airPlaneReflectionInfo) {
        return airPlaneReflectionInfo == null ? 0 : airPlaneReflectionInfo.getAirPlanesReachableCntr();
    }

    private String normalizeCallsignRaw(String callSignRaw) {
        if (callSignRaw == null) {
            return "";
        }
        return callSignRaw.trim().toUpperCase(Locale.ROOT);
    }

    private String normalizeLocator6(String locator) {
        if (locator == null) {
            return "";
        }

        String normalized = locator.trim().toUpperCase(Locale.ROOT);
        if (normalized.length() >= 6) {
            normalized = normalized.substring(0, 6);
        }

        if (!normalized.matches("^[A-R]{2}[0-9]{2}[A-X]{2}$")) {
            return "";
        }

        return normalized;
    }

    private String toBandDisplayLabel(Band band) {
        return switch (band) {
            case B_144 -> "144";
            case B_432 -> "432";
            case B_1296 -> "1296";
            case B_2320 -> "2320";
            case B_3400 -> "3400";
            case B_5760 -> "5760";
            case B_10G -> "10368";
            case B_24G -> "24048";
        };
    }

    private String formatFrequency(double frequencyMHz) {
        return String.format(Locale.US, "%.3f", frequencyMHz);
    }

    private record FrequencyCandidate(Band band, String formattedFrequency, long timestampEpochMs) {
    }
}