using Microsoft.Extensions.Logging;
using LocationTrackerApp.Models;

namespace LocationTrackerApp.Components;

/// <summary>
/// Custom WebView component for displaying OpenStreetMap
/// </summary>
public class OpenStreetMapView : WebView
{
    private readonly ILogger<OpenStreetMapView>? _logger;
    private List<LocationData> _locationData = new();
    private bool _isHeatMapVisible = true;
    private bool _isPointsVisible = true;

    /// <summary>
    /// Gets or sets a value indicating whether the heat map overlay is visible.
    /// </summary>
    public bool IsHeatMapVisible
    {
        get => _isHeatMapVisible;
        set
        {
            if (_isHeatMapVisible != value)
            {
                _isHeatMapVisible = value;
                UpdateHeatMapVisibility();
            }
        }
    }

    /// <summary>
    /// Gets or sets a value indicating whether individual location points are visible.
    /// </summary>
    public bool IsPointsVisible
    {
        get => _isPointsVisible;
        set
        {
            if (_isPointsVisible != value)
            {
                _isPointsVisible = value;
                UpdatePointsVisibility();
            }
        }
    }

    /// <summary>
    /// Default constructor for XAML
    /// </summary>
    public OpenStreetMapView()
    {
        InitializeMap();
    }

    /// <summary>
    /// Initializes a new instance of OpenStreetMapView
    /// </summary>
    /// <param name="logger">Logger for recording events</param>
    public OpenStreetMapView(ILogger<OpenStreetMapView> logger) : this()
    {
        _logger = logger;
    }

    /// <summary>
    /// Initializes the map with default settings
    /// </summary>
    private void InitializeMap()
    {
        // Set default location to San Francisco Bay Area
        var defaultLocation = new Location(37.7749, -122.4194);
        LoadMap(defaultLocation);
    }

    /// <summary>
    /// Loads the OpenStreetMap with Leaflet.js
    /// </summary>
    /// <param name="centerLocation">The center location for the map</param>
    private void LoadMap(Location centerLocation)
    {
        var html = GenerateMapHtml(centerLocation);
        var htmlSource = new HtmlWebViewSource { Html = html };
        Source = htmlSource;
    }

    /// <summary>
    /// Generates the HTML content for the OpenStreetMap with Leaflet.js
    /// </summary>
    /// <param name="centerLocation">The center location for the map</param>
    /// <returns>HTML string containing the map</returns>
    private string GenerateMapHtml(Location centerLocation)
    {
        return $@"
<!DOCTYPE html>
<html>
<head>
    <meta charset='utf-8' />
    <meta name='viewport' content='width=device-width, initial-scale=1.0'>
    <title>OpenStreetMap</title>
    <link rel='stylesheet' href='https://unpkg.com/leaflet@1.9.4/dist/leaflet.css' />
    <style>
        body {{ margin: 0; padding: 0; }}
        #map {{ height: 100vh; width: 100vw; }}
    </style>
</head>
<body>
    <div id='map'></div>
    <script src='https://unpkg.com/leaflet@1.9.4/dist/leaflet.js'></script>
    <script>
        // Initialize the map
        var map = L.map('map').setView([{centerLocation.Latitude}, {centerLocation.Longitude}], 13);
        
        // Add OpenStreetMap tiles
        L.tileLayer('https://{{s}}.tile.openstreetmap.org/{{z}}/{{x}}/{{y}}.png', {{
            attribution: '© <a href=""https://www.openstreetmap.org/copyright"">OpenStreetMap</a> contributors',
            maxZoom: 19
        }}).addTo(map);
        
        // Store markers for heat map functionality
        var markers = [];
        var polylines = [];
        
        // Function to add a location pin
        function addLocationPin(lat, lng, label, intensity) {{
            var color = getHeatColor(intensity);
            var marker = L.circleMarker([lat, lng], {{
                radius: 8,
                fillColor: color,
                color: '#000',
                weight: 1,
                opacity: 1,
                fillOpacity: 0.8
            }}).addTo(map);
            
            if (label) {{
                marker.bindPopup(label);
            }}
            
            markers.push(marker);
            return marker;
        }}
        
        // Function to add a polyline for heat map
        function addHeatMapPolyline(coordinates, intensity) {{
            var color = getHeatColor(intensity);
            var polyline = L.polyline(coordinates, {{
                color: color,
                weight: 7,
                opacity: 0.3 + (intensity * 0.7)
            }}).addTo(map);
            
            polylines.push(polyline);
            return polyline;
        }}
        
        // Function to get heat color based on intensity
        function getHeatColor(intensity) {{
            if (intensity < 0.25) return '#0000FF'; // Blue
            if (intensity < 0.50) return '#00FFFF'; // Cyan
            if (intensity < 0.75) return '#FFFF00'; // Yellow
            return '#FF0000'; // Red
        }}
        
        // Function to clear all markers and polylines
        function clearMap() {{
            markers.forEach(marker => map.removeLayer(marker));
            polylines.forEach(polyline => map.removeLayer(polyline));
            markers = [];
            polylines = [];
        }}
        
        // Function to fit map to bounds
        function fitToBounds() {{
            if (markers.length > 0) {{
                var group = new L.featureGroup(markers);
                map.fitBounds(group.getBounds().pad(0.1));
            }}
        }}
        
        // Expose functions to global scope for C# access
        window.addLocationPin = addLocationPin;
        window.addHeatMapPolyline = addHeatMapPolyline;
        window.clearMap = clearMap;
        window.fitToBounds = fitToBounds;
        window.map = map;
    </script>
</body>
</html>";
    }

