package com.openosrs.client.api;

import com.openosrs.client.core.ClientCore;
import com.openosrs.client.engine.GameEngine;
import com.openosrs.client.api.types.Path;
import com.openosrs.client.api.types.Position;
import com.openosrs.client.api.types.GameObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.PriorityQueue;
import java.util.Set;

/**
 * NavigationAPI - Provides pathfinding and navigation utilities.
 *
 * This API module handles:
 * - Path calculation and pathfinding
 * - Distance calculations
 * - Walkability checks
 * - Navigation utilities
 */
public class NavigationAPI {
    private static final Logger logger = LoggerFactory.getLogger(NavigationAPI.class);

    private final ClientCore clientCore;
    private final GameEngine gameEngine;

    public NavigationAPI(ClientCore clientCore, GameEngine gameEngine) {
        this.clientCore = clientCore;
        this.gameEngine = gameEngine;
    }

    /**
     * Calculate a path to a destination using A* pathfinding.
     */
    public Path calculatePath(Position destination) {
        Position start = clientCore.getPlayerState().getPosition();
        if (start == null || destination == null) {
            logger.warn("Cannot calculate path - invalid start or destination");
            return new Path(new Position[0]);
        }

        if (start.equals(destination)) {
            logger.debug("Already at destination");
            return new Path(new Position[]{start});
        }

        logger.debug("Calculating path from {} to {}", start, destination);

        try {
            List<Position> pathList = aStar(start, destination);
            return new Path(pathList.toArray(new Position[0]));
        } catch (Exception e) {
            logger.error("Error calculating path", e);
            return new Path(new Position[0]);
        }
    }

    /**
     * Check if a position is reachable.
     */
    public boolean isReachable(Position position) {
        if (position == null) {
            return false;
        }

        Position playerPos = clientCore.getPlayerState().getPosition();
        if (playerPos == null) {
            return false;
        }

        // If it's the same position, it's reachable
        if (playerPos.equals(position)) {
            return true;
        }

        // Check if the destination is walkable
        if (!isWalkable(position)) {
            return false;
        }

        // For now, assume positions within reasonable distance are reachable
        // TODO: Implement proper reachability analysis
        double distance = getDistanceTo(position);
        return distance <= 100; // Arbitrary limit for now
    }

    /**
     * Get the distance to a position.
     */
    public double getDistanceTo(Position position) {
        Position playerPos = clientCore.getPlayerState().getPosition();
        if (playerPos == null || position == null) {
            return Double.MAX_VALUE;
        }

        return playerPos.distanceTo(position);
    }

    /**
     * Check if a tile is walkable.
     */
    public boolean isWalkable(Position position) {
        if (position == null) {
            return false;
        }

        // TODO: Implement proper walkability checking
        // This would need to check:
        // - Terrain collision data
        // - Object blocking
        // - NPC blocking
        // - Player blocking
        // - Water/lava/etc.

        // For now, basic checks
        if (position.getX() < 0 || position.getY() < 0) {
            return false;
        }

        // Check if there are blocking objects at this position
        List<GameObject> objectsAtPos = clientCore.getWorldState().getGameObjects().stream()
                .filter(obj -> obj.getPosition().equals(position))
                .toList();

        for (GameObject obj : objectsAtPos) {
            // TODO: Check if object is blocking
            // For now, assume some object types are blocking
            if (obj.getName() != null &&
                (obj.getName().toLowerCase().contains("wall") ||
                 obj.getName().toLowerCase().contains("door") ||
                 obj.getName().toLowerCase().contains("gate"))) {
                return false;
            }
        }

        return true;
    }

    /**
     * Get the closest walkable position to a target.
     */
    public Position getClosestWalkablePosition(Position target) {
        if (target == null) {
            return null;
        }

        if (isWalkable(target)) {
            return target;
        }

        // Search in expanding circles for a walkable position
        for (int radius = 1; radius <= 5; radius++) {
            for (int dx = -radius; dx <= radius; dx++) {
                for (int dy = -radius; dy <= radius; dy++) {
                    if (Math.abs(dx) == radius || Math.abs(dy) == radius) {
                        Position candidate = target.offset(dx, dy);
                        if (isWalkable(candidate)) {
                            return candidate;
                        }
                    }
                }
            }
        }

        logger.warn("No walkable position found near {}", target);
        return null;
    }

