<?php

namespace App\Services;

use App\Enums\PredictionSource;
use App\Enums\TrendDirection;
use App\Models\BrentPrice;
use App\Models\PricePrediction;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Throwable;

class OilPriceService
{
    /**
     * Decay factor for EWMA. Higher = more weight on recent prices.
     */
    private const float EWMA_ALPHA = 0.3;

    /**
     * Minimum % change in EWMA to be considered rising/falling.
     */
    private const float EWMA_THRESHOLD_PCT = 1.5;

    /**
     * EWMA confidence is capped lower than LLM — it's a simpler model.
     */
    private const int EWMA_MAX_CONFIDENCE = 65;

    /**
     * LLM confidence is capped — no model should be certain about oil prices.
     */
    private const int LLM_MAX_CONFIDENCE = 85;

    /**
     * Minimum price rows needed before EWMA is meaningful.
     */
    private const int EWMA_MIN_ROWS = 14;

    public function __construct(
        private readonly ApiLogger $apiLogger,
    ) {}

    /**
     * Fetch the last 30 days of Brent crude prices from FRED and store them.
     */
    public function fetchBrentPrices(): void
    {
        $url = 'https://api.stlouisfed.org/fred/series/observations';

        try {
            $response = $this->apiLogger->send('fred', 'GET', $url, fn () => Http::timeout(10)
                ->get($url, [
                    'series_id' => 'DCOILBRENTEU',
                    'api_key' => config('services.fred.api_key'),
                    'sort_order' => 'desc',
                    'limit' => 30,
                    'file_type' => 'json',
                ]));

            if (! $response->successful()) {
                Log::error('OilPriceService: FRED request failed', ['status' => $response->status()]);

                return;
            }

            $rows = collect($response->json('observations') ?? [])
                ->filter(fn (array $obs) => $obs['value'] !== '.') // FRED uses '.' for missing data
                ->map(fn (array $obs) => [
                    'date' => $obs['date'],
                    'price_usd' => (float) $obs['value'],
                ])
                ->all();

            if (empty($rows)) {
                Log::warning('OilPriceService: no valid FRED observations returned');

                return;
            }

            BrentPrice::upsert($rows, ['date'], ['price_usd']);
        } catch (Throwable $e) {
            Log::error('OilPriceService: fetchBrentPrices failed', ['error' => $e->getMessage()]);
        }
    }

    /**
     * Generate predictions from all available sources and store each one.
     * EWMA always runs. LLM runs when an API key is configured.
     * Returns the highest-confidence prediction (LLM preferred over EWMA).
     */
    public function generatePrediction(): ?PricePrediction
    {
        $prices = BrentPrice::orderBy('date', 'desc')->limit(30)->get();

        if ($prices->count() < self::EWMA_MIN_ROWS) {
            Log::warning('OilPriceService: not enough price data to generate prediction', [
                'rows' => $prices->count(),
            ]);

            return null;
        }

        $ewma = $this->generateEwmaPrediction($prices);

        if ($ewma !== null) {
            PricePrediction::create($ewma->toArray());
        }

        $llm = null;

        if (config('services.anthropic.api_key')) {
            $llm = $this->generateLlmPredictionWithContext($prices);
            $llm ??= $this->generateLlmPrediction($prices);

            if ($llm !== null) {
                PricePrediction::create($llm->toArray());personal_access_tokens
            }
        }

        return $llm ?? $ewma;
    }

