from __future__ import annotations

import json
from typing import Any, Optional

import requests

from fundis.exceptions import APIError, FundisError
from fundis.types import Event, Pattern, Signal


class FundisClient:
    """Thin client for querying Fundis intelligence data via the API.

    Parameters
    ----------
    api_key:
        A valid SentiChain API key (used for ticker requests and billing).
    base_url:
        Root URL for the Fundis intelligence API.
    timeout:
        HTTP request timeout in seconds.
    """

    def __init__(
        self,
        api_key: str = "",
        base_url: str = "https://api.fundis.ai",
        timeout: float = 30.0,
    ) -> None:
        self._api_key = api_key
        self._base_url = base_url.rstrip("/")
        self._timeout = timeout
        self._session = requests.Session()

    def get_events(self, ticker: str) -> list[Event]:
        """Fetch the latest classified events for *ticker*.

        Returns a list of :class:`Event` objects parsed from the most recent
        pipeline chunk.
        """
        ticker = ticker.upper().strip()
        if not ticker:
            raise FundisError("ticker must be a non-empty string.")

        raw = self._fetch_reasoning(ticker=ticker, summary_type="sm_events")

        if raw is None:
            return []

        items = self._parse_json(raw)
        if not isinstance(items, list):
            return []

        events: list[Event] = []
        for item in items:
            events.append(
                Event(
                    timestamp=str(item.get("timestamp", "")),
                    type=str(item.get("event", "asset")).lower(),
                    sentiment=str(item.get("sentiment", "neutral")).lower(),
                    summary=str(item.get("summary", "")),
                )
            )
        return events

    def get_signal(self, ticker: str) -> Optional[Signal]:
        """Fetch the latest directional signal for *ticker*.

        Returns a :class:`Signal` object with direction, confidence,
        rationale, category summaries, and detected patterns.
        Returns None if no signal data exists for this ticker.
        """
        ticker = ticker.upper().strip()
        if not ticker:
            raise FundisError("ticker must be a non-empty string.")

        raw = self._fetch_reasoning(ticker=ticker, summary_type="sm_signal")

        if raw is None:
            return None

        data = self._parse_json(raw)
        if not isinstance(data, dict):
            raise APIError("Signal data is not a JSON object.")

        patterns: list[Pattern] = []
        for p in data.get("patterns", []):
            if isinstance(p, dict):
                patterns.append(
                    Pattern(
                        keywords=p.get("keywords", []),
                        event_indices=p.get("event_indices", []),
                        description=p.get("description", ""),
                    )
                )

        return Signal(
            direction=str(data.get("direction", "FLAT")),
            confidence=float(data.get("confidence", 0.0)),
            rationale=str(data.get("rationale", "")),
            categories=data.get("categories", {}),
            patterns=patterns,
        )

    def list_tickers(self) -> list[str]:
        """Return the list of active ticker symbols from the API."""
        url = f"{self._base_url}/agent/tickers"
        try:
            resp = self._session.get(url, timeout=self._timeout)
        except requests.RequestException as exc:
            raise APIError(f"Request failed: {exc}") from exc

        if resp.status_code != 200:
            raise APIError(
                f"Unexpected response from API (HTTP {resp.status_code}).",
                status_code=resp.status_code,
            )

        data: dict[str, Any] = resp.json()
        tickers = data.get("tickers", [])
        if not isinstance(tickers, list):
            raise APIError("Tickers response is not a list.")
        return tickers

    def request_ticker(self, ticker: str) -> bool:
        """Request a new ticker to be added to the pipeline.

        Requires an API key set on the client. Returns True if the request
        was submitted, False if the ticker already exists.
        """
        ticker = ticker.upper().strip()
        if not ticker:
            raise FundisError("ticker must be a non-empty string.")
        if not self._api_key:
            raise FundisError("api_key is required to request a ticker.")

        url = f"{self._base_url}/agent/tickers/request"
        try:
            resp = self._session.post(
                url,
                json={"ticker": ticker, "api_key": self._api_key},
                timeout=self._timeout,
            )
        except requests.RequestException as exc:
            raise APIError(f"Request failed: {exc}") from exc

        if resp.status_code != 200:
            raise APIError(
                f"Unexpected response from API (HTTP {resp.status_code}).",
                status_code=resp.status_code,
            )

        data: dict[str, Any] = resp.json()
        return data.get("status") == "pending"

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    def _fetch_reasoning(self, ticker: str, summary_type: str) -> str | None:
        url = f"{self._base_url}/agent/get_reasoning_last"
        params = {"ticker": ticker, "summary_type": summary_type}

        try:
            resp = self._session.get(url, params=params, timeout=self._timeout)
        except requests.RequestException as exc:
            raise APIError(f"Request failed: {exc}") from exc

        if resp.status_code != 200:
            raise APIError(
                f"Unexpected response from API (HTTP {resp.status_code}).",
                status_code=resp.status_code,
            )

        data: dict[str, Any] = resp.json()
        reasoning = data.get("reasoning")

        if reasoning is None:
            return None

        return str(reasoning)

    @staticmethod
    def _parse_json(raw: str) -> Any:
        try:
            return json.loads(raw)
        except json.JSONDecodeError as exc:
            raise APIError(f"Failed to parse reasoning JSON: {exc}") from exc
