namespace GeoSus.Server;

using System.Collections.Concurrent;
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;

/// <summary>
/// Service for fetching and processing OpenStreetMap data via Overpass API
/// </summary>
public class OverpassService
{
    private readonly ServerConfig _config;
    private readonly ILogger _logger;
    private readonly HttpClient _httpClient;
    private readonly ConcurrentDictionary<string, MapData> _cache = new();
    
    public OverpassService(ServerConfig config, ILogger logger)
    {
        _config = config;
        _logger = logger;
        _httpClient = new HttpClient
        {
            Timeout = TimeSpan.FromSeconds(config.OverpassTimeoutSec)
        };
    }
    
    /// <summary>
    /// Fetch map data for a circular area, with caching
    /// </summary>
    public async Task<MapData?> GetMapDataAsync(Position center, double radiusMeters)
    {
        var cacheKey = $"{center.Lat:F5}_{center.Lon:F5}_{radiusMeters:F0}";
        
        if (_cache.TryGetValue(cacheKey, out var cached))
        {
            _logger.LogDebug("Map data cache hit for {CacheKey}", cacheKey);
            return cached;
        }
        
        try
        {
            var mapData = await FetchMapDataAsync(center, radiusMeters);
            if (mapData != null)
            {
                _cache[cacheKey] = mapData;
                
                // Clean old cache entries if too many
                if (_cache.Count > _config.OverpassCacheMaxEntries)
                {
                    var oldest = _cache.Keys.First();
                    _cache.TryRemove(oldest, out _);
                }
            }
            return mapData;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to fetch Overpass data for {Center}, radius {Radius}m", center, radiusMeters);
            return null;
        }
    }
    
    public async Task<MapData?> FetchMapDataAsync(Position center, double radiusMeters)
    {
        // Build Overpass QL query
        var query = BuildOverpassQuery(center, radiusMeters);
        
        _logger.LogInformation("Fetching Overpass data for center {Center}, radius {Radius}m", center, radiusMeters);
        
        var response = await _httpClient.PostAsync(
            _config.OverpassApiUrl,
            new FormUrlEncodedContent(new[] { new KeyValuePair<string, string>("data", query) })
        );
        
        if (!response.IsSuccessStatusCode)
        {
            _logger.LogError("Overpass API returned {StatusCode}: {Content}", 
                response.StatusCode, await response.Content.ReadAsStringAsync());
            return null;
        }
        
        var json = await response.Content.ReadAsStringAsync();
        var overpassResult = JsonSerializer.Deserialize<OverpassResult>(json, new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase
        });
        
        if (overpassResult == null)
        {
            _logger.LogError("Failed to parse Overpass response");
            return null;
        }
        
