using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using SatelliteProvider.Common.Configs;
using SatelliteProvider.Common.DTO;
using SatelliteProvider.Common.Imaging;
using SatelliteProvider.Common.Utils;
using SatelliteProvider.DataAccess.Models;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;

namespace SatelliteProvider.Services.RouteManagement;

// AZ-364 / C11: extracted from RouteProcessingService.StitchRouteTilesAsync.
// Owns the route_<id>_stitched.jpg path computation, the per-tile filename
// parsing into TilePlacement, the call into the shared TileGridStitcher
// (Common), and the route-specific overlays (yellow geofence rectangles,
// red 50-arm crosses on each route point). Behavior preserved verbatim per
// AC-2 (pixel-for-pixel identical output for existing scenarios).
public class RouteImageRenderer
{
    private readonly StorageConfig _storageConfig;
    private readonly int _tileSizePixels;
    private readonly ILogger<RouteImageRenderer> _logger;

    public RouteImageRenderer(IOptions<StorageConfig> storageConfig, IOptions<MapConfig> mapConfig, ILogger<RouteImageRenderer> logger)
    {
        ArgumentNullException.ThrowIfNull(storageConfig);
        ArgumentNullException.ThrowIfNull(mapConfig);
        _storageConfig = storageConfig.Value;
        _tileSizePixels = mapConfig.Value.TileSizePixels;
        _logger = logger;
    }

    public async Task<string?> RenderAsync(
        Guid routeId,
        IReadOnlyList<TileInfo> tiles,
        int zoomLevel,
        IReadOnlyList<(int MinX, int MinY, int MaxX, int MaxY)> geofencePolygonBounds,
        IReadOnlyList<RoutePointEntity> routePoints,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(tiles);
        ArgumentNullException.ThrowIfNull(geofencePolygonBounds);
        ArgumentNullException.ThrowIfNull(routePoints);

        if (tiles.Count == 0)
        {
            _logger.LogWarning("No tiles to stitch for route map");
            return null;
        }

        var placements = tiles
            .Select(t =>
            {
                var (tileX, tileY) = ExtractTileCoordinatesFromFilename(t.FilePath);
                return new TilePlacement(tileX, tileY, t.FilePath);
            })
            .Where(p => p.TileX >= 0 && p.TileY >= 0)
            .ToList();

        if (placements.Count == 0)
        {
            _logger.LogWarning("No tiles with extractable coordinates to stitch for route map");
            return null;
        }

        Directory.CreateDirectory(_storageConfig.ReadyDirectory);
        var outputPath = Path.Combine(_storageConfig.ReadyDirectory, $"route_{routeId}_stitched.jpg");

        var stitcher = new TileGridStitcher();
        var result = await stitcher.StitchAsync(
            placements,
            _tileSizePixels,
            deduplicateByTileCoords: true,
            swallowTileLoadErrors: true,
            cancellationToken);

        using var stitchedImage = result.Image;

        foreach (var missing in result.MissingTiles)
        {
            if (missing.Reason == MissingTileReason.LoadFailed)
            {
                _logger.LogWarning(missing.Error, "Failed to load tile at {FilePath}, leaving black", missing.Tile.FilePath);
            }
            else
            {
                _logger.LogWarning("Tile file not found: {FilePath}, leaving black", missing.Tile.FilePath);
            }
        }

        var minX = result.MinX;
        var minY = result.MinY;
        var imageWidth = result.ImageWidth;
        var imageHeight = result.ImageHeight;
        var yellow = new Rgb24(255, 255, 0);
        var red = new Rgb24(255, 0, 0);

        foreach (var (geoMinX, geoMinY, geoMaxX, geoMaxY) in geofencePolygonBounds)
        {
            var x1 = (geoMinX - minX) * _tileSizePixels;
            var y1 = (geoMinY - minY + 1) * _tileSizePixels;
            var x2 = (geoMaxX - minX + 2) * _tileSizePixels - 1;
            var y2 = (geoMaxY - minY + 1) * _tileSizePixels - 1;

            x1 = Math.Max(0, Math.Min(x1, imageWidth - 1));
            y1 = Math.Max(0, Math.Min(y1, imageHeight - 1));
            x2 = Math.Max(0, Math.Min(x2, imageWidth - 1));
            y2 = Math.Max(0, Math.Min(y2, imageHeight - 1));

            if (x1 >= 0 && y1 >= 0 && x2 < imageWidth && y2 < imageHeight && x2 > x1 && y2 > y1)
            {
                stitcher.DrawRectangleBorder(
                    stitchedImage,
                    new Rectangle(x1, y1, x2 - x1 + 1, y2 - y1 + 1),
                    yellow);
            }
        }

        foreach (var point in routePoints)
        {
            var geoPoint = new GeoPoint(point.Latitude, point.Longitude);
            var (tileX, tileY) = GeoUtils.WorldToTilePos(geoPoint, zoomLevel);

            var pixelX = (tileX - minX) * _tileSizePixels + _tileSizePixels / 2;
            var pixelY = (tileY - minY) * _tileSizePixels + _tileSizePixels / 2;

            if (pixelX >= 0 && pixelX < imageWidth && pixelY >= 0 && pixelY < imageHeight)
            {
                stitcher.DrawCross(stitchedImage, new Point(pixelX, pixelY), red, 50);
            }
        }

        await stitchedImage.SaveAsJpegAsync(outputPath, cancellationToken);
        return outputPath;
    }

    // Parses tile_<zoom>_<x>_<y>_<timestamp>.jpg via StorageConfig.TryExtractTileCoordinates.
    // On parse failure, logs a warning and returns the (-1, -1) sentinel — the
    // RenderAsync pipeline filters those out before stitching. ArgumentNullException
    // on null input is propagated from StorageConfig.
    internal (int TileX, int TileY) ExtractTileCoordinatesFromFilename(string filePath)
    {
        if (StorageConfig.TryExtractTileCoordinates(filePath, out var tileX, out var tileY))
        {
            return (tileX, tileY);
        }

        _logger.LogWarning(
            "Could not extract tile coordinates from filename {FilePath}; expected pattern tile_<zoom>_<x>_<y>_<timestamp>",
            filePath);
        return (-1, -1);
    }
}
