<?php

namespace App\Services\MarketData;

use App\Models\Instrument;
use App\Models\MarketTick;
use App\Models\OptionGreek;
use App\Models\SystemLog;
use App\Services\FlatTrade\BrokerAccountResolver;
use App\Services\FlatTrade\FlatTradeClient;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use RuntimeException;

class MarketDataSyncService
{
    public function __construct(
        private FlatTradeClient $client,
        private GreeksCalculator $greeks,
        private BrokerAccountResolver $resolver,
    ) {}

    public function syncScanUniverse(?float $spotOverride = null): Collection
    {
        $this->ensureMarketFeedClient();

        $spot = $spotOverride ?? $this->fetchIndexLtp('NSE', config('flattrade.nifty_index_token'), 'NIFTY50');
        $this->syncIndexQuotes();

        $chainContracts = $this->fetchOptionChainContracts($spot);
        $instruments = collect();

        foreach ($chainContracts as $contract) {
            $instrument = $this->upsertFromContract($contract);
            if ($this->syncInstrumentQuote($instrument, $spot)) {
                $instruments->push($instrument->fresh(['latestTick', 'latestGreek']));
            }
        }

        Instrument::query()
            ->where('underlying', 'NIFTY')
            ->where('instrument_kind', 'option')
            ->whereNotIn('id', $instruments->pluck('id'))
            ->update(['is_active' => false]);

        SystemLog::create([
            'channel' => 'market_sync',
            'level' => 'info',
            'message' => 'Real market data synced from FlatTrade',
            'context' => ['count' => $instruments->count(), 'spot' => $spot],
        ]);

        return $instruments;
    }

    public function syncIndexQuotes(): void
    {
        $this->ensureMarketFeedClient();

        $this->storeIndexQuote(
            'NSE',
            config('flattrade.nifty_index_token'),
            'NIFTY50',
            'NIFTY 50'
        );

        $this->storeIndexQuote(
            'NSE',
            config('flattrade.vix_index_token'),
            'INDIA_VIX',
            'INDIA VIX'
        );
    }

    public function fetchNiftySpot(): float
    {
        $this->ensureMarketFeedClient();

        return $this->fetchIndexLtp('NSE', config('flattrade.nifty_index_token'), 'NIFTY50');
    }

    public function getIndexQuoteForDashboard(string $underlying): ?array
    {
        $instrument = Instrument::query()
            ->where('underlying', $underlying)
            ->where('instrument_kind', 'index')
            ->first();

        $tick = $instrument?->latestTick;

        if (! $tick) {
            return null;
        }

        $ltp = (float) $tick->ltp;
        $close = (float) ($tick->close ?: $ltp);
        $change = round($ltp - $close, 2);
        $changePct = $close > 0 ? round(($change / $close) * 100, 2) : 0;

        return [
            'name' => $underlying === 'NIFTY50' ? 'NIFTY 50' : 'INDIA VIX',
            'ltp' => $ltp,
            'change' => $change,
            'change_pct' => $changePct,
            'open' => (float) ($tick->open ?? $ltp),
            'high' => (float) ($tick->high ?? $ltp),
            'low' => (float) ($tick->low ?? $ltp),
            'close' => $close,
        ];
    }

    private function ensureMarketFeedClient(): void
    {
        if (config('flattrade.mock')) {
            throw new RuntimeException('FLATTRADE_MOCK is enabled. Set FLATTRADE_MOCK=false and connect admin FlatTrade credentials.');
        }

        if (! $this->resolver->bindMarketFeedClient($this->client)) {
            throw new RuntimeException('Admin FlatTrade credentials are not connected. Save API creds and complete OAuth on FlatTrade Account page.');
        }
    }

    private function fetchIndexLtp(string $exchange, string $token, string $underlying): float
    {
        $quote = $this->client->getQuotes($exchange, $token);

        if (($quote['stat'] ?? '') !== 'Ok' || empty($quote['lp'])) {
            throw new RuntimeException("Failed to fetch live quote for {$underlying}: ".($quote['emsg'] ?? 'unknown error'));
        }

        return (float) $quote['lp'];
    }

    private function storeIndexQuote(string $exchange, string $token, string $underlying, string $tradingsymbol): void
    {
        $quote = $this->client->getQuotes($exchange, $token);

        if (($quote['stat'] ?? '') !== 'Ok') {
            return;
        }

        $instrument = Instrument::updateOrCreate(
            ['exchange' => $exchange, 'token' => $token],
            [
                'tradingsymbol' => $tradingsymbol,
                'underlying' => $underlying,
                'instrument_kind' => 'index',
                'lot_size' => 1,
                'tick_size' => 0.05,
                'is_active' => true,
            ]
        );

        MarketTick::create([
            'instrument_id' => $instrument->id,
            'ltp' => (float) ($quote['lp'] ?? 0),
            'open' => (float) ($quote['o'] ?? 0),
            'high' => (float) ($quote['h'] ?? 0),
            'low' => (float) ($quote['l'] ?? 0),
            'close' => (float) ($quote['c'] ?? 0),
            'volume' => (int) ($quote['v'] ?? 0),
            'oi' => (int) ($quote['oi'] ?? 0),
            'bid_price' => isset($quote['bp1']) ? (float) $quote['bp1'] : null,
            'ask_price' => isset($quote['sp1']) ? (float) $quote['sp1'] : null,
            'tick_at' => now(),
        ]);
    }

