<?php

namespace App\Services\Forecasting;

use App\Services\HaversineQuery;
use Illuminate\Support\Facades\DB;

/**
 * Layer 2 — descriptive snapshot of the present.
 *
 * Pure SQL aggregates against `station_prices_current` + Haversine on
 * `stations.lat / lng`. No ML, no history, no surprises. Layer 2 never
 * speaks about the future.
 *
 * Used by Phase 4's WeeklyForecastService to enrich the public payload
 * with descriptive "your area" cards alongside the headline forecast.
 */
final class LocalSnapshotService
{
    /**
     * Snapshot for a coordinate (e.g. user's postcode-resolved lat/lng).
     *
     * @return array{
     *   national_avg_pence: ?float,
     *   local_avg_pence: ?float,
     *   local_minus_national_pence: ?float,
     *   cheapest_nearby: array<int, array{node_id: string, name: ?string, brand: ?string, price_pence: int, distance_km: float}>,
     *   supermarket_avg_pence: ?float,
     *   major_avg_pence: ?float,
     *   supermarket_gap_pence: ?float,
     *   stations_within_radius: int
     * }
     */
    public function snapshot(string $fuelType, float $lat, float $lng, int $radiusKm = 25): array
    {
        $nationalAvg = $this->nationalAverage($fuelType);
        $localAvg = $this->localAverage($fuelType, $lat, $lng, 50);
        $cheapest = $this->cheapestNearby($fuelType, $lat, $lng, $radiusKm, 5);
        [$superAvg, $majorAvg] = $this->brandSplit($fuelType, $lat, $lng, $radiusKm);
        $stationCount = $this->stationCountWithin($fuelType, $lat, $lng, $radiusKm);

        return [
            'national_avg_pence' => $nationalAvg,
            'local_avg_pence' => $localAvg,
            'local_minus_national_pence' => $localAvg !== null && $nationalAvg !== null
                ? round($localAvg - $nationalAvg, 1)
                : null,
            'cheapest_nearby' => $cheapest,
            'supermarket_avg_pence' => $superAvg,
            'major_avg_pence' => $majorAvg,
            'supermarket_gap_pence' => $superAvg !== null && $majorAvg !== null
                ? round($superAvg - $majorAvg, 1)
                : null,
            'stations_within_radius' => $stationCount,
        ];
    }

    private function nationalAverage(string $fuelType): ?float
    {
        $avg = DB::table('station_prices_current')
            ->where('fuel_type', $fuelType)
            ->avg('price_pence');

        return $avg === null ? null : round((float) $avg / 100, 1);
    }

    private function localAverage(string $fuelType, float $lat, float $lng, int $km): ?float
    {
        [$within, $bindings] = HaversineQuery::withinKm($lat, $lng, $km);

        $avg = DB::table('station_prices_current')
            ->join('stations', 'station_prices_current.station_id', '=', 'stations.node_id')
            ->where('station_prices_current.fuel_type', $fuelType)
            ->whereRaw($within, $bindings)
            ->avg('station_prices_current.price_pence');

        return $avg === null ? null : round((float) $avg / 100, 1);
    }

    /**
     * @return array<int, array{node_id: string, name: ?string, brand: ?string, price_pence: int, distance_km: float}>
     */
    private function cheapestNearby(string $fuelType, float $lat, float $lng, int $km, int $limit): array
    {
        [$distance, $distanceBindings] = HaversineQuery::distanceKm($lat, $lng);
        [$within, $withinBindings] = HaversineQuery::withinKm($lat, $lng, $km);

        $rows = DB::table('station_prices_current')
            ->join('stations', 'station_prices_current.station_id', '=', 'stations.node_id')
            ->where('station_prices_current.fuel_type', $fuelType)
            ->whereRaw($within, $withinBindings)
            ->selectRaw(
                'stations.node_id, stations.trading_name as name, stations.brand_name as brand, '
                .'station_prices_current.price_pence, '.$distance.' as distance_km',
                $distanceBindings,
            )
            ->orderBy('station_prices_current.price_pence')
            ->limit($limit)
            ->get();

        return $rows->map(fn ($r): array => [
            'node_id' => (string) $r->node_id,
            'name' => $r->name === null ? null : (string) $r->name,
            'brand' => $r->brand === null ? null : (string) $r->brand,
            'price_pence' => (int) $r->price_pence,
            'distance_km' => round((float) $r->distance_km, 2),
        ])->all();
    }

    /** @return array{0: ?float, 1: ?float} [supermarket_avg, major_avg] */
    private function brandSplit(string $fuelType, float $lat, float $lng, int $km): array
    {
        [$within, $bindings] = HaversineQuery::withinKm($lat, $lng, $km);

        $rows = DB::table('station_prices_current')
            ->join('stations', 'station_prices_current.station_id', '=', 'stations.node_id')
            ->where('station_prices_current.fuel_type', $fuelType)
            ->whereRaw($within, $bindings)
            ->selectRaw('stations.is_supermarket, AVG(station_prices_current.price_pence) as avg_pence')
            ->groupBy('stations.is_supermarket')
            ->get();

        $super = null;
        $major = null;
        foreach ($rows as $r) {
            $avg = round((float) $r->avg_pence / 100, 1);
            if ((int) $r->is_supermarket === 1) {
                $super = $avg;
            } else {
                $major = $avg;
            }
        }

        return [$super, $major];
    }

    private function stationCountWithin(string $fuelType, float $lat, float $lng, int $km): int
    {
        [$within, $bindings] = HaversineQuery::withinKm($lat, $lng, $km);

        return DB::table('station_prices_current')
            ->join('stations', 'station_prices_current.station_id', '=', 'stations.node_id')
            ->where('station_prices_current.fuel_type', $fuelType)
            ->whereRaw($within, $bindings)
            ->count();
    }
}