        return ProcessOverpassResult(overpassResult, center, radiusMeters);
    }
    
    private string BuildOverpassQuery(Position center, double radiusMeters)
    {
        // Query for: roads, paths, buildings, amenities
        // We use "around" to get data within radius of center point
        return $@"
[out:json][timeout:{_config.OverpassTimeoutSec}];
(
  // Walkable ways (roads, paths, sidewalks)
  way[""highway""~""footway|path|pedestrian|steps|track|residential|tertiary|secondary|primary|service|living_street|cycleway|bridleway""]
    (around:{radiusMeters},{center.Lat},{center.Lon});
  
  // Buildings
  way[""building""]
    (around:{radiusMeters},{center.Lat},{center.Lon});
  relation[""building""]
    (around:{radiusMeters},{center.Lat},{center.Lon});
  
  // Interesting POIs for task placement
  node[""amenity""]
    (around:{radiusMeters},{center.Lat},{center.Lon});
  way[""amenity""]
    (around:{radiusMeters},{center.Lat},{center.Lon});
  
  // Parks and green areas
  way[""leisure""~""park|playground|garden""]
    (around:{radiusMeters},{center.Lat},{center.Lon});
  relation[""leisure""~""park|playground|garden""]
    (around:{radiusMeters},{center.Lat},{center.Lon});
);
out body;
>;
out skel qt;
";
    }
    
    private MapData ProcessOverpassResult(OverpassResult result, Position center, double radiusMeters)
    {
        var mapData = new MapData
        {
            Center = center,
            RadiusMeters = radiusMeters,
            FetchedAt = DateTime.UtcNow
        };
        
        // Index nodes by ID for quick lookup
        var nodesById = new Dictionary<long, OverpassNode>();
        foreach (var element in result.Elements)
        {
            if (element.Type == "node" && element.Lat.HasValue && element.Lon.HasValue)
            {
                nodesById[element.Id] = new OverpassNode
                {
                    Id = element.Id,
                    Lat = element.Lat.Value,
                    Lon = element.Lon.Value,
                    Tags = element.Tags
                };
            }
        }
        
        // Process ways
        foreach (var element in result.Elements.Where(e => e.Type == "way"))
        {
            if (element.Nodes == null || element.Nodes.Count < 2) continue;
            
            var wayNodes = new List<Position>();
            foreach (var nodeId in element.Nodes)
            {
                if (nodesById.TryGetValue(nodeId, out var node))
                {
                    wayNodes.Add(new Position(node.Lat, node.Lon));
                }
            }
            
            if (wayNodes.Count < 2) continue;
            
            var tags = element.Tags ?? new Dictionary<string, string>();
            
            // Classify way type
            if (tags.ContainsKey("building"))
            {
                mapData.Buildings.Add(new MapBuilding
                {
                    Id = $"b_{element.Id}",
                    Outline = wayNodes,
                    BuildingType = tags.GetValueOrDefault("building", "yes"),
                    Name = tags.GetValueOrDefault("name"),
                    Levels = int.TryParse(tags.GetValueOrDefault("building:levels", "1"), out var l) ? l : 1
                });
            }
            else if (tags.ContainsKey("highway"))
            {
                var highwayType = tags["highway"];
                mapData.Pathways.Add(new MapPathway
                {
                    Id = $"p_{element.Id}",
                    Points = wayNodes,
                    PathType = ClassifyPathType(highwayType),
                    Name = tags.GetValueOrDefault("name"),
                    IsWalkable = IsWalkableHighway(highwayType),
                    Width = EstimatePathWidth(highwayType)
                });
            }
            else if (tags.ContainsKey("leisure"))
            {
                mapData.Areas.Add(new MapArea
                {
                    Id = $"a_{element.Id}",
                    Outline = wayNodes,
                    AreaType = MapAreaType.Park,
                    Name = tags.GetValueOrDefault("name")
                });
            }
            else if (tags.ContainsKey("amenity"))
            {
                // Calculate centroid for POI
                var centroid = CalculateCentroid(wayNodes);
                mapData.PointsOfInterest.Add(new MapPOI
                {
                    Id = $"poi_{element.Id}",
                    Position = centroid,
                    PoiType = ClassifyAmenity(tags["amenity"]),
                    Name = tags.GetValueOrDefault("name")
                });
            }
        }
        
        // Process standalone POI nodes
        foreach (var element in result.Elements.Where(e => e.Type == "node" && e.Tags != null))
        {
            var tags = element.Tags!;
            if (tags.ContainsKey("amenity") && element.Lat.HasValue && element.Lon.HasValue)
            {
                mapData.PointsOfInterest.Add(new MapPOI
                {
                    Id = $"poi_{element.Id}",
                    Position = new Position(element.Lat.Value, element.Lon.Value),
                    PoiType = ClassifyAmenity(tags["amenity"]),
                    Name = tags.GetValueOrDefault("name")
                });
            }
        }
        
        // Build reachability graph and compute reachable positions
        BuildReachabilityData(mapData, center, radiusMeters);
        
        _logger.LogInformation(
            "Processed map data: {Buildings} buildings, {Pathways} pathways, {POIs} POIs, {ReachablePoints} reachable points",
            mapData.Buildings.Count, mapData.Pathways.Count, mapData.PointsOfInterest.Count, mapData.ReachablePositions.Count);
        
        return mapData;
    }
    
    private void BuildReachabilityData(MapData mapData, Position center, double radiusMeters)
    {
        // Build a graph of walkable paths and find connected components from center
        var graph = new PathGraph();
        
        // Add all walkable pathway segments to graph
        foreach (var pathway in mapData.Pathways.Where(p => p.IsWalkable))
        {
            for (int i = 0; i < pathway.Points.Count - 1; i++)
            {
                graph.AddEdge(pathway.Points[i], pathway.Points[i + 1]);
            }
        }
        
        // Find all positions reachable from center (or nearest walkable point to center)
        var startNode = graph.FindNearestNode(center);
        if (startNode == null)
        {
            _logger.LogWarning("No walkable paths found near center, using all pathway points as reachable");
            // Fallback: all pathway points are considered reachable
            foreach (var pathway in mapData.Pathways.Where(p => p.IsWalkable))
            {
                mapData.ReachablePositions.AddRange(pathway.Points);
            }
            return;
        }
        
        // BFS from start node to find all reachable nodes
        var reachable = graph.GetReachableNodes(startNode.Value, radiusMeters);
        
        // Filter to only include points within the play area
        foreach (var pos in reachable)
        {
            if (pos.DistanceTo(center) <= radiusMeters)
            {
                mapData.ReachablePositions.Add(pos);
            }
        }
        
        // Also mark pathways as reachable or not
        foreach (var pathway in mapData.Pathways)
        {
            var reachablePoints = pathway.Points.Count(p => 
                mapData.ReachablePositions.Any(rp => rp.DistanceTo(p) < 5));
            pathway.IsFullyReachable = reachablePoints == pathway.Points.Count;
            pathway.IsPartiallyReachable = reachablePoints > 0;
        }
        
        _logger.LogDebug("Reachability analysis: {Reachable} of {Total} positions are reachable from center",
            mapData.ReachablePositions.Count, 
            mapData.Pathways.Sum(p => p.Points.Count));
    }
    
    private PathType ClassifyPathType(string highway)
    {
        return highway switch
        {
            "footway" or "pedestrian" or "path" => PathType.Footway,
            "steps" => PathType.Steps,
            "cycleway" => PathType.Cycleway,
            "residential" or "living_street" => PathType.Residential,
            "service" => PathType.Service,
            "tertiary" or "secondary" or "primary" => PathType.Road,
            "track" or "bridleway" => PathType.Track,
            _ => PathType.Other
        };
    }
    
    private bool IsWalkableHighway(string highway)
    {
        // Everything except motorways is considered walkable
        return highway switch
        {
            "motorway" or "motorway_link" or "trunk" or "trunk_link" => false,
            _ => true
        };
    }
    
    private double EstimatePathWidth(string highway)
    {
        return highway switch
        {
            "footway" or "path" => 1.5,
            "pedestrian" => 5.0,
            "steps" => 2.0,
            "cycleway" => 2.0,
            "residential" => 6.0,
            "living_street" => 5.0,
            "service" => 4.0,
            "tertiary" => 7.0,
            "secondary" => 9.0,
            "primary" => 11.0,
            _ => 3.0
        };
    }
    
    private MapPOIType ClassifyAmenity(string amenity)
    {
        return amenity switch
        {
            "cafe" or "restaurant" or "fast_food" or "bar" or "pub" => MapPOIType.FoodDrink,
            "bank" or "atm" => MapPOIType.Finance,
            "pharmacy" or "hospital" or "clinic" or "doctors" => MapPOIType.Health,
            "school" or "university" or "library" => MapPOIType.Education,
            "fuel" or "parking" => MapPOIType.Transport,
            "shop" or "supermarket" or "convenience" => MapPOIType.Shop,
            "toilets" or "bench" or "drinking_water" => MapPOIType.Amenity,
            _ => MapPOIType.Other
        };
    }
    
    private Position CalculateCentroid(List<Position> points)
    {
        if (points.Count == 0) return new Position(0, 0);
        var lat = points.Average(p => p.Lat);
        var lon = points.Average(p => p.Lon);
        return new Position(lat, lon);
    }
    
    /// <summary>
    /// Get a random reachable position suitable for placing a task or repair station
    /// </summary>
    public Position? GetRandomReachablePosition(MapData mapData, Random random, double minDistFromCenter = 0)
    {
        var candidates = mapData.ReachablePositions
            .Where(p => p.DistanceTo(mapData.Center) >= minDistFromCenter)
            .ToList();
        
        if (candidates.Count == 0)
        {
            // Fallback to any pathway point
            candidates = mapData.Pathways
                .Where(p => p.IsWalkable)
                .SelectMany(p => p.Points)
                .Where(p => p.DistanceTo(mapData.Center) <= mapData.RadiusMeters)
                .ToList();
        }
        
        if (candidates.Count == 0)
            return null;
        
        return candidates[random.Next(candidates.Count)];
    }
    
    /// <summary>
    /// Get multiple well-distributed reachable positions (e.g., for placing multiple tasks)
    /// </summary>
    public List<Position> GetDistributedReachablePositions(MapData mapData, int count, Random random, double minSpacing = 20)
    {
        var result = new List<Position>();
        var available = mapData.ReachablePositions.ToList();
        
        // Shuffle available positions
        for (int i = available.Count - 1; i > 0; i--)
        {
            int j = random.Next(i + 1);
            (available[i], available[j]) = (available[j], available[i]);
        }
        
        foreach (var pos in available)
        {
            if (result.Count >= count) break;
            
            // Check minimum spacing from already selected positions
            bool tooClose = result.Any(r => r.DistanceTo(pos) < minSpacing);
            if (!tooClose)
            {
                result.Add(pos);
            }
        }
        
        // If we couldn't find enough spaced positions, fill with any available
        if (result.Count < count)
        {
            foreach (var pos in available)
            {
                if (result.Count >= count) break;
                if (!result.Contains(pos))
                {
                    result.Add(pos);
                }
            }
        }
        
        return result;
    }
    
    /// <summary>
    /// Get positions near POIs (better for task placement as they have semantic meaning)
    /// </summary>
    public List<Position> GetPOIBasedPositions(MapData mapData, int count, Random random)
    {
        var result = new List<Position>();
        
        // Prefer POIs that are reachable
        var reachablePOIs = mapData.PointsOfInterest
            .Where(poi => mapData.ReachablePositions.Any(rp => rp.DistanceTo(poi.Position) < 10))
            .ToList();
        
        // Shuffle
        for (int i = reachablePOIs.Count - 1; i > 0; i--)
        {
            int j = random.Next(i + 1);
            (reachablePOIs[i], reachablePOIs[j]) = (reachablePOIs[j], reachablePOIs[i]);
        }
        
        foreach (var poi in reachablePOIs.Take(count))
        {
            result.Add(poi.Position);
        }
        
        // Fill remaining with distributed positions
        if (result.Count < count)
        {
            var additional = GetDistributedReachablePositions(mapData, count - result.Count, random);
            result.AddRange(additional);
        }
        
        return result;
    }
}

