"""
Demo data source with synthetic data.

Generates realistic-looking OHLCV data plus additional columns for testing.
"""

import asyncio
import logging
import math
import random
import time
from typing import Callable, Dict, List, Optional

from ..base import DataSource

logger = logging.getLogger(__name__)
from ..schema import (
    Bar,
    ColumnInfo,
    DatafeedConfig,
    HistoryResult,
    Resolution,
    SearchResult,
    SymbolInfo,
)


class DemoDataSource(DataSource):
    """
    Demo data source that generates synthetic time-series data.

    Provides:
    - Standard OHLCV columns
    - Additional demo columns (RSI, sentiment, volume_profile)
    - Real-time updates via polling simulation
    """

    def __init__(self):
        self._subscriptions: Dict[str, asyncio.Task] = {}
        self._symbols = {
            "DEMO:BTC/USD": {
                "name": "Bitcoin",
                "type": "crypto",
                "base_price": 50000.0,
                "volatility": 0.02,
            },
            "DEMO:ETH/USD": {
                "name": "Ethereum",
                "type": "crypto",
                "base_price": 3000.0,
                "volatility": 0.03,
            },
            "DEMO:SOL/USD": {
                "name": "Solana",
                "type": "crypto",
                "base_price": 100.0,
                "volatility": 0.04,
            },
        }

    async def get_config(self) -> DatafeedConfig:
        return DatafeedConfig(
            name="Demo DataSource",
            description="Synthetic data generator for testing. Provides OHLCV plus additional indicator columns.",
            supported_resolutions=[
                Resolution.M1,
                Resolution.M5,
                Resolution.M15,
                Resolution.H1,
                Resolution.D1,
            ],
            supports_search=True,
            supports_time=True,
            exchanges=["DEMO"],
            symbols_types=["crypto"],
        )

    async def search_symbols(
        self,
        query: str,
        type: Optional[str] = None,
        exchange: Optional[str] = None,
        limit: int = 30,
    ) -> List[SearchResult]:
        query_lower = query.lower()
        results = []

        for symbol, info in self._symbols.items():
            if query_lower in symbol.lower() or query_lower in info["name"].lower():
                if type and info["type"] != type:
                    continue
                results.append(
                    SearchResult(
                        symbol=info['name'],  # Clean user-facing format (e.g., "Bitcoin")
                        ticker=symbol,  # Keep DEMO:BTC/USD format for routing
                        full_name=f"{info['name']} (DEMO)",
                        description=f"Demo {info['name']} pair",
                        exchange="DEMO",
                        type=info["type"],
                    )
                )

        return results[:limit]

    async def resolve_symbol(self, symbol: str) -> SymbolInfo:
        if symbol not in self._symbols:
            raise ValueError(f"Symbol '{symbol}' not found")

        info = self._symbols[symbol]
        base, quote = symbol.split(":")[1].split("/")

        return SymbolInfo(
            symbol=info["name"],  # Clean user-facing format (e.g., "Bitcoin")
            ticker=symbol,  # Keep DEMO:BTC/USD format for routing
            name=info["name"],
            description=f"Demo {info['name']} spot price with synthetic indicators",
            type=info["type"],
            exchange="DEMO",
            timezone="Etc/UTC",
            session="24x7",
            supported_resolutions=[Resolution.M1, Resolution.M5, Resolution.M15, Resolution.H1, Resolution.D1],
            has_intraday=True,
            has_daily=True,
            has_weekly_and_monthly=False,
            columns=[
                ColumnInfo(
                    name="open",
                    type="float",
                    description=f"Opening price in {quote}",
                    unit=quote,
                ),
                ColumnInfo(
                    name="high",
                    type="float",
                    description=f"Highest price in {quote}",
                    unit=quote,
                ),
                ColumnInfo(
                    name="low",
                    type="float",
                    description=f"Lowest price in {quote}",
                    unit=quote,
                ),
                ColumnInfo(
                    name="close",
                    type="float",
                    description=f"Closing price in {quote}",
                    unit=quote,
                ),
                ColumnInfo(
                    name="volume",
                    type="float",
                    description=f"Trading volume in {base}",
                    unit=base,
                ),
                ColumnInfo(
                    name="rsi",
                    type="float",
                    description="Relative Strength Index (14-period), range 0-100",
                    unit=None,
                ),
                ColumnInfo(
                    name="sentiment",
                    type="float",
                    description="Synthetic social sentiment score, range -1.0 to 1.0",
                    unit=None,
                ),
                ColumnInfo(
                    name="volume_profile",
                    type="float",
                    description="Volume as percentage of 24h average",
                    unit="%",
                ),
            ],
            time_column="time",
            has_ohlcv=True,
            pricescale=100,
            minmov=1,
            base_currency=base,
            quote_currency=quote,
        )

    async def get_bars(
        self,
        symbol: str,
        resolution: str,
        from_time: int,
        to_time: int,
        countback: Optional[int] = None,
    ) -> HistoryResult:
        if symbol not in self._symbols:
            raise ValueError(f"Symbol '{symbol}' not found")

        logger.info(
            f"DemoDataSource.get_bars: symbol={symbol}, resolution={resolution}, "
            f"from_time={from_time}, to_time={to_time}, countback={countback}"
        )

        info = self._symbols[symbol]
        symbol_meta = await self.resolve_symbol(symbol)

        # Convert resolution to seconds
        resolution_seconds = self._resolution_to_seconds(resolution)

        # Generate bars
        bars = []
        # Align current_time to resolution, but ensure it's >= from_time
        current_time = from_time - (from_time % resolution_seconds)
        if current_time < from_time:
            current_time += resolution_seconds

        price = info["base_price"]

        bar_count = 0
        max_bars = countback if countback else 5000

        while current_time <= to_time and bar_count < max_bars:
            bar_data = self._generate_bar(current_time, price, info["volatility"], resolution_seconds)

            # Only include bars within the requested range
            if from_time <= current_time <= to_time:
                bars.append(Bar(time=current_time * 1000, data=bar_data))  # Convert to milliseconds
                bar_count += 1

            price = bar_data["close"]  # Next bar starts from previous close
            current_time += resolution_seconds

        logger.info(
            f"DemoDataSource.get_bars: Generated {len(bars)} bars. "
            f"First: {bars[0].time if bars else 'N/A'}, Last: {bars[-1].time if bars else 'N/A'}"
        )

        # Determine if there's more data (for pagination)
        next_time = current_time if current_time <= to_time else None

        return HistoryResult(
            symbol=symbol,
            resolution=resolution,
            bars=bars,
            columns=symbol_meta.columns,
            nextTime=next_time,
        )

    async def subscribe_bars(
        self,
        symbol: str,
        resolution: str,
        on_tick: Callable[[dict], None],
    ) -> str:
        if symbol not in self._symbols:
            raise ValueError(f"Symbol '{symbol}' not found")

        subscription_id = f"{symbol}:{resolution}:{time.time()}"

        # Start background task to simulate real-time updates
        task = asyncio.create_task(
            self._tick_generator(symbol, resolution, on_tick, subscription_id)
        )
        self._subscriptions[subscription_id] = task

        return subscription_id

    async def unsubscribe_bars(self, subscription_id: str) -> None:
        task = self._subscriptions.pop(subscription_id, None)
        if task:
            task.cancel()
            try:
                await task
            except asyncio.CancelledError:
                pass

    def _resolution_to_seconds(self, resolution: str) -> int:
        """Convert resolution string to seconds"""
        if resolution.endswith("D"):
            return int(resolution[:-1]) * 86400
        elif resolution.endswith("W"):
            return int(resolution[:-1]) * 604800
        elif resolution.endswith("M"):
            return int(resolution[:-1]) * 2592000  # Approximate month
        else:
            # Assume minutes
            return int(resolution) * 60

    def _generate_bar(self, timestamp: int, base_price: float, volatility: float, period_seconds: int) -> dict:
        """Generate a single synthetic OHLCV bar"""
        # Random walk for the period
        open_price = base_price

        # Generate intra-period price movement
        num_ticks = max(10, period_seconds // 60)  # More ticks for longer periods
        prices = [open_price]

        for _ in range(num_ticks):
            change = random.gauss(0, volatility / math.sqrt(num_ticks))
            prices.append(prices[-1] * (1 + change))

        close_price = prices[-1]
        high_price = max(prices)
        low_price = min(prices)

        # Volume with some randomness
        base_volume = 1000000 * (period_seconds / 60)  # Scale with period
        volume = base_volume * random.uniform(0.5, 2.0)

        # Additional synthetic indicators
        rsi = 30 + random.random() * 40  # Biased toward middle range
        sentiment = math.sin(timestamp / 3600) * 0.5 + random.gauss(0, 0.2)  # Hourly cycle + noise
        sentiment = max(-1.0, min(1.0, sentiment))
        volume_profile = 100 * random.uniform(0.5, 1.5)

        return {
            "open": round(open_price, 2),
            "high": round(high_price, 2),
            "low": round(low_price, 2),
            "close": round(close_price, 2),
            "volume": round(volume, 2),
            "rsi": round(rsi, 2),
            "sentiment": round(sentiment, 3),
            "volume_profile": round(volume_profile, 2),
        }

    async def _tick_generator(
        self,
        symbol: str,
        resolution: str,
        on_tick: Callable[[dict], None],
        subscription_id: str,
    ):
        """Background task that generates periodic ticks"""
        info = self._symbols[symbol]
        resolution_seconds = self._resolution_to_seconds(resolution)

        # Start from current aligned time
        current_time = int(time.time())
        current_time = current_time - (current_time % resolution_seconds)
        price = info["base_price"]

        try:
            while True:
                # Wait until next bar
                await asyncio.sleep(resolution_seconds)

                current_time += resolution_seconds
                bar_data = self._generate_bar(current_time, price, info["volatility"], resolution_seconds)
                price = bar_data["close"]

                # Call the tick handler
                tick_data = {"time": current_time, **bar_data}
                on_tick(tick_data)

        except asyncio.CancelledError:
            # Subscription cancelled
            pass
