package com.trunksbomb.minetriad.game;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

public final class TriadMatch {
    private final MatchParticipant firstParticipant;
    private final MatchParticipant secondParticipant;
    private final TriadRuleSet ruleSet;
    private final List<GameCard> firstHand;
    private final List<GameCard> secondHand;
    private final PlacedCard[] board;
    private MatchParticipant activeParticipant;

    public TriadMatch(
            MatchParticipant firstParticipant,
            MatchParticipant secondParticipant,
            List<GameCard> firstHand,
            List<GameCard> secondHand,
            TriadRuleSet ruleSet) {
        this.firstParticipant = firstParticipant;
        this.secondParticipant = secondParticipant;
        this.ruleSet = ruleSet;
        this.firstHand = validateHand(firstHand);
        this.secondHand = validateHand(secondHand);
        this.board = new PlacedCard[BoardCell.SIZE * BoardCell.SIZE];
        this.activeParticipant = firstParticipant;
    }

    public MatchParticipant activeParticipant() {
        return activeParticipant;
    }

    public boolean isComplete() {
        return Arrays.stream(board).noneMatch(card -> card == null);
    }

    public int scoreFor(MatchParticipant participant) {
        int boardScore = (int) Arrays.stream(board)
                .filter(card -> card != null && card.owner().equals(participant))
                .count();
        int handScore = handFor(participant).size();
        return boardScore + handScore;
    }

    public List<GameCard> handFor(MatchParticipant participant) {
        if (participant.equals(firstParticipant)) {
            return List.copyOf(firstHand);
        }
        if (participant.equals(secondParticipant)) {
            return List.copyOf(secondHand);
        }
        throw new IllegalArgumentException("Participant is not part of this match");
    }

    public PlacedCard cardAt(BoardCell cell) {
        return board[cell.index()];
    }

    public MoveResult play(TriadMove move) {
        if (!activeParticipant.equals(move.participant())) {
            return MoveResult.failure("It is not that participant's turn");
        }
        if (cardAt(move.targetCell()) != null) {
            return MoveResult.failure("That board cell is already occupied");
        }

        List<GameCard> hand = mutableHandFor(move.participant());
        if (move.handIndex() < 0 || move.handIndex() >= hand.size()) {
            return MoveResult.failure("Hand index is out of bounds");
        }

        GameCard playedCard = hand.remove(move.handIndex());
        board[move.targetCell().index()] = new PlacedCard(move.participant(), playedCard);

        List<String> battleLog = new ArrayList<>();
        CaptureResolution captureResolution = resolveCaptures(move.targetCell(), move.participant(), playedCard, battleLog);
        activeParticipant = otherParticipant(move.participant());
        return MoveResult.success(captureResolution.captureWaves(), battleLog, playedCard.definition().name().getString(), playedCard.definition().id(), move.targetCell());
    }

    private CaptureResolution resolveCaptures(BoardCell origin, MatchParticipant owner, GameCard playedCard, List<String> battleLog) {
        List<SideComparison> comparisons = buildComparisons(origin, owner, playedCard, battleLog);
        LinkedHashSet<BoardCell> firstWave = new LinkedHashSet<>();
        LinkedHashSet<BoardCell> comboSeeds = new LinkedHashSet<>();

        for (SideComparison comparison : comparisons) {
            if (!comparison.neighbor().owner().equals(owner) && comparison.attackValue() > comparison.defendValue()) {
                firstWave.add(comparison.cell());
            }
        }

        if (ruleSet.sameRule()) {
            List<SameCandidate> sameCandidates = buildSameCandidates(origin, playedCard, battleLog);
            long sameMatches = sameCandidates.stream().filter(SameCandidate::matches).count();
            if (sameMatches >= 2) {
                List<BoardCell> sameCaptures = sameCandidates.stream()
                        .filter(SameCandidate::matches)
                        .map(SameCandidate::cell)
                        .filter(cell -> cell != null)
                        .distinct()
                        .filter(cell -> {
                            PlacedCard placedCard = cardAt(cell);
                            return placedCard != null && !placedCard.owner().equals(owner);
                        })
                        .toList();
                if (!sameCaptures.isEmpty()) {
                    battleLog.add("Same triggers.");
                    firstWave.addAll(sameCaptures);
                    comboSeeds.addAll(sameCaptures);
                }
            }
        }

        if (ruleSet.plusRule()) {
            Map<Integer, List<SideComparison>> plusGroups = new LinkedHashMap<>();
            for (SideComparison comparison : comparisons) {
                int sum = comparison.attackValue() + comparison.defendValue();
                plusGroups.computeIfAbsent(sum, ignored -> new ArrayList<>()).add(comparison);
            }
            for (Map.Entry<Integer, List<SideComparison>> entry : plusGroups.entrySet()) {
                if (entry.getValue().size() < 2) {
                    continue;
                }
                List<BoardCell> plusCaptures = entry.getValue().stream()
                        .map(SideComparison::cell)
                        .distinct()
                        .filter(cell -> {
                            PlacedCard placedCard = cardAt(cell);
                            return placedCard != null && !placedCard.owner().equals(owner);
                        })
                        .toList();
                if (!plusCaptures.isEmpty()) {
                    battleLog.add("Plus triggers on sum " + entry.getKey() + ".");
                    firstWave.addAll(plusCaptures);
                    comboSeeds.addAll(plusCaptures);
                }
            }
        }

        List<List<BoardCell>> waves = new ArrayList<>();
        if (!firstWave.isEmpty()) {
            applyOwnership(owner, firstWave);
            waves.add(List.copyOf(firstWave));
        }

        if (!comboSeeds.isEmpty()) {
            LinkedHashSet<BoardCell> comboWave = new LinkedHashSet<>();
            for (BoardCell comboCell : comboSeeds) {
                PlacedCard comboCard = cardAt(comboCell);
                if (comboCard == null || !comboCard.owner().equals(owner)) {
                    continue;
                }
                for (CardSide side : CardSide.values()) {
                    BoardCell neighborCell = neighbor(comboCell, side);
                    if (neighborCell == null) {
                        continue;
                    }
                    PlacedCard neighbor = cardAt(neighborCell);
                    if (neighbor == null || neighbor.owner().equals(owner)) {
                        continue;
                    }
                    int attackValue = comboCard.card().value(side);
                    int defendValue = neighbor.card().value(side.opposite());
                    if (attackValue > defendValue) {
                        comboWave.add(neighborCell);
                    }
                }
            }
            if (!comboWave.isEmpty()) {
                battleLog.add("Combo triggers.");
                applyOwnership(owner, comboWave);
                waves.add(List.copyOf(comboWave));
            }
        }

        return new CaptureResolution(waves);
    }