    private function fetchOptionChainContracts(float $spot): array
    {
        $strikeStep = (int) config('flattrade.strike_step', 50);
        $scanSteps = (int) config('flattrade.scan_steps_from_atm', 5);
        $range = (int) config('flattrade.strike_range', 400);
        $atm = round($spot / $strikeStep) * $strikeStep;

        $referenceSymbol = $this->resolveReferenceOptionSymbol($atm);
        $chain = $this->client->getOptionChain('NFO', $referenceSymbol, $atm, $scanSteps);

        if (($chain['stat'] ?? '') !== 'Ok') {
            throw new RuntimeException('Failed to fetch option chain: '.($chain['emsg'] ?? 'unknown error'));
        }

        $values = $chain['values'] ?? [];
        if (! is_array($values) || count($values) === 0) {
            throw new RuntimeException('Option chain returned no contracts.');
        }

        return array_values(array_filter($values, function ($row) use ($spot, $range) {
            $strike = (float) ($row['strprc'] ?? 0);

            return $strike > 0 && abs($strike - $spot) <= $range;
        }));
    }

    private function resolveReferenceOptionSymbol(float $atmStrike): string
    {
        $search = $this->client->searchScrip('NFO', 'NIFTY');

        if (($search['stat'] ?? '') !== 'Ok' || empty($search['values'])) {
            throw new RuntimeException('Failed to search NIFTY option symbols.');
        }

        $candidates = collect($search['values'])
            ->filter(fn ($row) => str_contains($row['tsym'] ?? '', 'NIFTY'))
            ->sortBy(fn ($row) => $row['tsym'] ?? '')
            ->values();

        $weekly = $candidates->first(function ($row) use ($atmStrike) {
            $tsym = $row['tsym'] ?? '';

            return str_contains($tsym, 'C'.(int) $atmStrike) || str_contains($tsym, 'P'.(int) $atmStrike);
        });

        if ($weekly) {
            return $weekly['tsym'];
        }

        $first = $candidates->first();
        if (! $first) {
            throw new RuntimeException('No NIFTY option symbols found.');
        }

        return $first['tsym'];
    }

    private function upsertFromContract(array $contract): Instrument
    {
        $strike = (float) ($contract['strprc'] ?? 0);
        $type = strtoupper($contract['optt'] ?? '');

        return Instrument::updateOrCreate(
            ['exchange' => $contract['exch'] ?? 'NFO', 'token' => (string) ($contract['token'] ?? '')],
            [
                'tradingsymbol' => $contract['tsym'] ?? '',
                'underlying' => 'NIFTY',
                'instrument_kind' => 'option',
                'expiry_date' => $this->parseExpiryFromSymbol($contract['tsym'] ?? ''),
                'strike_price' => $strike,
                'option_type' => $type,
                'lot_size' => (int) ($contract['ls'] ?? 75),
                'tick_size' => (float) ($contract['ti'] ?? 0.05),
                'is_active' => true,
            ]
        );
    }

    private function syncInstrumentQuote(Instrument $instrument, float $spot): bool
    {
        $quote = $this->client->getQuotes($instrument->exchange, $instrument->token);

        if (($quote['stat'] ?? '') !== 'Ok' || ! isset($quote['lp'])) {
            SystemLog::create([
                'channel' => 'market_sync',
                'level' => 'warning',
                'message' => 'Skipped instrument without live quote',
                'context' => ['tsym' => $instrument->tradingsymbol],
            ]);

            return false;
        }

        $ltp = (float) $quote['lp'];

        $tick = MarketTick::create([
            'instrument_id' => $instrument->id,
            'ltp' => $ltp,
            'open' => (float) ($quote['o'] ?? $ltp),
            'high' => (float) ($quote['h'] ?? $ltp),
            'low' => (float) ($quote['l'] ?? $ltp),
            'close' => (float) ($quote['c'] ?? $ltp),
            'volume' => (int) ($quote['v'] ?? 0),
            'oi' => (int) ($quote['oi'] ?? 0),
            'bid_price' => isset($quote['bp1']) ? (float) $quote['bp1'] : null,
            'ask_price' => isset($quote['sp1']) ? (float) $quote['sp1'] : null,
            'tick_at' => now(),
        ]);

        if ($instrument->instrument_kind !== 'option' || ! $instrument->expiry_date) {
            return true;
        }

        $expiry = Carbon::parse($instrument->expiry_date)->endOfDay();
        $years = max($expiry->diffInSeconds(now()) / (365 * 24 * 3600), 1 / 365);

        $greekValues = $this->greeks->calculate(
            $spot,
            (float) $instrument->strike_price,
            $ltp,
            $years,
            (string) $instrument->option_type
        );

        $weights = config('rulebook.strike_selection.score_weights', []);
        $score = $this->greeks->score($greekValues, $tick->toArray(), $weights);

        OptionGreek::create([
            'instrument_id' => $instrument->id,
            'delta' => $greekValues['delta'],
            'gamma' => $greekValues['gamma'],
            'theta' => $greekValues['theta'],
            'vega' => $greekValues['vega'],
            'iv' => $greekValues['iv'],
            'score' => $score,
            'computed_at' => now(),
        ]);

        return true;
    }

    private function parseExpiryFromSymbol(string $tsym): ?string
    {
        if (preg_match('/NIFTY(\d{2}[A-Z]{3}\d{2})/', $tsym, $m)) {
            try {
                return Carbon::createFromFormat('dMy', $m[1])->format('Y-m-d');
            } catch (\Throwable) {
                return null;
            }
        }

        return null;
    }
}
