using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using SatelliteProvider.Common.Configs;
using SatelliteProvider.Common.DTO;
using SatelliteProvider.Common.Enums;
using SatelliteProvider.Common.Interfaces;
using SatelliteProvider.Common.Utils;
using SatelliteProvider.DataAccess.Models;
using SatelliteProvider.DataAccess.Repositories;

namespace SatelliteProvider.Services.TileDownloader;

public class TileService : ITileService
{
    private static readonly TimeSpan TileCacheAbsolute = TimeSpan.FromHours(1);
    private static readonly TimeSpan TileCacheSliding = TimeSpan.FromMinutes(30);
    private static readonly TimeSpan TileResponseMaxAge = TimeSpan.FromDays(1);
    private const string TileImageContentType = "image/jpeg";

    private readonly ISatelliteDownloader _downloader;
    private readonly ITileRepository _tileRepository;
    private readonly IMemoryCache _cache;
    private readonly MapConfig _mapConfig;
    private readonly ILogger<TileService> _logger;

    public TileService(
        ISatelliteDownloader downloader,
        ITileRepository tileRepository,
        IMemoryCache cache,
        IOptions<MapConfig> mapConfig,
        ILogger<TileService> logger)
    {
        _downloader = downloader;
        _tileRepository = tileRepository;
        _cache = cache;
        _mapConfig = mapConfig.Value;
        _logger = logger;
    }

    public async Task<List<TileMetadata>> DownloadAndStoreTilesAsync(
        double latitude,
        double longitude,
        double sizeMeters,
        int zoomLevel,
        CancellationToken cancellationToken = default)
    {
        var existingTiles = await _tileRepository.GetTilesByRegionAsync(latitude, longitude, sizeMeters, zoomLevel);
        var existingTilesList = existingTiles.ToList();

        var centerPoint = new GeoPoint(latitude, longitude);

        var existingTileInfos = existingTilesList
            .Select(t => new ExistingTileInfo(t.Latitude, t.Longitude, t.TileZoom))
            .ToList();

        var downloadedTiles = await _downloader.GetTilesWithMetadataAsync(
            centerPoint,
            sizeMeters / 2,
            zoomLevel,
            existingTileInfos,
            cancellationToken);

        var result = new List<TileMetadata>();

        foreach (var existingTile in existingTilesList)
        {
            result.Add(MapToMetadata(existingTile));
        }

        foreach (var downloadedTile in downloadedTiles)
        {
            var tileEntity = BuildTileEntity(downloadedTile);
            await _tileRepository.InsertAsync(tileEntity);
            result.Add(MapToMetadata(tileEntity));
        }

        return result;
    }

    public async Task<TileMetadata?> GetTileAsync(Guid id)
    {
        var tile = await _tileRepository.GetByIdAsync(id);
        return tile != null ? MapToMetadata(tile) : null;
    }

    public async Task<IEnumerable<TileMetadata>> GetTilesByRegionAsync(
        double latitude,
        double longitude,
        double sizeMeters,
        int zoomLevel)
    {
        var tiles = await _tileRepository.GetTilesByRegionAsync(latitude, longitude, sizeMeters, zoomLevel);
        return tiles.Select(MapToMetadata);
    }

    public async Task<TileBytes> GetOrDownloadTileAsync(int z, int x, int y, CancellationToken cancellationToken = default)
    {
        var cacheKey = $"tile_{z}_{x}_{y}";
        var etag = $"\"{z}_{x}_{y}\"";

        if (_cache.TryGetValue(cacheKey, out byte[]? cachedBytes) && cachedBytes != null)
        {
            return new TileBytes(cachedBytes, TileImageContentType, etag, TileResponseMaxAge);
        }

        string filePath;
        var existing = await _tileRepository.GetByTileCoordinatesAsync(z, x, y);
        if (existing != null && File.Exists(existing.FilePath))
        {
            filePath = existing.FilePath;
        }
        else
        {
            var tileCenter = GeoUtils.TileToWorldPos(x, y, z);
            var downloaded = await _downloader.DownloadSingleTileAsync(tileCenter.Lat, tileCenter.Lon, z, cancellationToken);
            var entity = BuildTileEntity(downloaded);
            await _tileRepository.InsertAsync(entity);
            filePath = entity.FilePath;
        }

        var bytes = await File.ReadAllBytesAsync(filePath, cancellationToken);
        _cache.Set(cacheKey, bytes, new MemoryCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TileCacheAbsolute,
            SlidingExpiration = TileCacheSliding
        });

        return new TileBytes(bytes, TileImageContentType, etag, TileResponseMaxAge);
    }

    public async Task<TileMetadata> DownloadAndStoreSingleTileAsync(
        double latitude,
        double longitude,
        int zoomLevel,
        CancellationToken cancellationToken = default)
    {
        var downloaded = await _downloader.DownloadSingleTileAsync(latitude, longitude, zoomLevel, cancellationToken);
        var entity = BuildTileEntity(downloaded);
        await _tileRepository.InsertAsync(entity);
        return MapToMetadata(entity);
    }

    private TileEntity BuildTileEntity(DownloadedTileInfoV2 downloaded)
    {
        var now = DateTime.UtcNow;
        return new TileEntity
        {
            Id = Guid.NewGuid(),
            TileZoom = downloaded.ZoomLevel,
            TileX = downloaded.X,
            TileY = downloaded.Y,
            Latitude = downloaded.CenterLatitude,
            Longitude = downloaded.CenterLongitude,
            TileSizeMeters = downloaded.TileSizeMeters,
            TileSizePixels = _mapConfig.TileSizePixels,
            ImageType = "jpg",
            MapsVersion = null,
            Version = null,
            FilePath = downloaded.FilePath,
            Source = TileSourceConverter.ToWireValue(TileSource.GoogleMaps),
            CapturedAt = now,
            CreatedAt = now,
            UpdatedAt = now
        };
    }

    private static TileMetadata MapToMetadata(TileEntity entity)
    {
        return new TileMetadata
        {
            Id = entity.Id,
            TileZoom = entity.TileZoom,
            TileX = entity.TileX,
            TileY = entity.TileY,
            Latitude = entity.Latitude,
            Longitude = entity.Longitude,
            TileSizeMeters = entity.TileSizeMeters,
            TileSizePixels = entity.TileSizePixels,
            ImageType = entity.ImageType,
            Version = entity.Version,
            FilePath = entity.FilePath,
            CreatedAt = entity.CreatedAt,
            UpdatedAt = entity.UpdatedAt
        };
    }
}