    /**
     * Option B — LLM prediction via Anthropic API.
     * Sends recent prices + pre-computed EWMA context and asks for direction + confidence.
     */
    public function generateLlmPrediction(Collection $prices): ?PricePrediction
    {
        $chronological = $prices->sortBy('date');
        $ewma3 = $this->computeEwma($chronological->take(-3)->pluck('price_usd')->values()->all());
        $ewma7 = $this->computeEwma($chronological->take(-7)->pluck('price_usd')->values()->all());
        $ewma14 = $this->computeEwma($chronological->pluck('price_usd')->values()->all());

        $priceList = $chronological
            ->map(fn (BrentPrice $p) => "{$p->date->toDateString()}: \${$p->price_usd}")
            ->implode("\n");

        $prompt = <<<PROMPT
            You are analyzing Brent crude oil price data for a UK fuel price alert service.
            Your goal is to predict the short-term direction over the next 3–5 days.

            Recent Brent crude prices (USD/barrel):
            {$priceList}

            Pre-computed indicators:
            - 3-day EWMA:  \${$ewma3}
            - 7-day EWMA:  \${$ewma7}
            - 14-day EWMA: \${$ewma14}

            Respond with JSON only, no other text:
            {"direction": "rising|falling|flat", "confidence": 0-85, "reasoning": "one sentence"}
            PROMPT;

        $url = 'https://api.anthropic.com/v1/messages';

        try {
            $response = $this->apiLogger->send('anthropic', 'POST', $url, fn () => Http::timeout(15)
                ->withHeaders([
                    'x-api-key' => config('services.anthropic.api_key'),
                    'anthropic-version' => '2023-06-01',
                ])
                ->post($url, [
                    'model' => config('services.anthropic.model', 'claude-haiku-4-5-20251001'),
                    'max_tokens' => 256,
                    'messages' => [
                        ['role' => 'user', 'content' => $prompt],
                    ],
                ]));

            if (! $response->successful()) {
                Log::error('OilPriceService: Anthropic request failed', ['status' => $response->status()]);

                return null;
            }

            $text = $response->json('content.0.text') ?? '';
            $data = $this->extractJson($text);

            if (! isset($data['direction'], $data['confidence'], $data['reasoning'])) {
                Log::error('OilPriceService: unexpected LLM response format', ['text' => $text]);

                return null;
            }

            $direction = TrendDirection::tryFrom($data['direction']);
            $confidence = min((int) $data['confidence'], self::LLM_MAX_CONFIDENCE);

            if ($direction === null) {
                Log::error('OilPriceService: invalid direction in LLM response', ['direction' => $data['direction']]);

                return null;
            }

            return new PricePrediction([
                'predicted_for' => now()->toDateString(),
                'source' => PredictionSource::Llm,
                'direction' => $direction,
                'confidence' => $confidence,
                'reasoning' => $data['reasoning'],
                'generated_at' => now(),
            ]);
        } catch (Throwable $e) {
            Log::error('OilPriceService: generateLlmPrediction failed', ['error' => $e->getMessage()]);

            return null;
        }
    }

    /**
     * LLM prediction with 48h geopolitical context via Anthropic web search.
     * Claude searches for recent oil/geopolitical news before answering.
     * Reasons from raw prices only — no pre-computed indicators in prompt.
     */
    public function generateLlmPredictionWithContext(Collection $prices): ?PricePrediction
    {
        $priceList = $prices->sortBy('date')
            ->map(fn (BrentPrice $p) => "{$p->date->toDateString()}: \${$p->price_usd}")
            ->implode("\n");

        $prompt = <<<PROMPT
            You are analyzing Brent crude oil price data for a UK fuel price alert service.
            Your goal is to predict the short-term direction over the next 3–5 days.

            First, search for recent news (last 48 hours) about:
            - Brent crude oil price movements
            - OPEC+ production decisions or announcements
            - Major geopolitical events affecting oil supply (Middle East, Russia, US sanctions)
            - Global demand signals (China economic data, US inventory reports)

            Then, combining the news context with the price history below, predict the direction.

            Recent Brent crude prices (USD/barrel):
            {$priceList}

            Respond with JSON only, no other text:
            {"direction": "rising|falling|flat", "confidence": 0-85, "reasoning": "one sentence combining price trend and key news factor"}
            PROMPT;

        $url = 'https://api.anthropic.com/v1/messages';
        $messages = [['role' => 'user', 'content' => $prompt]];

        try {
            for ($i = 0, $response = null; $i < 5; $i++) {
                $response = $this->apiLogger->send('anthropic', 'POST', $url, fn () => Http::timeout(30)
                    ->withHeaders([
                        'x-api-key' => config('services.anthropic.api_key'),
                        'anthropic-version' => '2023-06-01',
                    ])
                    ->post($url, [
                        'model' => config('services.anthropic.model', 'claude-sonnet-4-6'),
                        'max_tokens' => 1024,
                        'tools' => [['type' => 'web_search_20250305', 'name' => 'web_search']],
                        'messages' => $messages,
                    ]));

                if (! $response->successful()) {
                    Log::error('OilPriceService: Anthropic context request failed', [
                        'status' => $response->status(),
                        'body' => $response->body(),
                    ]);

                    return null;
                }

                if ($response->json('stop_reason') !== 'pause_turn') {
                    break;
                }

                $messages[] = ['role' => 'assistant', 'content' => $response->json('content')];
            }

            $content = $response->json('content') ?? [];

            $text = collect($content)
                ->filter(fn ($b) => ($b['type'] ?? '') === 'text')
                ->implode('text', '');

            $data = $this->extractJson($text);

            if (! isset($data['direction'], $data['confidence'], $data['reasoning'])) {
                Log::error('OilPriceService: unexpected context LLM response format', ['text' => $text]);

                return null;
            }

            $direction = TrendDirection::tryFrom($data['direction']);
            $confidence = min((int) $data['confidence'], self::LLM_MAX_CONFIDENCE);

            if ($direction === null) {
                Log::error('OilPriceService: invalid direction in context LLM response', ['direction' => $data['direction']]);

                return null;
            }

            return new PricePrediction([
                'predicted_for' => now()->toDateString(),
                'source' => PredictionSource::LlmWithContext,
                'direction' => $direction,
                'confidence' => $confidence,
                'reasoning' => $data['reasoning'],
                'generated_at' => now(),
            ]);
        } catch (Throwable $e) {
            Log::error('OilPriceService: generateLlmPredictionWithContext failed', ['error' => $e->getMessage()]);

            return null;
        }
    }

