"""Live scanner using neural network for signal generation."""

from __future__ import annotations

from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Set
from zoneinfo import ZoneInfo

import numpy as np
import pandas as pd
from loguru import logger

from core.data_loader import DataPreparator
from ai.inference_v2 import NeuralPredictor
from scanner.repository import InstrumentRepository

_MS = ZoneInfo("Europe/Moscow")


class NeuralScanner:
    """Scanner that uses neural network to generate trading signals.

    Iterates through all available instruments, loads H1 data,
    runs neural network inference, and outputs signals with entry/SL/TP.

    Conflicting signal resolution:
    When both LONG and SHORT signals fire at the same timestamp with
    probability gap < ``min_signal_gap``, both are discarded — the model
    is uncertain. Only the higher-probability side survives when gap >= threshold.
    """

    def __init__(
        self,
        model_path: Optional[Path] = None,
        entry_threshold: float = 0.6,
        min_confidence: float = 0.3,
        min_signal_gap: float = 0.05,
        live_mode: bool = False,
    ):
        self.entry_threshold = entry_threshold
        self.min_confidence = min_confidence
        self.min_signal_gap = min_signal_gap
        self.model_path = model_path
        self.predictor: Optional[NeuralPredictor] = None
        self.repo = InstrumentRepository()
        self.live_mode = live_mode

        if model_path and model_path.exists():
            self.predictor = NeuralPredictor(
                model_path=model_path,
                entry_threshold=entry_threshold,
                min_confidence=min_confidence,
            )
            logger.info(f"Neural model loaded from {model_path}")
        else:
            logger.warning(f"Model not found: {model_path}")

    def get_all_instruments(self) -> List[str]:
        return self.repo.get_all_instruments()

    def get_latest_timestamp(self, ticker: str) -> Optional[int]:
        return self.repo.get_latest_timestamp(ticker)

    def get_latest_price(self, ticker: str) -> Optional[float]:
        return self.repo.get_latest_price(ticker)

    def get_latest_candle_state(self, ticker: str) -> Optional[Dict]:
        """Get latest timestamp and close price for change detection."""
        return self.repo.get_latest_candle_state(ticker)

    def scan_instrument(
        self,
        ticker: str,
        lookback_hours: int = 72,
        sides: List[str] = ["LONG", "SHORT"],
    ) -> List[Dict]:
        """Scan a single instrument for neural network signals.

        Uses dual-head architecture: single forward pass gives both LONG and SHORT.
        No more two-pass inference needed.

        Args:
            ticker: Ticker symbol
            lookback_hours: How many hours of H1 data to load
            sides: Which sides to analyze (filter after signal generation)

        Returns:
            List of signal dictionaries
        """
        if self.predictor is None:
            logger.warning("No model loaded, cannot scan")
            return []

        end_ts = int(datetime.now(_MS).timestamp())
        start_ts = end_ts - lookback_hours * 3600

        try:
            prep = DataPreparator([ticker], ["H1"])
            df_h1 = prep.load_h1(ticker, start_ts, end_ts)

            if df_h1.empty:
                return []

            # Single pass: model returns BOTH LONG and SHORT signals
            all_signals = self.predictor.predict_signals(df_h1, side=None)
            
            # Filter by requested sides
            if sides:
                all_signals = [s for s in all_signals if s.side in sides]

            # Show the model's raw prediction for current bar (both heads)
            prediction = self.predictor.get_latest_prediction(df_h1, side=None)
            if prediction is not None:
                self._log_prediction(ticker, prediction)

            # Resolve conflicting LONG/SHORT signals at the same timestamp.
            # If probability gap is too small, model is uncertain → discard both.
            if len(all_signals) >= 2:
                all_signals = self._resolve_conflicts(ticker, all_signals)

            if not all_signals:
                return []

            # Return the strongest signal for each direction
            latest_signal = all_signals[-1]
            return [self._format_signal(latest_signal, ticker)]

        except Exception as e:
            logger.error(f"Error scanning {ticker}: {e}")
            return []

    def _resolve_conflicts(
        self,
        ticker: str,
        signals: List,
    ) -> List:
        """Resolve conflicting LONG/SHORT signals at the same timestamp.

        When both directions fire at the same bar with probability gap
        smaller than ``min_signal_gap``, the model is uncertain — both
        signals are discarded. Only the higher-probability side survives
        when the gap is sufficient.

        Args:
            ticker: Ticker symbol (for logging)
            signals: List of NeuralSignal objects

        Returns:
            Filtered list of NeuralSignal objects
        """
        by_ts: Dict[int, List] = {}
        for s in signals:
            by_ts.setdefault(s.timestamp, []).append(s)

        resolved = []
        for ts, sigs in by_ts.items():
            longs = [s for s in sigs if s.side == "LONG"]
            shorts = [s for s in sigs if s.side == "SHORT"]

            if longs and shorts:
                best_long = max(longs, key=lambda s: s.entry_probability)
                best_short = max(shorts, key=lambda s: s.entry_probability)
                gap = abs(best_long.entry_probability - best_short.entry_probability)

                if gap >= self.min_signal_gap:
                    # Keep only the higher-probability side
                    winner = best_long if best_long.entry_probability > best_short.entry_probability else best_short
                    resolved.append(winner)
                    logger.debug(
                        f"[{ticker}] Conflicting signals resolved at "
                        f"{datetime.fromtimestamp(ts, _MS).strftime('%H:%M')}: "
                        f"LONG={best_long.entry_probability:.2%} "
                        f"SHORT={best_short.entry_probability:.2%} "
                        f"gap={gap:.2%} → {winner.side}"
                    )
                else:
                    # Gap too small — model uncertain, discard both
                    logger.debug(
                        f"[{ticker}] Conflicting signals DISCARDED at "
                        f"{datetime.fromtimestamp(ts, _MS).strftime('%H:%M')}: "
                        f"LONG={best_long.entry_probability:.2%} "
                        f"SHORT={best_short.entry_probability:.2%} "
                        f"gap={gap:.2%} < {self.min_signal_gap:.2%}"
                    )
            else:
                resolved.extend(sigs)

        return resolved

    def _format_signal(self, signal, ticker: str) -> Dict:
        """Format a NeuralSignal into a dictionary."""
        return {
            "ticker": ticker,
            "signal_time": datetime.fromtimestamp(signal.timestamp, _MS).strftime("%Y-%m-%d %H:%M:%S"),
            "timestamp": signal.timestamp,
            "signal_type": signal.side,
            "entry_price": signal.entry_price,
            "sl_price": signal.sl_price,
            "tp_price": signal.tp_price,
            "entry_probability": signal.entry_probability,
            "confidence": signal.confidence,
            "sl_distance_atr": signal.sl_distance_atr,
            "tp_distance_atr": signal.tp_distance_atr,
        }

    def _log_prediction(self, ticker: str, pred: Dict):
        """Log raw model prediction for a ticker (signal-agnostic).

        Shows what BOTH LONG and SHORT heads think about the current bar,
        plus the gap between them.

        Args:
            ticker: Ticker symbol
            pred: Prediction dict from get_latest_prediction() (contains both heads)
        """
        long_proba = pred.get("entry_long_proba", 0)
        short_proba = pred.get("entry_short_proba", 0)
        conf = pred.get("confidence", 0)
        sl_atr = pred.get("sl_distance_atr", 0)
        tp_atr = pred.get("tp_distance_atr", 0)
        price = pred.get("entry_price", 0)
        gap = abs(long_proba - short_proba)
        
        long_flag = "⚡ SIGNAL" if long_proba >= self.entry_threshold and conf >= self.min_confidence else "—"
        short_flag = "⚡ SIGNAL" if short_proba >= self.entry_threshold and conf >= self.min_confidence else "—"

        logger.debug(
            f"[{ticker}] LONG  | "
            f"Prob: {long_proba:.2%} | Conf: {conf:.2%} | "
            f"SL: {sl_atr:.2f}×ATR | TP: {tp_atr:.2f}×ATR | "
            f"Price: {price:.2f} | Δ:{gap:.2%} {long_flag}"
        )
        logger.debug(
            f"[{ticker}] SHORT | "
            f"Prob: {short_proba:.2%} | Conf: {conf:.2%} | "
            f"SL: {sl_atr:.2f}×ATR | TP: {tp_atr:.2f}×ATR | "
            f"Price: {price:.2f} | Δ:{gap:.2%} {short_flag}"
        )

    def log_signal(self, signal: Dict):
        """Log signal to console in compact single-line format."""
        ts = signal['signal_time']
        logger.info(
            f"[NN] {signal['ticker']} {signal['signal_type']} @ "
            f"{signal['entry_price']:.2f} | "
            f"SL={signal['sl_price']:.2f} ({signal.get('sl_distance_atr', 0):.2f}x) "
            f"TP={signal['tp_price']:.2f} ({signal.get('tp_distance_atr', 0):.2f}x) | "
            f"P={signal.get('entry_probability', 0):.0%} C={signal.get('confidence', 0):.0%} [{ts}]"
        )

    def scan_all_instruments(
        self,
        lookback_hours: int = 72,
        instruments: Optional[List[str]] = None,
        sides: List[str] = ["LONG", "SHORT"],
    ) -> List[Dict]:
        """Scan all instruments and collect signals.

        Args:
            lookback_hours: Hours of H1 data to load per ticker
            instruments: List of tickers (default: all available)
            sides: Which sides to analyze

        Returns:
            List of all signals found
        """
        if instruments is None:
            instruments = self.get_all_instruments()

        all_signals = []
        for ticker in instruments:
            signals = self.scan_instrument(ticker, lookback_hours, sides)
            for sig in signals:
                self.log_signal(sig)
                all_signals.append(sig)

        return all_signals
