<?php

namespace App\Services;

use App\Enums\FuelType;
use App\Models\StationPriceCurrent;
use App\Services\Prediction\Signals\BrandBehaviourSignal;
use App\Services\Prediction\Signals\DayOfWeekSignal;
use App\Services\Prediction\Signals\OilSignal;
use App\Services\Prediction\Signals\RegionalMomentumSignal;
use App\Services\Prediction\Signals\SignalContext;
use App\Services\Prediction\Signals\StickinessSignal;
use App\Services\Prediction\Signals\TrendSignal;
use Carbon\CarbonInterface;
use Illuminate\Support\Facades\DB;

class NationalFuelPredictionService
{
    private const float SLOPE_THRESHOLD_PENCE = 0.3;

    private const int PREDICTION_HORIZON_DAYS = 7;

    public function __construct(
        private readonly TrendSignal $trendSignal,
        private readonly DayOfWeekSignal $dayOfWeekSignal,
        private readonly BrandBehaviourSignal $brandBehaviourSignal,
        private readonly StickinessSignal $stickinessSignal,
        private readonly RegionalMomentumSignal $regionalMomentumSignal,
        private readonly OilSignal $oilSignal,
    ) {}

    /**
     * @return array{
     *     fuel_type: string,
     *     current_avg: float,
     *     predicted_direction: string,
     *     predicted_change_pence: float,
     *     confidence_score: float,
     *     confidence_label: string,
     *     action: string,
     *     reasoning: string,
     *     prediction_horizon_days: int,
     *     region_key: string,
     *     methodology: string,
     *     signals: array
     * }
     */
    public function predict(?float $lat = null, ?float $lng = null): array
    {
        $fuelType = FuelType::E10;
        $hasCoordinates = $lat !== null && $lng !== null;
        $context = new SignalContext($fuelType, $lat, $lng);

        $currentAvg = $this->getCurrentAverage($fuelType, $lat, $lng);
        $trend = $this->trendSignal->compute($context);
        $dayOfWeek = $this->dayOfWeekSignal->compute($context);
        $brandBehaviour = $this->brandBehaviourSignal->compute($context);
        $stickiness = $this->stickinessSignal->compute($context);
        $oil = $this->oilSignal->compute($context);

        $nationalMomentum = $this->disabledSignal('National momentum disabled for national predictions');
        $regionalMomentum = $this->regionalMomentumSignal->compute($context);

        $signals = compact('trend', 'dayOfWeek', 'brandBehaviour', 'nationalMomentum', 'regionalMomentum', 'stickiness', 'oil');

        [$direction, $confidenceScore] = $this->aggregateSignals($signals, $hasCoordinates);

        $slope = $trend['slope'] ?? 0.0;
        $predictedChangePence = round($slope * self::PREDICTION_HORIZON_DAYS, 1);

        $confidenceLabel = match (true) {
            $confidenceScore >= 70 => 'high',
            $confidenceScore >= 40 => 'medium',
            default => 'low',
        };

        $action = match ($direction) {
            'up' => 'fill_now',
            'down' => 'wait',
            default => 'no_signal',
        };

        $weeklySummary = $this->computeWeeklySummary($fuelType, $lat, $lng, $currentAvg, $slope);

        return [
            'fuel_type' => $fuelType->value,
            'current_avg' => $currentAvg,
            'predicted_direction' => $direction,
            'predicted_change_pence' => $predictedChangePence,
            'confidence_score' => $confidenceScore,
            'confidence_label' => $confidenceLabel,
            'action' => $action,
            'reasoning' => $this->buildReasoning($direction, $slope, $trend, $brandBehaviour, $dayOfWeek),
            'prediction_horizon_days' => self::PREDICTION_HORIZON_DAYS,
            'region_key' => $hasCoordinates ? 'regional' : 'national',
            'methodology' => 'multi_signal_live_fallback',
            'weekly_summary' => $weeklySummary,
            'signals' => [
                'trend' => $trend,
                'day_of_week' => $dayOfWeek,
                'brand_behaviour' => $brandBehaviour,
                'national_momentum' => $nationalMomentum,
                'regional_momentum' => $regionalMomentum,
                'price_stickiness' => $stickiness,
                'oil' => $oil,
            ],
        ];
    }