    /**
     * Get all adjacent walkable positions.
     */
    public List<Position> getAdjacentWalkablePositions(Position center) {
        List<Position> adjacent = new ArrayList<>();

        if (center == null) {
            return adjacent;
        }

        int[] dx = {-1, -1, -1, 0, 0, 1, 1, 1};
        int[] dy = {-1, 0, 1, -1, 1, -1, 0, 1};

        for (int i = 0; i < dx.length; i++) {
            Position pos = center.offset(dx[i], dy[i]);
            if (isWalkable(pos)) {
                adjacent.add(pos);
            }
        }

        return adjacent;
    }

    /**
     * Simple A* pathfinding implementation.
     */
    private List<Position> aStar(Position start, Position goal) {
        PriorityQueue<Node> openSet = new PriorityQueue<>();
        Set<Position> closedSet = new HashSet<>();

        Node startNode = new Node(start, null, 0, heuristic(start, goal));
        openSet.offer(startNode);

        int maxIterations = 1000; // Prevent infinite loops
        int iterations = 0;

        while (!openSet.isEmpty() && iterations < maxIterations) {
            iterations++;

            Node current = openSet.poll();

            if (current.position.equals(goal)) {
                // Reconstruct path
                List<Position> path = new ArrayList<>();
                Node node = current;
                while (node != null) {
                    path.add(0, node.position);
                    node = node.parent;
                }
                logger.debug("Path found with {} steps in {} iterations", path.size(), iterations);
                return path;
            }

            closedSet.add(current.position);

            // Check all adjacent positions
            for (Position adjacent : getAdjacentWalkablePositions(current.position)) {
                if (closedSet.contains(adjacent)) {
                    continue;
                }

                double gScore = current.gScore + current.position.distanceTo(adjacent);
                double fScore = gScore + heuristic(adjacent, goal);

                // Check if this path to adjacent is better
                boolean inOpenSet = openSet.stream().anyMatch(n -> n.position.equals(adjacent));
                if (!inOpenSet) {
                    Node adjacentNode = new Node(adjacent, current, gScore, fScore);
                    openSet.offer(adjacentNode);
                }
            }
        }

        logger.warn("No path found from {} to {} after {} iterations", start, goal, iterations);
        return new ArrayList<>();
    }

    /**
     * Heuristic function for A* (Manhattan distance).
     */
    private double heuristic(Position a, Position b) {
        return Math.abs(a.getX() - b.getX()) + Math.abs(a.getY() - b.getY());
    }

    /**
     * Check if two positions are adjacent (within 1 tile).
     */
    public boolean isAdjacent(Position pos1, Position pos2) {
        if (pos1 == null || pos2 == null) {
            return false;
        }

        if (pos1.getPlane() != pos2.getPlane()) {
            return false;
        }

        int dx = Math.abs(pos1.getX() - pos2.getX());
        int dy = Math.abs(pos1.getY() - pos2.getY());

        return dx <= 1 && dy <= 1 && (dx + dy) > 0;
    }

    /**
     * Check if a position is within interaction range.
     */
    public boolean isInInteractionRange(Position target) {
        Position playerPos = clientCore.getPlayerState().getPosition();
        if (playerPos == null || target == null) {
            return false;
        }

        double distance = playerPos.distanceTo(target);
        return distance <= 1.5; // Standard interaction range
    }

    /**
     * Get a random walkable position within a radius.
     */
    public Position getRandomWalkablePosition(Position center, int radius) {
        if (center == null || radius <= 0) {
            return null;
        }

        List<Position> candidates = new ArrayList<>();

        for (int dx = -radius; dx <= radius; dx++) {
            for (int dy = -radius; dy <= radius; dy++) {
                Position candidate = center.offset(dx, dy);
                if (isWalkable(candidate) && center.distanceTo(candidate) <= radius) {
                    candidates.add(candidate);
                }
            }
        }

        if (candidates.isEmpty()) {
            return null;
        }

        return candidates.get((int) (Math.random() * candidates.size()));
    }

    /**
     * Node class for A* pathfinding.
     */
    private static class Node implements Comparable<Node> {
        final Position position;
        final Node parent;
        final double gScore; // Cost from start
        final double fScore; // gScore + heuristic

        Node(Position position, Node parent, double gScore, double fScore) {
            this.position = position;
            this.parent = parent;
            this.gScore = gScore;
            this.fScore = fScore;
        }

        @Override
        public int compareTo(Node other) {
            return Double.compare(this.fScore, other.fScore);
        }
    }
}
