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

namespace SatelliteProvider.Services.RegionProcessing;

public class RegionService : IRegionService
{
    private readonly IRegionRepository _regionRepository;
    private readonly IRegionRequestQueue _queue;
    private readonly ITileService _tileService;
    private readonly StorageConfig _storageConfig;
    private readonly ProcessingConfig _processingConfig;
    private readonly ILogger<RegionService> _logger;

    public RegionService(
        IRegionRepository regionRepository,
        IRegionRequestQueue queue,
        ITileService tileService,
        IOptions<StorageConfig> storageConfig,
        IOptions<ProcessingConfig> processingConfig,
        ILogger<RegionService> logger)
    {
        _regionRepository = regionRepository;
        _queue = queue;
        _tileService = tileService;
        _storageConfig = storageConfig.Value;
        _processingConfig = processingConfig.Value;
        _logger = logger;
    }

    public async Task<RegionStatusResponse> RequestRegionAsync(Guid id, double latitude, double longitude, double sizeMeters, int zoomLevel, bool stitchTiles = false)
    {
        // AZ-362: idempotent POST contract. A retried POST with the same caller-supplied
        // Id returns the existing region instead of bubbling a unique-key violation.
        var existing = await _regionRepository.GetByIdAsync(id);
        if (existing != null)
        {
            _logger.LogInformation(
                "Idempotent region POST: id {RegionId} already exists with status {Status}; returning existing resource without re-enqueueing",
                id, existing.Status.ToString().ToLowerInvariant());
            return MapToStatus(existing);
        }

        var now = DateTime.UtcNow;
        var region = new RegionEntity
        {
            Id = id,
            Latitude = latitude,
            Longitude = longitude,
            SizeMeters = sizeMeters,
            ZoomLevel = zoomLevel,
            StitchTiles = stitchTiles,
            Status = RegionStatus.Queued,
            TilesDownloaded = 0,
            TilesReused = 0,
            CreatedAt = now,
            UpdatedAt = now
        };

        await _regionRepository.InsertAsync(region);

        var request = new RegionRequest
        {
            Id = id,
            Latitude = latitude,
            Longitude = longitude,
            SizeMeters = sizeMeters,
            ZoomLevel = zoomLevel,
            StitchTiles = stitchTiles
        };

        await _queue.EnqueueAsync(request);

        return MapToStatus(region);
    }

    public async Task<RegionStatusResponse?> GetRegionStatusAsync(Guid id)
    {
        var region = await _regionRepository.GetByIdAsync(id);
        return region != null ? MapToStatus(region) : null;
    }

