struct GameEngine {

    // MARK: - Primary entry point

    static func processMove(state: GameState, blockIndex: Int, direction: Direction) -> MoveResult {
        guard blockIndex >= 0 && blockIndex < state.blocks.count else {
            return MoveResult(newState: state, events: [])
        }

        let movingBlock = state.blocks[blockIndex]
        let origin = movingBlock.position

        // --- Step 1: Compute landing position ---
        let landingPos = slide(from: origin, direction: direction, state: state)

        // --- Step 2: If block didn't move, return unchanged ---
        if landingPos == origin {
            return MoveResult(newState: state, events: [])
        }

        // --- Step 3: Check cell adjacent to landing in direction ---
        let adjacentPos = landingPos.offset(by: direction)
        var events: [GameEvent] = []
        events.append(.slid(blockIndex: blockIndex, from: origin, to: landingPos))

        var newBlocks = state.blocks

        // Move the block to its landing position
        newBlocks[blockIndex] = Block(id: movingBlock.id, position: landingPos, color: movingBlock.color)

        // --- Step 4: Check for same-color collision at adjacent cell ---
        if state.grid.contains(adjacentPos),
           let adjacentIdx = state.blocks.indices.first(where: {
               $0 != blockIndex && state.blocks[$0].position == adjacentPos
           }) {
            let adjacentBlock = state.blocks[adjacentIdx]
            if adjacentBlock.color == movingBlock.color {
                // Same color → destroy both
                events.append(.destroyed(blockIndex1: blockIndex, blockIndex2: adjacentIdx, at: adjacentPos))
                // Remove by index (higher index first to keep indices stable)
                let removeIndices = [blockIndex, adjacentIdx].sorted(by: >)
                for idx in removeIndices {
                    newBlocks.remove(at: idx)
                }
            } else if state.allowsMerging,
                      let merged = GameEngine.mergeColor(movingBlock.color, adjacentBlock.color) {
                // Different primary colors → merge into a secondary color
                events.append(.merged(
                    blockIndex1: blockIndex,
                    blockIndex2: adjacentIdx,
                    resultColor: merged,
                    at: adjacentBlock.position
                ))
                // Remove both in reverse index order to avoid index shifting
                let removeIndices = [blockIndex, adjacentIdx].sorted(by: >)
                for idx in removeIndices {
                    newBlocks.remove(at: idx)
                }
                // Place merged block at the stationary block's position
                let mergedBlock = Block(id: adjacentBlock.id, position: adjacentBlock.position, color: merged)
                newBlocks.append(mergedBlock)
            }
            // Any other different-color combination: block already stopped at landingPos
        }

        let newState = state.withBlocks(newBlocks, incrementMove: true)
        return MoveResult(newState: newState, events: events)
    }

    // MARK: - Undo

    static func undo(state: GameState) -> GameState {
        return state.undoState() ?? state
    }

    // MARK: - Merge color lookup

    private static func mergeColor(_ a: BlockColor, _ b: BlockColor) -> BlockColor? {
        guard a.isPrimary && b.isPrimary && a != b else { return nil }
        let pair = Set([a, b])
        if pair == Set([BlockColor.red,  .blue])   { return .purple }
        if pair == Set([BlockColor.red,  .yellow]) { return .orange }
        if pair == Set([BlockColor.blue, .yellow]) { return .green  }
        return nil
    }

    // MARK: - Sliding algorithm

    /// Returns the furthest empty cell reachable from `from` in `direction`.
    /// Returns `from` if the very first step is blocked.
    private static func slide(from: GridPosition, direction: Direction, state: GameState) -> GridPosition {
        var current = from
        while true {
            let next = current.offset(by: direction)
            // Stop if next is out of bounds
            guard state.grid.contains(next) else { break }
            // Stop if next is a wall
            guard !state.walls.contains(next) else { break }
            // Stop if next is occupied by another block
            guard state.blocks.first(where: { $0.position == next }) == nil else { break }
            current = next
        }
        return current
    }
}