    private function getCurrentAverage(FuelType $fuelType, ?float $lat, ?float $lng): float
    {
        if ($lat !== null && $lng !== null) {
            [$radiusSql, $radiusBindings] = HaversineQuery::withinKm($lat, $lng, 50);

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

            if ($avg !== null) {
                return round((float) $avg / 100, 1);
            }
        }

        $avg = StationPriceCurrent::where('fuel_type', $fuelType->value)->avg('price_pence');

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

    /** @return array{score: float, confidence: float, direction: string, detail: string, data_points: int, enabled: bool} */
    private function disabledSignal(string $detail): array
    {
        return [
            'score' => 0.0,
            'confidence' => 0.0,
            'direction' => 'stable',
            'detail' => $detail,
            'data_points' => 0,
            'enabled' => false,
        ];
    }

    /**
     * Aggregate enabled signals into a final direction + confidence score.
     *
     * Direction: weighted vote across signals that have a non-stable direction.
     *   stable signals do NOT dilute the directional vote.
     *
     * Confidence: weighted average of enabled signals' own confidence values,
     *   multiplied by an agreement coefficient (0..1) measuring how the signals
     *   line up with the chosen direction.
     *
     * @param  array<string, array{score: float, confidence: float, direction: string, enabled: bool}>  $signals
     * @return array{0: string, 1: float}
     */
    private function aggregateSignals(array $signals, bool $hasCoordinates = false): array
    {
        $weights = $hasCoordinates
            ? [
                'regionalMomentum' => 0.35,
                'oil' => 0.20,
                'trend' => 0.15,
                'dayOfWeek' => 0.15,
                'brandBehaviour' => 0.10,
                'stickiness' => 0.05,
            ]
            : [
                'trend' => 0.30,
                'oil' => 0.25,
                'dayOfWeek' => 0.20,
                'brandBehaviour' => 0.15,
                'stickiness' => 0.10,
            ];

        $directionalScoreSum = 0.0;
        $directionalWeightSum = 0.0;
        $confidenceWeightedSum = 0.0;
        $totalEnabledWeight = 0.0;

        foreach ($weights as $key => $weight) {
            $signal = $signals[$key] ?? null;
            if (! $signal || ! $signal['enabled']) {
                continue;
            }

            $totalEnabledWeight += $weight;
            $confidenceWeightedSum += $signal['confidence'] * $weight;

            if ($signal['direction'] !== 'stable') {
                $directionalScoreSum += $signal['score'] * $signal['confidence'] * $weight;
                $directionalWeightSum += $weight;
            }
        }

        if ($totalEnabledWeight < 0.01) {
            return ['stable', 0.0];
        }

        $normalised = $directionalWeightSum > 0.01
            ? $directionalScoreSum / $directionalWeightSum
            : 0.0;

        $direction = match (true) {
            $normalised >= 0.1 => 'up',
            $normalised <= -0.1 => 'down',
            default => 'stable',
        };

        $avgConfidence = $confidenceWeightedSum / $totalEnabledWeight;
        $agreement = $this->computeAgreement($signals, $weights, $direction);

        $confidenceScore = round(min(100.0, $avgConfidence * $agreement * 100), 1);

        return [$direction, $confidenceScore];
    }

    /**
     * How well the enabled signals line up with the chosen direction.
     *   - aligned signal: full credit (signal_confidence × weight)
     *   - one side stable, other directional: half credit
     *   - opposing signals: no credit
     *
     * Range: 0 (full disagreement) → 1 (unanimous).
     *
     * @param  array<string, array{confidence: float, direction: string, enabled: bool}>  $signals
     * @param  array<string, float>  $weights
     */
    private function computeAgreement(array $signals, array $weights, string $finalDirection): float
    {
        $finalDir = match ($finalDirection) {
            'up' => 1,
            'down' => -1,
            default => 0,
        };

        $credit = 0.0;
        $maxCredit = 0.0;

        foreach ($weights as $key => $weight) {
            $signal = $signals[$key] ?? null;
            if (! $signal || ! $signal['enabled']) {
                continue;
            }

            $maxCredit += $signal['confidence'] * $weight;

            $signalDir = match ($signal['direction']) {
                'up' => 1,
                'down' => -1,
                default => 0,
            };

            if ($signalDir === $finalDir) {
                $credit += $signal['confidence'] * $weight;
            } elseif ($signalDir === 0 || $finalDir === 0) {
                $credit += 0.5 * $signal['confidence'] * $weight;
            }
        }

        return $maxCredit > 0.0 ? $credit / $maxCredit : 0.0;
    }

    /**
     * Yesterday / today / tomorrow snapshot + last-7-days series.
     * Regional (50km) when coordinates are given, with national fallback when
     * regional data is empty.
     *
     * @return array{
     *   yesterday_avg: ?float,
     *   today_avg: float,
     *   tomorrow_estimated_avg: ?float,
     *   yesterday_today_delta_pence: ?float,
     *   last_7_days_series: array<int, array{date: string, avg: float}>,
     *   last_7_days_change_pence: ?float,
     *   cheapest_day: ?array{date: string, avg: float},
     *   priciest_day: ?array{date: string, avg: float},
     *   is_regional: bool
     * }
     */
    private function computeWeeklySummary(FuelType $fuelType, ?float $lat, ?float $lng, float $todayAvg, float $slope): array
    {
        $yesterdayAvg = $this->getDailyAverage($fuelType, now()->subDay(), $lat, $lng);
        [$series, $usedRegional] = $this->getDailySeries($fuelType, 7, $lat, $lng);

        $tomorrowEstimated = $todayAvg > 0.0 ? round($todayAvg + $slope, 1) : null;
        $yesterdayTodayDelta = $yesterdayAvg !== null ? round($todayAvg - $yesterdayAvg, 1) : null;

        $cheapestDay = null;
        $priciestDay = null;
        $weekChange = null;

        if (count($series) >= 2) {
            $byPrice = $series;
            usort($byPrice, fn ($a, $b) => $a['avg'] <=> $b['avg']);
            $cheapestDay = $byPrice[0];
            $priciestDay = $byPrice[count($byPrice) - 1];
            $weekChange = round(end($series)['avg'] - $series[0]['avg'], 1);
        }

        return [
            'yesterday_avg' => $yesterdayAvg,
            'today_avg' => $todayAvg,
            'tomorrow_estimated_avg' => $tomorrowEstimated,
            'yesterday_today_delta_pence' => $yesterdayTodayDelta,
            'last_7_days_series' => $series,
            'last_7_days_change_pence' => $weekChange,
            'cheapest_day' => $cheapestDay,
            'priciest_day' => $priciestDay,
            'is_regional' => $usedRegional,
        ];
    }

    private function getDailyAverage(FuelType $fuelType, CarbonInterface $date, ?float $lat, ?float $lng): ?float
    {
        $dateString = $date->toDateString();

        if ($lat !== null && $lng !== null) {
            [$radiusSql, $radiusBindings] = HaversineQuery::withinKm($lat, $lng, 50);

            $regional = DB::table('station_prices')
                ->join('stations', 'station_prices.station_id', '=', 'stations.node_id')
                ->where('station_prices.fuel_type', $fuelType->value)
                ->whereDate('station_prices.price_effective_at', $dateString)
                ->whereRaw($radiusSql, $radiusBindings)
                ->avg('station_prices.price_pence');

            if ($regional !== null) {
                return round((float) $regional / 100, 1);
            }
        }

        $national = DB::table('station_prices')
            ->where('fuel_type', $fuelType->value)
            ->whereDate('price_effective_at', $dateString)
            ->avg('price_pence');

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

    /**
     * @return array{0: array<int, array{date: string, avg: float}>, 1: bool}
     */
    private function getDailySeries(FuelType $fuelType, int $days, ?float $lat, ?float $lng): array
    {
        $rows = collect();
        $usedRegional = false;

        if ($lat !== null && $lng !== null) {
            [$radiusSql, $radiusBindings] = HaversineQuery::withinKm($lat, $lng, 50);

            $rows = DB::table('station_prices')
                ->join('stations', 'station_prices.station_id', '=', 'stations.node_id')
                ->where('station_prices.fuel_type', $fuelType->value)
                ->where('station_prices.price_effective_at', '>=', now()->subDays($days)->startOfDay())
                ->whereRaw($radiusSql, $radiusBindings)
                ->selectRaw('DATE(station_prices.price_effective_at) as day, AVG(station_prices.price_pence) as avg_price')
                ->groupBy('day')
                ->orderBy('day')
                ->get();

            $usedRegional = $rows->isNotEmpty();
        }

        if ($rows->isEmpty()) {
            $rows = DB::table('station_prices')
                ->where('fuel_type', $fuelType->value)
                ->where('price_effective_at', '>=', now()->subDays($days)->startOfDay())
                ->selectRaw('DATE(price_effective_at) as day, AVG(price_pence) as avg_price')
                ->groupBy('day')
                ->orderBy('day')
                ->get();
        }

        $series = $rows->map(fn ($r): array => [
            'date' => (string) $r->day,
            'avg' => round((float) $r->avg_price / 100, 1),
        ])->values()->all();

        return [$series, $usedRegional];
    }

    /**
     * @param  array{enabled: bool, detail: string, direction: string}  $trend
     * @param  array{enabled: bool, detail: string, direction: string}  $brandBehaviour
     * @param  array{enabled: bool, detail: string, direction: string}  $dayOfWeek
     */
    private function buildReasoning(string $direction, float $slope, array $trend, array $brandBehaviour, array $dayOfWeek): string
    {
        $parts = [];

        if ($trend['enabled'] && abs($slope) >= self::SLOPE_THRESHOLD_PENCE) {
            $parts[] = $trend['detail'];
        }

        if ($brandBehaviour['enabled'] && $brandBehaviour['direction'] !== 'stable') {
            $parts[] = $brandBehaviour['detail'];
        }

        if ($dayOfWeek['enabled']) {
            $parts[] = $dayOfWeek['detail'];
        }

        if (empty($parts)) {
            return match ($direction) {
                'up' => 'Mild upward signals — top up soon if you\'re nearby.',
                'down' => 'Mild downward signals — wait a day or two if your tank can hold.',
                default => 'No clear pattern — fill up at the cheapest station near you now.',
            };
        }

        return implode(' ', $parts);
    }
}
