using System.Diagnostics;
using Dapper;
using Microsoft.Extensions.Logging;
using Npgsql;
using SatelliteProvider.Common.Configs;
using SatelliteProvider.Common.Utils;
using SatelliteProvider.DataAccess.Models;

namespace SatelliteProvider.DataAccess.Repositories;

public class TileRepository : ITileRepository
{
    private const int SlowQueryThresholdMs = 500;

    private const string ColumnList = @"id, tile_zoom as TileZoom, tile_x as TileX, tile_y as TileY,
                   latitude, longitude,
                   tile_size_meters as TileSizeMeters, tile_size_pixels as TileSizePixels,
                   image_type as ImageType, maps_version as MapsVersion, version,
                   file_path as FilePath, source, captured_at as CapturedAt,
                   created_at as CreatedAt, updated_at as UpdatedAt";

    private readonly string _connectionString;
    private readonly ILogger<TileRepository> _logger;

    public TileRepository(string connectionString, ILogger<TileRepository> logger)
    {
        _connectionString = connectionString;
        _logger = logger;
    }

    public async Task<TileEntity?> GetByIdAsync(Guid id)
    {
        using var connection = new NpgsqlConnection(_connectionString);
        const string sql = $@"
            SELECT {ColumnList}
            FROM tiles
            WHERE id = @Id";

        return await connection.QuerySingleOrDefaultAsync<TileEntity>(sql, new { Id = id });
    }

    public async Task<TileEntity?> GetByTileCoordinatesAsync(int tileZoom, int tileX, int tileY)
    {
        using var connection = new NpgsqlConnection(_connectionString);
        // AZ-484 selection rule: most-recent across sources, deterministic tie-break on
        // (captured_at DESC, updated_at DESC, id DESC).
        const string sql = $@"
            SELECT {ColumnList}
            FROM tiles
            WHERE tile_zoom = @TileZoom AND tile_x = @TileX AND tile_y = @TileY
            ORDER BY captured_at DESC, updated_at DESC, id DESC
            LIMIT 1";

        return await connection.QuerySingleOrDefaultAsync<TileEntity>(sql, new { TileZoom = tileZoom, TileX = tileX, TileY = tileY });
    }

    public async Task<IEnumerable<TileEntity>> GetTilesByRegionAsync(double latitude, double longitude, double sizeMeters, int zoomLevel)
    {
        using var connection = new NpgsqlConnection(_connectionString);

        var latRad = latitude * Math.PI / 180.0;
        var metersPerPixel = (GeoUtils.EarthEquatorialCircumferenceMeters * Math.Cos(latRad)) / (Math.Pow(2, zoomLevel) * MapConfig.DefaultTileSizePixels);
        var tileSizeMeters = metersPerPixel * MapConfig.DefaultTileSizePixels;

        var expandedSizeMeters = sizeMeters + (tileSizeMeters * 2);

        var latRange = expandedSizeMeters / GeoUtils.MetersPerDegreeLatitude;
        var lonRange = expandedSizeMeters / (GeoUtils.MetersPerDegreeLatitude * Math.Cos(latitude * Math.PI / 180.0));

        // AZ-484 selection rule: at most one row per (lat, lon, zoom, size) cell, picking
        // the most-recent across sources via DISTINCT ON, with deterministic tie-break on
        // (captured_at DESC, updated_at DESC, id DESC). The outer ORDER BY restores the
        // pre-AZ-484 caller-facing order (latitude DESC, longitude ASC); the pre-AZ-484
        // updated_at DESC tiebreak is unreachable here because DISTINCT ON already
        // guarantees one row per (latitude, longitude, ...) tuple.
        const string sql = $@"
            SELECT * FROM (
                SELECT DISTINCT ON (latitude, longitude, tile_zoom, tile_size_meters)
                       {ColumnList}
                FROM tiles
                WHERE latitude BETWEEN @MinLat AND @MaxLat
                  AND longitude BETWEEN @MinLon AND @MaxLon
                  AND tile_zoom = @TileZoom
                ORDER BY latitude, longitude, tile_zoom, tile_size_meters,
                         captured_at DESC, updated_at DESC, id DESC
            ) deduped
            ORDER BY latitude DESC, longitude ASC";

        var stopwatch = Stopwatch.StartNew();
        var tiles = await connection.QueryAsync<TileEntity>(sql, new
        {
            MinLat = latitude - latRange / 2,
            MaxLat = latitude + latRange / 2,
            MinLon = longitude - lonRange / 2,
            MaxLon = longitude + lonRange / 2,
            TileZoom = zoomLevel
        });
        stopwatch.Stop();

        if (stopwatch.ElapsedMilliseconds > SlowQueryThresholdMs)
        {
            _logger.LogWarning(
                "Slow GetTilesByRegionAsync: {ElapsedMs} ms (threshold {ThresholdMs} ms) for lat={Latitude}, lon={Longitude}, sizeMeters={SizeMeters}, zoom={ZoomLevel}",
                stopwatch.ElapsedMilliseconds, SlowQueryThresholdMs, latitude, longitude, sizeMeters, zoomLevel);
        }

        return tiles;
    }

    public async Task<Guid> InsertAsync(TileEntity tile)
    {
        using var connection = new NpgsqlConnection(_connectionString);
        // AZ-484: per-source UPSERT — conflict key now includes `source` so that two
        // producers (e.g. google_maps + uav) can coexist for the same cell. A re-insert
        // for the SAME source updates file_path / tile_x / tile_y plus refreshes
        // captured_at and updated_at to reflect the new acquisition.
        const string sql = @"
            INSERT INTO tiles (id, tile_zoom, tile_x, tile_y, latitude, longitude, tile_size_meters,
                             tile_size_pixels, image_type, maps_version, version, file_path,
                             source, captured_at, created_at, updated_at)
            VALUES (@Id, @TileZoom, @TileX, @TileY, @Latitude, @Longitude, @TileSizeMeters,
                    @TileSizePixels, @ImageType, @MapsVersion, @Version, @FilePath,
                    @Source, @CapturedAt, @CreatedAt, @UpdatedAt)
            ON CONFLICT (latitude, longitude, tile_zoom, tile_size_meters, source)
            DO UPDATE SET
                file_path = EXCLUDED.file_path,
                tile_x = EXCLUDED.tile_x,
                tile_y = EXCLUDED.tile_y,
                captured_at = EXCLUDED.captured_at,
                updated_at = EXCLUDED.updated_at
            RETURNING id";

        return await connection.ExecuteScalarAsync<Guid>(sql, tile);
    }

    public async Task<int> UpdateAsync(TileEntity tile)
    {
        using var connection = new NpgsqlConnection(_connectionString);
        const string sql = @"
            UPDATE tiles
            SET tile_zoom = @TileZoom,
                tile_x = @TileX,
                tile_y = @TileY,
                latitude = @Latitude,
                longitude = @Longitude,
                tile_size_meters = @TileSizeMeters,
                tile_size_pixels = @TileSizePixels,
                image_type = @ImageType,
                maps_version = @MapsVersion,
                version = @Version,
                file_path = @FilePath,
                source = @Source,
                captured_at = @CapturedAt,
                updated_at = @UpdatedAt
            WHERE id = @Id";

        return await connection.ExecuteAsync(sql, tile);
    }

    public async Task<int> DeleteAsync(Guid id)
    {
        using var connection = new NpgsqlConnection(_connectionString);
        const string sql = "DELETE FROM tiles WHERE id = @Id";
        return await connection.ExecuteAsync(sql, new { Id = id });
    }
}