    public async Task ProcessRegionAsync(Guid id, CancellationToken cancellationToken = default)
    {
        var startTime = DateTime.UtcNow;

        var region = await _regionRepository.GetByIdAsync(id);
        if (region == null)
        {
            _logger.LogWarning("Region {RegionId} not found in database", id);
            return;
        }

        region.Status = RegionStatus.Processing;
        region.UpdatedAt = DateTime.UtcNow;
        await _regionRepository.UpdateAsync(region);

        using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(_processingConfig.RegionProcessingTimeoutSeconds));
        using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);

        string? errorMessage = null;
        List<TileMetadata>? tiles = null;
        int tilesDownloaded = 0;
        int tilesReused = 0;

        try
        {
            var processingStartTime = DateTime.UtcNow;

            var tilesBeforeDownload = await _tileService.GetTilesByRegionAsync(
                region.Latitude,
                region.Longitude,
                region.SizeMeters,
                region.ZoomLevel);
            var existingTileIds = new HashSet<Guid>(tilesBeforeDownload.Select(t => t.Id));

            tiles = await _tileService.DownloadAndStoreTilesAsync(
                region.Latitude,
                region.Longitude,
                region.SizeMeters,
                region.ZoomLevel,
                linkedCts.Token);

            tilesDownloaded = tiles.Count(t => !existingTileIds.Contains(t.Id));
            tilesReused = tiles.Count(t => existingTileIds.Contains(t.Id));

            var readyDir = _storageConfig.ReadyDirectory;
            Directory.CreateDirectory(readyDir);

            var csvPath = Path.Combine(readyDir, $"region_{id}_ready.csv");
            var summaryPath = Path.Combine(readyDir, $"region_{id}_summary.txt");
            string? stitchedImagePath = null;

            await GenerateCsvFileAsync(csvPath, tiles, linkedCts.Token);

            if (region.StitchTiles)
            {
                stitchedImagePath = Path.Combine(readyDir, $"region_{id}_stitched.jpg");
                await StitchTilesAsync(tiles, region.Latitude, region.Longitude, region.ZoomLevel, stitchedImagePath, linkedCts.Token);
            }

            await GenerateSummaryFileAsync(summaryPath, id, region, tiles, tilesDownloaded, tilesReused, stitchedImagePath, processingStartTime, linkedCts.Token, errorMessage);

            region.Status = RegionStatus.Completed;
            region.CsvFilePath = csvPath;
            region.SummaryFilePath = summaryPath;
            region.TilesDownloaded = tilesDownloaded;
            region.TilesReused = tilesReused;
            region.UpdatedAt = DateTime.UtcNow;
            await _regionRepository.UpdateAsync(region);
        }
        catch (Exception ex)
        {
            var classification = RegionFailureClassifier.Classify(ex, timeoutCts, cancellationToken);
            errorMessage = classification.ErrorMessage;
            _logger.LogError(
                ex,
                "Region {RegionId} processing failed (category={Category}): {ErrorMessage}",
                id,
                classification.Category,
                classification.ErrorMessage);
            await HandleProcessingFailureAsync(id, region, startTime, tiles, tilesDownloaded, tilesReused, errorMessage);
        }
    }

    private async Task HandleProcessingFailureAsync(
        Guid id,
        RegionEntity region,
        DateTime startTime,
        List<TileMetadata>? tiles,
        int tilesDownloaded,
        int tilesReused,
        string errorMessage)
    {
        region.Status = RegionStatus.Failed;
        region.UpdatedAt = DateTime.UtcNow;

        try
        {
            var readyDir = _storageConfig.ReadyDirectory;
            Directory.CreateDirectory(readyDir);

            var summaryPath = Path.Combine(readyDir, $"region_{id}_summary.txt");
            region.SummaryFilePath = summaryPath;

            await GenerateSummaryFileAsync(
                summaryPath,
                id,
                region,
                tiles ?? new List<TileMetadata>(),
                tilesDownloaded,
                tilesReused,
                null,
                startTime,
                CancellationToken.None,
                errorMessage);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to generate error summary for region {RegionId}", id);
        }

        await _regionRepository.UpdateAsync(region);
    }

    private async Task<string> StitchTilesAsync(
        List<TileMetadata> tiles,
        double centerLatitude,
        double centerLongitude,
        int zoomLevel,
        string outputPath,
        CancellationToken cancellationToken)
    {
        if (tiles.Count == 0)
        {
            throw new InvalidOperationException("No tiles to stitch");
        }

        var tileSizePixels = tiles.First().TileSizePixels;

        var placements = tiles.Select(t =>
        {
            var (x, y) = GeoUtils.WorldToTilePos(new GeoPoint(t.Latitude, t.Longitude), zoomLevel);
            return new TilePlacement(x, y, t.FilePath);
        });

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

        using var stitchedImage = result.Image;

        foreach (var missing in result.MissingTiles)
        {
            _logger.LogWarning("Tile file not found: {FilePath}", missing.Tile.FilePath);
        }

        var (centerTileX, centerTileY) = GeoUtils.WorldToTilePos(new GeoPoint(centerLatitude, centerLongitude), zoomLevel);

        var n = Math.Pow(2.0, zoomLevel);
        var centerTilePixelX = ((centerLongitude + 180.0) / 360.0 * n - centerTileX) * tileSizePixels;
        var centerTilePixelY = ((1.0 - Math.Log(Math.Tan(centerLatitude * Math.PI / 180.0) + 1.0 / Math.Cos(centerLatitude * Math.PI / 180.0)) / Math.PI) / 2.0 * n - centerTileY) * tileSizePixels;

        var crossX = (int)Math.Round((centerTileX - result.MinX) * tileSizePixels + centerTilePixelX);
        var crossY = (int)Math.Round((centerTileY - result.MinY) * tileSizePixels + centerTilePixelY);

        var red = new Rgb24(255, 0, 0);
        var imageWidth = result.ImageWidth;
        var imageHeight = result.ImageHeight;

        for (int i = -5; i < 5; i++)
        {
            var hx = crossX + i;
            var vy = crossY + i;

            if (hx >= 0 && hx < imageWidth && crossY >= 0 && crossY < imageHeight)
            {
                stitchedImage[hx, crossY] = red;
            }

            if (crossX >= 0 && crossX < imageWidth && vy >= 0 && vy < imageHeight)
            {
                stitchedImage[crossX, vy] = red;
            }
        }

        await stitchedImage.SaveAsJpegAsync(outputPath, cancellationToken);

        return outputPath;
    }

    private static async Task GenerateCsvFileAsync(string filePath, List<TileMetadata> tiles, CancellationToken cancellationToken)
    {
        var rows = tiles.Select(t => new TileCsvRow(t.Latitude, t.Longitude, t.FilePath));
        await new TileCsvWriter().WriteAsync(filePath, rows, cancellationToken);
    }

    private async Task GenerateSummaryFileAsync(
        string filePath,
        Guid regionId,
        RegionEntity region,
        List<TileMetadata> tiles,
        int tilesDownloaded,
        int tilesReused,
        string? stitchedImagePath,
        DateTime startTime,
        CancellationToken cancellationToken,
        string? errorMessage = null)
    {
        var endTime = DateTime.UtcNow;
        var processingTime = (endTime - startTime).TotalSeconds;

        var summary = new System.Text.StringBuilder();
        summary.AppendLine("Region Processing Summary");
        summary.AppendLine("========================");
        summary.AppendLine($"Region ID: {regionId}");
        summary.AppendLine($"Center: {region.Latitude:F6}, {region.Longitude:F6}");
        summary.AppendLine($"Size: {region.SizeMeters:F0} meters");
        summary.AppendLine($"Zoom Level: {region.ZoomLevel}");
        summary.AppendLine($"Status: {region.Status.ToString().ToLowerInvariant()}");
        summary.AppendLine();

        if (!string.IsNullOrEmpty(errorMessage))
        {
            summary.AppendLine("ERROR:");
            summary.AppendLine(errorMessage);
            summary.AppendLine();
        }

        summary.AppendLine("Processing Statistics:");
        summary.AppendLine($"- Tiles Downloaded: {tilesDownloaded}");
        summary.AppendLine($"- Tiles Reused from Cache: {tilesReused}");
        summary.AppendLine($"- Total Tiles: {tiles.Count}");
        summary.AppendLine($"- Processing Time: {processingTime:F2} seconds");
        summary.AppendLine($"- Started: {startTime:yyyy-MM-dd HH:mm:ss} UTC");

        if (region.Status == RegionStatus.Completed)
        {
            summary.AppendLine($"- Completed: {endTime:yyyy-MM-dd HH:mm:ss} UTC");
        }
        else
        {
            summary.AppendLine($"- Failed: {endTime:yyyy-MM-dd HH:mm:ss} UTC");
        }

        summary.AppendLine();
        summary.AppendLine("Files Created:");

        if (tiles.Count > 0)
        {
            summary.AppendLine($"- CSV: {Path.GetFileName(filePath).Replace("_summary.txt", "_ready.csv")}");
        }

        if (!string.IsNullOrEmpty(stitchedImagePath))
        {
            summary.AppendLine($"- Stitched Image: {Path.GetFileName(stitchedImagePath)}");
            summary.AppendLine($"- Stitched Image Path: {stitchedImagePath}");
        }

        summary.AppendLine($"- Summary: {Path.GetFileName(filePath)}");

        await File.WriteAllTextAsync(filePath, summary.ToString(), cancellationToken);
    }

    private static RegionStatusResponse MapToStatus(RegionEntity region)
    {
        return new RegionStatusResponse
        {
            Id = region.Id,
            Status = region.Status,
            CsvFilePath = region.CsvFilePath,
            SummaryFilePath = region.SummaryFilePath,
            TilesDownloaded = region.TilesDownloaded,
            TilesReused = region.TilesReused,
            CreatedAt = region.CreatedAt,
            UpdatedAt = region.UpdatedAt
        };
    }
}

