using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using SatelliteProvider.Common.Configs;
using SatelliteProvider.Common.DTO;
using SatelliteProvider.Common.Enums;
using SatelliteProvider.Common.Utils;
using SatelliteProvider.DataAccess.Models;
using SatelliteProvider.DataAccess.Repositories;

namespace SatelliteProvider.Services.TileDownloader;

public interface IUavTileUploadHandler
{
    Task<UavTileUploadHandlerResult> HandleAsync(string metadataJson, IReadOnlyList<UavUploadFile> files, CancellationToken cancellationToken = default);
}

public sealed record UavUploadFile(string FileName, string? ContentType, ReadOnlyMemory<byte> Content);

public sealed record UavTileUploadHandlerResult(bool EnvelopeRejected, string? EnvelopeError, UavTileBatchUploadResponse? Response);

public sealed class UavTileUploadHandler : IUavTileUploadHandler
{
    private const string UavTileFileExtension = ".jpg";
    private const string UavTileSubdirectory = "uav";

    private readonly IUavTileQualityGate _qualityGate;
    private readonly ITileRepository _tileRepository;
    private readonly StorageConfig _storageConfig;
    private readonly MapConfig _mapConfig;
    private readonly UavQualityConfig _qualityConfig;
    private readonly TimeProvider _timeProvider;
    private readonly ILogger<UavTileUploadHandler> _logger;

    public UavTileUploadHandler(
        IUavTileQualityGate qualityGate,
        ITileRepository tileRepository,
        IOptions<StorageConfig> storageConfig,
        IOptions<MapConfig> mapConfig,
        IOptions<UavQualityConfig> qualityConfig,
        ILogger<UavTileUploadHandler> logger,
        TimeProvider? timeProvider = null)
    {
        _qualityGate = qualityGate ?? throw new ArgumentNullException(nameof(qualityGate));
        _tileRepository = tileRepository ?? throw new ArgumentNullException(nameof(tileRepository));
        _storageConfig = storageConfig?.Value ?? throw new ArgumentNullException(nameof(storageConfig));
        _mapConfig = mapConfig?.Value ?? throw new ArgumentNullException(nameof(mapConfig));
        _qualityConfig = qualityConfig?.Value ?? throw new ArgumentNullException(nameof(qualityConfig));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _timeProvider = timeProvider ?? TimeProvider.System;
    }

    private static readonly JsonSerializerOptions MetadataJsonOptions = new()
    {
        PropertyNameCaseInsensitive = true,
    };

    public async Task<UavTileUploadHandlerResult> HandleAsync(string metadataJson, IReadOnlyList<UavUploadFile> files, CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(files);

        if (string.IsNullOrWhiteSpace(metadataJson))
        {
            return EnvelopeError("Request `metadata` field is required.");
        }

        UavTileBatchMetadataPayload? payload;
        try
        {
            payload = JsonSerializer.Deserialize<UavTileBatchMetadataPayload>(metadataJson, MetadataJsonOptions);
        }
        catch (JsonException ex)
        {
            return EnvelopeError($"Invalid `metadata` JSON: {ex.Message}");
        }

        if (payload is null || payload.Items.Count == 0)
        {
            return EnvelopeError("Request `metadata.items` is required and must contain at least one entry.");
        }

        if (payload.Items.Count != files.Count)
        {
            return EnvelopeError($"Mismatched batch: metadata has {payload.Items.Count} entries but {files.Count} file(s) were uploaded.");
        }

        if (payload.Items.Count > _qualityConfig.MaxBatchSize)
        {
            return EnvelopeError($"Batch size {payload.Items.Count} exceeds the configured maximum of {_qualityConfig.MaxBatchSize}.");
        }

        var response = new UavTileBatchUploadResponse();
        for (var index = 0; index < payload.Items.Count; index++)
        {
            cancellationToken.ThrowIfCancellationRequested();

            var metadata = payload.Items[index];
            var file = files[index];

            var gateResult = _qualityGate.Validate(file.Content, file.ContentType, metadata);
            if (!gateResult.Accepted)
            {
                response.Items.Add(new UavTileUploadResultItem
                {
                    Index = index,
                    Status = UavTileUploadStatus.Rejected,
                    RejectReason = gateResult.Reason,
                    RejectDetails = gateResult.Details,
                });
                continue;
            }

            try
            {
                var persistedId = await PersistAsync(metadata, file.Content, cancellationToken);
                response.Items.Add(new UavTileUploadResultItem
                {
                    Index = index,
                    Status = UavTileUploadStatus.Accepted,
                    TileId = persistedId,
                });
            }
            catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
            {
                _logger.LogError(ex, "UAV tile persistence failed at index {Index}", index);
                response.Items.Add(new UavTileUploadResultItem
                {
                    Index = index,
                    Status = UavTileUploadStatus.Rejected,
                    RejectReason = UavTileRejectReasons.StorageFailure,
                });
            }
        }

        return new UavTileUploadHandlerResult(EnvelopeRejected: false, EnvelopeError: null, Response: response);
    }

    private async Task<Guid> PersistAsync(UavTileMetadata metadata, ReadOnlyMemory<byte> imageBytes, CancellationToken cancellationToken)
    {
        var (tileX, tileY) = GeoUtils.WorldToTilePos(new GeoPoint(metadata.Latitude, metadata.Longitude), metadata.TileZoom);
        var filePath = BuildUavTileFilePath(_storageConfig, metadata.TileZoom, tileX, tileY);

        var directory = Path.GetDirectoryName(filePath);
        if (!string.IsNullOrEmpty(directory))
        {
            Directory.CreateDirectory(directory);
        }

        // File-first, row-second so a crash leaves an orphan file rather than a row
        // pointing at nothing (Risk 2 in the AZ-488 task spec).
        await File.WriteAllBytesAsync(filePath, imageBytes.ToArray(), cancellationToken);

        var capturedAtUtc = metadata.CapturedAt.Kind == DateTimeKind.Utc
            ? metadata.CapturedAt
            : metadata.CapturedAt.ToUniversalTime();
        var now = _timeProvider.GetUtcNow().UtcDateTime;

        var entity = new TileEntity
        {
            Id = Guid.NewGuid(),
            TileZoom = metadata.TileZoom,
            TileX = tileX,
            TileY = tileY,
            Latitude = metadata.Latitude,
            Longitude = metadata.Longitude,
            TileSizeMeters = metadata.TileSizeMeters,
            TileSizePixels = _mapConfig.TileSizePixels,
            ImageType = "jpg",
            MapsVersion = null,
            Version = null,
            FilePath = filePath,
            Source = TileSourceConverter.ToWireValue(TileSource.Uav),
            CapturedAt = capturedAtUtc,
            CreatedAt = now,
            UpdatedAt = now,
        };

        return await _tileRepository.InsertAsync(entity);
    }

    public static string BuildUavTileFilePath(StorageConfig storageConfig, int tileZoom, int tileX, int tileY)
    {
        ArgumentNullException.ThrowIfNull(storageConfig);
        return Path.Combine(
            storageConfig.TilesDirectory,
            UavTileSubdirectory,
            tileZoom.ToString(System.Globalization.CultureInfo.InvariantCulture),
            tileX.ToString(System.Globalization.CultureInfo.InvariantCulture),
            tileY.ToString(System.Globalization.CultureInfo.InvariantCulture) + UavTileFileExtension);
    }

    private static UavTileUploadHandlerResult EnvelopeError(string detail) =>
        new(EnvelopeRejected: true, EnvelopeError: detail, Response: null);
}