    /// <summary>
    /// Adds a location pin to the map
    /// </summary>
    /// <param name="location">The location to add</param>
    /// <param name="label">The label for the pin</param>
    /// <param name="intensity">Heat intensity (0.0 to 1.0)</param>
    public async Task AddLocationPinAsync(Location location, string label, double intensity = 0.5)
    {
        try
        {
            var script = $"addLocationPin({location.Latitude}, {location.Longitude}, '{label}', {intensity});";
            await EvaluateJavaScriptAsync(script);
        }
        catch (Exception ex)
        {
            _logger?.LogError(ex, "Failed to add location pin to map");
        }
    }

    /// <summary>
    /// Adds a heat map polyline to the map
    /// </summary>
    /// <param name="coordinates">List of coordinates for the polyline</param>
    /// <param name="intensity">Heat intensity (0.0 to 1.0)</param>
    public async Task AddHeatMapPolylineAsync(List<Location> coordinates, double intensity = 0.5)
    {
        try
        {
            var coordString = string.Join(",", coordinates.Select(c => $"[{c.Latitude}, {c.Longitude}]"));
            var script = $"addHeatMapPolyline([{coordString}], {intensity});";
            await EvaluateJavaScriptAsync(script);
        }
        catch (Exception ex)
        {
            _logger?.LogError(ex, "Failed to add heat map polyline to map");
        }
    }

    /// <summary>
    /// Clears all markers and polylines from the map
    /// </summary>
    public async Task ClearMapAsync()
    {
        try
        {
            await EvaluateJavaScriptAsync("clearMap();");
        }
        catch (Exception ex)
        {
            _logger?.LogError(ex, "Failed to clear map");
        }
    }

    /// <summary>
    /// Fits the map view to show all markers
    /// </summary>
    public async Task FitToBoundsAsync()
    {
        try
        {
            await EvaluateJavaScriptAsync("fitToBounds();");
        }
        catch (Exception ex)
        {
            _logger?.LogError(ex, "Failed to fit map to bounds");
        }
    }

    /// <summary>
    /// Updates the heat map visualization based on current location data.
    /// </summary>
    public async Task UpdateHeatMapAsync()
    {
        try
        {
            await ClearMapAsync();

            if (!_locationData.Any())
            {
                _logger?.LogInformation("No location data to display for heat map.");
                return;
            }

            // Group nearby points to determine intensity
            var groupedLocations = GroupLocations(_locationData, 0.001); // Group within ~100 meters

            // Create heat map polylines and pins
            foreach (var group in groupedLocations)
            {
                var intensity = Math.Min(1.0, (double)group.Count / 10.0); // Scale intensity

                if (group.Count > 1)
                {
                    // Create polyline for grouped points
                    var coordinates = group.Select(loc => new Location(loc.Latitude, loc.Longitude)).ToList();
                    await AddHeatMapPolylineAsync(coordinates, intensity);
                }

                // Add a pin for each location (or group center)
                var pinLocation = new Location(group.First().Latitude, group.First().Longitude);
                await AddLocationPinAsync(pinLocation, $"Location {group.First().Id} (Intensity: {intensity:P0})", intensity);
            }

            await FitToBoundsAsync();
            _logger?.LogInformation("Heat map updated with {Count} points.", _locationData.Count);
        }
        catch (Exception ex)
        {
            _logger?.LogError(ex, "Failed to update heat map");
        }
    }

    /// <summary>
    /// Groups locations that are within a certain distance of each other.
    /// </summary>
    /// <param name="locations">The list of all location data.</param>
    /// <param name="tolerance">The maximum distance (in degrees) for grouping.</param>
    /// <returns>A list of grouped locations.</returns>
    private static List<List<LocationData>> GroupLocations(List<LocationData> locations, double tolerance)
    {
        var grouped = new List<List<LocationData>>();
        var processed = new HashSet<LocationData>();

        foreach (var loc in locations)
        {
            if (processed.Contains(loc)) continue;

            var currentGroup = new List<LocationData> { loc };
            processed.Add(loc);

            foreach (var otherLoc in locations)
            {
                if (loc == otherLoc || processed.Contains(otherLoc)) continue;

                var distance = Location.CalculateDistance(
                    new Location(loc.Latitude, loc.Longitude),
                    new Location(otherLoc.Latitude, otherLoc.Longitude),
                    DistanceUnits.Kilometers);

                if (distance < tolerance) // Roughly 100 meters
                {
                    currentGroup.Add(otherLoc);
                    processed.Add(otherLoc);
                }
            }
            grouped.Add(currentGroup);
        }
        return grouped;
    }

    /// <summary>
    /// Updates the visibility of heat map polylines.
    /// </summary>
    private void UpdateHeatMapVisibility()
    {
        // Implementation would require JavaScript communication
        // For now, we'll handle this in the UpdateHeatMapAsync method
    }

    /// <summary>
    /// Updates the visibility of individual location pins.
    /// </summary>
    private void UpdatePointsVisibility()
    {
        // Implementation would require JavaScript communication
        // For now, we'll handle this in the UpdateHeatMapAsync method
    }

    /// <summary>
    /// Loads location data and updates the heat map
    /// </summary>
    /// <param name="locationData">The location data to display</param>
    public async Task LoadLocationDataAsync(List<LocationData> locationData)
    {
        _locationData = locationData;
        await UpdateHeatMapAsync();
    }
}