    /**
     * Option A — EWMA-based trend extrapolation. Used as fallback when LLM is unavailable.
     * Compares the 3-day EWMA against the 7-day EWMA to detect direction.
     */
    public function generateEwmaPrediction(Collection $prices): ?PricePrediction
    {
        $chronological = $prices->sortBy('date')->pluck('price_usd')->values()->all();

        if (count($chronological) < self::EWMA_MIN_ROWS) {
            return null;
        }

        $ewma3 = $this->computeEwma(array_slice($chronological, -3));
        $ewma7 = $this->computeEwma(array_slice($chronological, -7));

        $changePct = (($ewma3 - $ewma7) / $ewma7) * 100;

        [$direction, $confidence] = match (true) {
            $changePct >= self::EWMA_THRESHOLD_PCT => [
                TrendDirection::Rising,
                $this->ewmaConfidence($changePct),
            ],
            $changePct <= -self::EWMA_THRESHOLD_PCT => [
                TrendDirection::Falling,
                $this->ewmaConfidence(abs($changePct)),
            ],
            default => [TrendDirection::Flat, 50],
        };

        $reasoning = sprintf(
            '3-day EWMA ($%.2f) vs 7-day EWMA ($%.2f): %.2f%% %s.',
            $ewma3,
            $ewma7,
            abs($changePct),
            $direction === TrendDirection::Flat ? 'difference (flat)' : $direction->value,
        );

        return new PricePrediction([
            'predicted_for' => now()->toDateString(),
            'source' => PredictionSource::Ewma,
            'direction' => $direction,
            'confidence' => $confidence,
            'reasoning' => $reasoning,
            'generated_at' => now(),
        ]);
    }

    /**
     * Compute Exponential Weighted Moving Average for a series of prices.
     *
     * @param  float[]  $prices  Chronological order (oldest first)
     */
    private function computeEwma(array $prices): float
    {
        $ema = $prices[0];

        foreach (array_slice($prices, 1) as $price) {
            $ema = self::EWMA_ALPHA * $price + (1 - self::EWMA_ALPHA) * $ema;
        }

        return round($ema, 4);
    }

    /**
     * Strip markdown code fences from a string and extract the first JSON object found.
     * Handles prose preambles that Claude sometimes adds before the JSON.
     */
    private function extractJson(string $text): ?array
    {
        $text = preg_replace('/^```(?:json)?\s*/m', '', trim($text));
        $text = preg_replace('/```\s*$/m', '', $text);
        $start = strpos($text, '{');
        $end = strrpos($text, '}');
        if ($start === false || $end === false) {
            return null;
        }

        return json_decode(substr($text, $start, $end - $start + 1), true) ?: null;
    }

    /**
     * Map a % change magnitude to a 0–EWMA_MAX_CONFIDENCE confidence score.
     * 1.5% → ~30, 3% → ~50, 5%+ → 65.
     */
    private function ewmaConfidence(float $changePct): int
    {
        $scaled = min($changePct / 5.0, 1.0) * self::EWMA_MAX_CONFIDENCE;

        return (int) round(max(30, $scaled));
    }
}
