from __future__ import annotations

import logging
from dataclasses import asdict
from typing import Any

from coinjure.strategy.strategy import Strategy, StrategyContext

logger = logging.getLogger(__name__)


def _import_agents_sdk():
    try:
        from agents import Agent, Runner, function_tool
    except ImportError as exc:  # pragma: no cover - depends on optional package
        raise RuntimeError(
            'OpenAI Agents SDK is not installed. Install it with `pip install openai-agents` '
            'or `poetry add openai-agents` to enable tool-using AgentStrategy runs.'
        ) from exc
    return Agent, Runner, function_tool


class AgentStrategy(Strategy):
    """Base for LLM-driven or tool-using strategies.

    These strategies may call external APIs (LLMs, web search, MCP tools) and
    are NOT eligible for parameter grid search — use paper trading for evaluation.
    """

    strategy_type = 'agent'

    #: Maximum quantity the agent is allowed to request per trade.
    max_agent_trade_size: int = 100

    @classmethod
    def supports_auto_tune(cls) -> bool:
        return False

    @classmethod
    def sdk_available(cls) -> bool:
        try:
            _import_agents_sdk()
        except RuntimeError:
            return False
        return True

    # -- Hallucination guards ------------------------------------------------

    def _validate_agent_action(
        self,
        *,
        symbol: str,
        quantity: float | int,
        price: float,
        context: StrategyContext | None = None,
    ) -> str | None:
        """Validate an action proposed by the LLM agent.

        Returns ``None`` if the action is valid, or a string describing the
        rejection reason.

        Checks:
        1. Ticker symbol exists in the data manager (if context is available).
        2. Quantity is positive and <= ``max_agent_trade_size``.
        3. Price is between 0 and 1 (prediction market constraint).
        """
        ctx = context or self.get_context()

        # 1. Ticker existence
        if ctx is not None:
            resolved = ctx.resolve_ticker(symbol)
            if resolved is None:
                reason = f'Unknown ticker symbol: {symbol!r}'
                logger.warning('Agent action rejected: %s', reason)
                return reason

        # 2. Quantity bounds
        if quantity <= 0:
            reason = f'Quantity must be positive, got {quantity}'
            logger.warning('Agent action rejected: %s', reason)
            return reason
        if quantity > self.max_agent_trade_size:
            reason = (
                f'Quantity {quantity} exceeds max_agent_trade_size '
                f'{self.max_agent_trade_size}'
            )
            logger.warning('Agent action rejected: %s', reason)
            return reason

        # 3. Price in valid prediction-market range (0, 1)
        if price <= 0 or price >= 1:
            reason = f'Price {price} outside valid range (0, 1)'
            logger.warning('Agent action rejected: %s', reason)
            return reason

        return None

    def get_agent_name(self) -> str:
        return getattr(self, 'agent_name', self.__class__.__name__)

    def get_agent_model(self) -> str:
        return str(getattr(self, 'agent_model', 'gpt-4.1-mini'))

    def get_agent_max_turns(self) -> int:
        return int(getattr(self, 'agent_max_turns', 8))

    def get_prompt_guide(self) -> str:
        """Default operator/LLM guide for agent strategies."""
        return (
            'You are an agent strategy running inside Coinjure. '
            'Use the bound StrategyContext as the source of truth. '
            'Inspect context.ticker_history(...) and context.price_history(...) '
            'for the current market, context.market_history(...) for cross-market '
            'state, context.available_tickers(include_complements=False) to choose '
            'base tradable markets, and context.resolve_trade_ticker(symbol, side) '
            'when you need the actual YES/NO contract, context.order_books() for current available books, '
            'context.positions() for exposure, and context.recent_news() for '
            'available news. Do not use future information. Only act on data '
            'visible in the current context and place trades through trader.place_order(...).'
        )

    def build_prompt_context(self, context: StrategyContext | None = None) -> str:
        """Render a prompt-friendly snapshot from the unified context."""
        ctx = context or self.require_context()
        ticker = ctx.ticker
        ticker_label = getattr(ticker, 'symbol', 'none')
        ticker_history = ctx.ticker_history(limit=10)
        price_history = ctx.price_history(limit=10)
        related_books = ctx.order_books(limit=10)
        available_tickers = [
            ticker.symbol
            for ticker in ctx.available_tickers(limit=10, include_complements=False)
        ]
        active_positions = ctx.active_positions()
        recent_news = ctx.recent_news(limit=5)

        return '\n'.join(
            [
                self.get_prompt_guide(),
                f'event_type={ctx.event_type}',
                f'event_ticker={ticker_label}',
                f'event_timestamp={ctx.event_timestamp}',
                f'ticker_history_points={len(ticker_history)}',
                f'global_market_points={len(ctx.market_history(limit=200))}',
                f'recent_prices={price_history}',
                f'available_tickers={available_tickers}',
                f'visible_order_books={[book.__dict__ for book in related_books]}',
                f'active_positions={[pos.__dict__ for pos in active_positions]}',
                f'recent_news={recent_news}',
            ]
        )

    def build_agent_instructions(self, context: StrategyContext | None = None) -> str:
        return self.build_prompt_context(context)

    def build_task_input(self, context: StrategyContext | None = None) -> str:
        ctx = context or self.require_context()
        ticker = ctx.ticker
        ticker_symbol = getattr(ticker, 'symbol', 'none')
        event = ctx.event
        fragments = [
            f'Analyze the current trading step for ticker={ticker_symbol}.',
            f'event_type={ctx.event_type}',
        ]
        news = getattr(event, 'title', '') or getattr(event, 'news', '')
        if news:
            fragments.append(f'event_text={news[:300]}')
        if hasattr(event, 'price'):
            fragments.append(f'event_price={event.price}')
        return '\n'.join(fragments)

    def build_openai_tools(self, context: StrategyContext | None = None) -> list[Any]:  # noqa: C901
        ctx = context or self.require_context()
        _Agent, _Runner, function_tool = _import_agents_sdk()

        @function_tool
        def list_available_tickers() -> list[dict[str, object]]:
            """List currently visible tradable tickers."""
            rows: list[dict[str, object]] = []
            for ticker in ctx.available_tickers(include_complements=False):
                rows.append(
                    {
                        'symbol': ticker.symbol,
                        'name': getattr(ticker, 'name', '') or ticker.symbol,
                        'market_id': getattr(ticker, 'market_id', ''),
                        'event_id': getattr(ticker, 'event_id', ''),
                    }
                )
            return rows

        @function_tool
        def get_ticker_history(symbol: str, limit: int = 20) -> list[dict[str, object]]:
            """Return visible market history for a ticker symbol."""
            ticker = ctx.resolve_ticker(symbol)
            if ticker is None:
                return []
            history = ctx.market_history(ticker=ticker, limit=limit)
            return [self._market_point_to_dict(point) for point in history]

        @function_tool
        def get_price_history(symbol: str, limit: int = 20) -> list[float]:
            """Return recent price history for a ticker symbol."""
            ticker = ctx.resolve_ticker(symbol)
            if ticker is None:
                return []
            return [
                float(price) for price in ctx.price_history(ticker=ticker, limit=limit)
            ]

        @function_tool
        def resolve_trade_contract(symbol: str, side: str = 'yes') -> dict[str, object]:
            """Resolve a base market symbol plus side into the actual tradable contract."""
            ticker = ctx.resolve_trade_ticker(symbol, side)
            if ticker is None:
                return {}
            return {
                'symbol': ticker.symbol,
                'name': getattr(ticker, 'name', '') or ticker.symbol,
                'market_id': getattr(ticker, 'market_id', ''),
                'event_id': getattr(ticker, 'event_id', ''),
                'side': side.strip().lower() or 'yes',
            }

        @function_tool
        def get_order_books(limit: int = 20) -> list[dict[str, object]]:
            """Return current best bid/ask snapshots for visible tickers."""
            return [asdict(book) for book in ctx.order_books(limit=limit)]

        @function_tool
        def get_positions() -> list[dict[str, object]]:
            """Return current portfolio positions."""
            return [asdict(pos) for pos in ctx.positions()]

        @function_tool
        def get_recent_news(limit: int = 10) -> list[dict[str, str]]:
            """Return recent visible news items."""
            return ctx.recent_news(limit=limit)

        return [
            list_available_tickers,
            get_ticker_history,
            get_price_history,
            resolve_trade_contract,
            get_order_books,
            get_positions,
            get_recent_news,
        ]

    def create_openai_agent(self, context: StrategyContext | None = None) -> Any:
        ctx = context or self.require_context()
        Agent, _Runner, _function_tool = _import_agents_sdk()
        return Agent(
            name=self.get_agent_name(),
            instructions=self.build_agent_instructions(ctx),
            model=self.get_agent_model(),
            tools=self.build_openai_tools(ctx),
        )

    async def run_openai_agent(
        self,
        *,
        input_text: str | None = None,
        context: StrategyContext | None = None,
    ) -> Any:
        ctx = context or self.require_context()
        agent = self.create_openai_agent(ctx)
        _Agent, Runner, _function_tool = _import_agents_sdk()
        return await Runner.run(
            agent,
            input_text or self.build_task_input(ctx),
            max_turns=self.get_agent_max_turns(),
        )

    @staticmethod
    def get_run_final_output(run_result: Any) -> str:
        output = getattr(run_result, 'final_output', '')
        return output if isinstance(output, str) else str(output)

    @staticmethod
    def _market_point_to_dict(point) -> dict[str, object]:
        return {
            'sequence': point.sequence,
            'symbol': point.ticker.symbol,
            'name': getattr(point.ticker, 'name', '') or point.ticker.symbol,
            'event_type': point.event_type,
            'timestamp': point.timestamp,
            'event_price': float(point.event_price)
            if point.event_price is not None
            else None,
            'event_side': point.event_side,
            'event_size': float(point.event_size)
            if point.event_size is not None
            else None,
            'event_size_delta': (
                float(point.event_size_delta)
                if point.event_size_delta is not None
                else None
            ),
            'best_bid': float(point.best_bid) if point.best_bid is not None else None,
            'best_bid_size': (
                float(point.best_bid_size) if point.best_bid_size is not None else None
            ),
            'best_ask': float(point.best_ask) if point.best_ask is not None else None,
            'best_ask_size': (
                float(point.best_ask_size) if point.best_ask_size is not None else None
            ),
        }