#region Overpass API Response Types

internal class OverpassResult
{
    [JsonPropertyName("elements")]
    public List<OverpassElement> Elements { get; set; } = new();
}

internal class OverpassElement
{
    [JsonPropertyName("type")]
    public string Type { get; set; } = "";
    
    [JsonPropertyName("id")]
    public long Id { get; set; }
    
    [JsonPropertyName("lat")]
    public double? Lat { get; set; }
    
    [JsonPropertyName("lon")]
    public double? Lon { get; set; }
    
    [JsonPropertyName("nodes")]
    public List<long>? Nodes { get; set; }
    
    [JsonPropertyName("tags")]
    public Dictionary<string, string>? Tags { get; set; }
}

internal class OverpassNode
{
    public long Id { get; set; }
    public double Lat { get; set; }
    public double Lon { get; set; }
    public Dictionary<string, string>? Tags { get; set; }
}

#endregion

#region Path Graph for Reachability

internal class PathGraph
{
    private readonly Dictionary<Position, HashSet<Position>> _adjacency = new();
    private readonly List<Position> _allNodes = new();
    
    public void AddEdge(Position a, Position b)
    {
        if (!_adjacency.ContainsKey(a))
        {
            _adjacency[a] = new HashSet<Position>();
            _allNodes.Add(a);
        }
        if (!_adjacency.ContainsKey(b))
        {
            _adjacency[b] = new HashSet<Position>();
            _allNodes.Add(b);
        }
        
        _adjacency[a].Add(b);
        _adjacency[b].Add(a);
    }
    
    public Position? FindNearestNode(Position target)
    {
        if (_allNodes.Count == 0) return null;
        
        Position? nearest = null;
        double minDist = double.MaxValue;
        
        foreach (var node in _allNodes)
        {
            var dist = node.DistanceTo(target);
            if (dist < minDist)
            {
                minDist = dist;
                nearest = node;
            }
        }
        
        return nearest;
    }
    
    public HashSet<Position> GetReachableNodes(Position start, double maxDistance)
    {
        var visited = new HashSet<Position>();
        var queue = new Queue<Position>();
        
        queue.Enqueue(start);
        visited.Add(start);
        
        while (queue.Count > 0)
        {
            var current = queue.Dequeue();
            
            if (!_adjacency.TryGetValue(current, out var neighbors))
                continue;
            
            foreach (var neighbor in neighbors)
            {
                if (!visited.Contains(neighbor) && neighbor.DistanceTo(start) <= maxDistance)
                {
                    visited.Add(neighbor);
                    queue.Enqueue(neighbor);
                }
            }
        }
        
        return visited;
    }
}

#endregion