    private List<SideComparison> buildComparisons(BoardCell origin, MatchParticipant owner, GameCard playedCard, List<String> battleLog) {
        List<SideComparison> comparisons = new ArrayList<>();
        for (CardSide side : CardSide.values()) {
            BoardCell neighborCell = neighbor(origin, side);
            if (neighborCell == null) {
                continue;
            }

            PlacedCard neighbor = cardAt(neighborCell);
            if (neighbor == null) {
                continue;
            }

            int attackValue = playedCard.value(side);
            int defendValue = neighbor.card().value(side.opposite());
            battleLog.add(String.format(
                    "%s %s=%d vs %s %s=%d at [%d,%d] -> %s",
                    playedCard.definition().name().getString(),
                    side.name(),
                    attackValue,
                    neighbor.card().definition().name().getString(),
                    side.opposite().name(),
                    defendValue,
                    neighborCell.row(),
                    neighborCell.column(),
                    !neighbor.owner().equals(owner) && attackValue > defendValue ? "flip" : "hold"));
            comparisons.add(new SideComparison(side, neighborCell, neighbor, attackValue, defendValue));
        }
        return comparisons;
    }

    private List<SameCandidate> buildSameCandidates(BoardCell origin, GameCard playedCard, List<String> battleLog) {
        List<SameCandidate> candidates = new ArrayList<>();
        for (CardSide side : CardSide.values()) {
            BoardCell neighborCell = neighbor(origin, side);
            if (neighborCell == null) {
                boolean wallMatch = ruleSet.sameWallRule() && playedCard.value(side) == 10;
                battleLog.add(String.format(
                        "%s %s=%d vs WALL=%d -> %s",
                        playedCard.definition().name().getString(),
                        side.name(),
                        playedCard.value(side),
                        10,
                        wallMatch ? "same" : "ignore"));
                candidates.add(new SameCandidate(side, null, wallMatch));
                continue;
            }

            PlacedCard neighbor = cardAt(neighborCell);
            if (neighbor == null) {
                continue;
            }

            boolean sameMatch = playedCard.value(side) == neighbor.card().value(side.opposite());
            candidates.add(new SameCandidate(side, neighborCell, sameMatch));
        }
        return candidates;
    }

    private void applyOwnership(MatchParticipant owner, Set<BoardCell> capturedCells) {
        for (BoardCell cell : capturedCells) {
            PlacedCard placedCard = cardAt(cell);
            if (placedCard != null) {
                board[cell.index()] = new PlacedCard(owner, placedCard.card());
            }
        }
    }

    private BoardCell neighbor(BoardCell cell, CardSide side) {
        int row = cell.row() + side.rowOffset();
        int column = cell.column() + side.columnOffset();
        if (row < 0 || row >= BoardCell.SIZE || column < 0 || column >= BoardCell.SIZE) {
            return null;
        }
        return new BoardCell(row, column);
    }

    private List<GameCard> mutableHandFor(MatchParticipant participant) {
        if (participant.equals(firstParticipant)) {
            return firstHand;
        }
        if (participant.equals(secondParticipant)) {
            return secondHand;
        }
        throw new IllegalArgumentException("Participant is not part of this match");
    }

    private MatchParticipant otherParticipant(MatchParticipant participant) {
        if (participant.equals(firstParticipant)) {
            return secondParticipant;
        }
        if (participant.equals(secondParticipant)) {
            return firstParticipant;
        }
        throw new IllegalArgumentException("Participant is not part of this match");
    }

    private static List<GameCard> validateHand(List<GameCard> hand) {
        if (hand.isEmpty()) {
            return new ArrayList<>();
        }
        if (hand.size() > 5) {
            throw new IllegalArgumentException("Triple Triad hands cannot contain more than 5 cards");
        }
        return new ArrayList<>(hand);
    }

    public TriadRuleSet ruleSet() {
        return ruleSet;
    }

    void forcePlace(BoardCell cell, PlacedCard placedCard) {
        board[cell.index()] = placedCard;
    }

    void forceActiveParticipant(MatchParticipant participant) {
        this.activeParticipant = participant;
    }

    private record SideComparison(CardSide side, BoardCell cell, PlacedCard neighbor, int attackValue, int defendValue) {
    }

    private record SameCandidate(CardSide side, BoardCell cell, boolean matches) {
    }

    private record CaptureResolution(List<List<BoardCell>> captureWaves) {
    }
}
