"""YukkaAPI (ABC) + YukkaClient (live) + DummyClient (offline) + Session.

This module implements the abstract API pattern where strategy/analysis code depends on
an abstract interface, not on HTTP calls or specific infrastructure.
"""

from __future__ import annotations

import io
import os
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import date
from typing import TYPE_CHECKING, Self

import polars as pl
from httpx import Client
from yarl import URL

from .auth import Token
from .utils import (
    DATA_API_URL,
    DATE_FORMATTER,
    IntoDateTime,
    to_datetime,
)

if TYPE_CHECKING:
    from .entities import Asset, EntityResolver


class YukkaAPI(ABC):
    """Abstract interface for YUKKA data access.

    Defines WHAT data is available without specifying HOW it's retrieved.
    Client code writes against this interface, not specific implementations.
    """

    @abstractmethod
    def sentiment(self, entities: list[str], date_from: IntoDateTime, date_to: IntoDateTime) -> pl.DataFrame:
        """Fetch sentiment scores for entities over a time range.

        Args:
            entities: List of entity IDs
            date_from: Start date
            date_to: End date

        Returns:
            DataFrame with sentiment scores
        """
        pass


@dataclass
class YukkaClient(YukkaAPI):
    """Live implementation of YukkaAPI wrapping the three YUKKA APIs.

    This client makes actual HTTP requests to YUKKA's APIs and requires authentication.
    Can be used as a context manager for automatic resource cleanup.

    Example:
        >>> with YukkaClient.from_token("eyJ...") as client:
        ...     df = client.sentiment(["company:bmw"], "2026-01-01", "2026-01-07")
    """

    _token: Token
    _client: Client
    _data_api_url: URL

    @classmethod
    def from_token(cls, token: str) -> YukkaClient:
        """Create a YukkaClient from an existing JWT token.

        Args:
            token: JWT token string

        Returns:
            YukkaClient instance

        Example:
            >>> client = YukkaClient.from_token("eyJ...")
            >>> df = client.sentiment(["company:bmw"], "2026-01-01", "2026-01-07")
        """
        client = Client(timeout=None)  # nosec B113
        return cls(
            _token=Token.from_token(token),
            _client=client,
            _data_api_url=DATA_API_URL,
        )

    @property
    def _auth_headers(self) -> dict[str, str]:
        """Get authorization headers with current JWT token."""
        return {"Authorization": f"Bearer {self._token.get()}"}

    def _build_time_range(self, date_from: IntoDateTime, date_to: IntoDateTime) -> dict[str, str]:
        """Build time range dict for API requests.

        Args:
            date_from: Start date
            date_to: End date

        Returns:
            Dict with date_from and date_to formatted as strings
        """
        return {
            "date_from": to_datetime(date_from).strftime(DATE_FORMATTER),
            "date_to": to_datetime(date_to).strftime(DATE_FORMATTER),
        }

    def sentiment(
        self, entities: list[str], date_from: IntoDateTime, date_to: IntoDateTime, batch_size: int = 50
    ) -> pl.DataFrame:
        """Fetch sentiment counts for entities over a time range.

        Args:
            entities: List of entity IDs in format "type:alpha_id" (e.g., "company:bmw")
            date_from: Start date
            date_to: End date
            batch_size: Number of entities per API request (default 50)

        Returns:
            DataFrame with columns: date, entity, positive, neutral, negative

        Raises:
            httpx.HTTPStatusError: If API request fails
        """
        if len(entities) == 0:
            return pl.DataFrame()

        all_dfs: list[pl.DataFrame] = []

        # Batch entities to avoid API limits
        for i in range(0, len(entities), batch_size):
            batch = entities[i : i + batch_size]
            print(f"Fetching sentiment for batch {i // batch_size + 1} ({len(batch)} entities)...")

            data: list[bytes] = []
            with self._client.stream(
                "POST",
                str(self._data_api_url / "api" / "exports" / "sentiment_matches"),
                json={
                    "time_range": self._build_time_range(date_from, date_to),
                    "sentiment_filter": {"entities": batch},
                    "language_filter": ["en", "de"],
                },
                headers=self._auth_headers,
            ) as resp:
                resp.raise_for_status()
                for chunk in resp.iter_bytes(chunk_size=10_000):
                    data.append(chunk)

            df = pl.read_parquet(io.BytesIO(b"".join(data)))
            all_dfs.append(df)

        # Combine all batches
        df = pl.concat(all_dfs)

        # Pivot the data: one row per date/entity with positive, neutral, negative columns
        return (
            df.with_columns(
                pl.from_epoch(pl.col("publish_time"), time_unit="d").alias("date"),
                pl.col("entity_compound_key").cast(pl.Utf8).alias("entity"),
            )
            .pivot(on="sentiment", index=["date", "entity"], values="count()", aggregate_function="sum")
            .rename({"1": "positive", "0": "neutral", "-1": "negative"})
            .select(["date", "entity", "positive", "neutral", "negative"])
            .sort("date", "entity")
        )

    def close(self) -> None:
        """Close the HTTP client and release resources."""
        self._client.close()

    def __enter__(self) -> Self:
        """Enter the context manager."""
        return self

    def __exit__(self, exc_type, exc_val, exc_tb) -> None:
        """Exit the context manager and close the client."""
        self.close()


@dataclass
class Session:
    """High-level session for working with YUKKA APIs.

    Session provides a convenient interface that:
    - Auto-loads token from environment variables
    - Provides entity resolution via the resolver property
    - Accepts Asset objects in API methods
    - Can be used as a context manager for automatic resource cleanup

    Example:
        >>> with Session() as session:
        ...     bmw = Asset.from_isin("DE0005190003", session.resolver)
        ...     df = session.sentiment([bmw], date_from="2026-01-01")
    """

    _client: YukkaClient = field(repr=False)
    _resolver: EntityResolver = field(repr=False)

    def __init__(self, token: str | None = None):
        """Create a new Session.

        Token is resolved in this order:
        1. Explicit token parameter
        2. YUKKA_TOKEN environment variable

        Args:
            token: Optional JWT token

        Raises:
            ValueError: If no token is found
        """
        from .entities import EntityResolver

        # Resolve token
        if token:
            self._client = YukkaClient.from_token(token)
        elif os.environ.get("YUKKA_TOKEN"):
            self._client = YukkaClient.from_token(os.environ["YUKKA_TOKEN"])
        else:
            raise ValueError("No token found. Provide token parameter or set YUKKA_TOKEN environment variable.")

        self._resolver = EntityResolver(
            _client=self._client._client,
            _token_getter=self._client._token.get,
        )

    @property
    def today(self) -> date:
        """Return today's date."""
        return date.today()

    @property
    def resolver(self) -> EntityResolver:
        """Return the entity resolver for converting ISINs/RICs to YUKKA IDs."""
        return self._resolver

    def sentiment(
        self,
        assets: Asset | list[Asset],
        date_from: IntoDateTime,
        date_to: IntoDateTime | None = None,
        batch_size: int = 50,
    ) -> pl.DataFrame:
        """Fetch sentiment scores for assets over a time range.

        Args:
            assets: Single Asset or list of Assets
            date_from: Start date
            date_to: End date (defaults to today)
            batch_size: Number of entities per API request (default 50)

        Returns:
            DataFrame with sentiment scores

        Example:
            >>> session = Session()
            >>> bmw = Asset.from_yukka_id("company:bmw")
            >>> df = session.sentiment(bmw, date_from="2026-01-01")
        """
        from .entities import Asset

        if date_to is None:
            date_to = self.today

        # Normalize to list
        if isinstance(assets, Asset):
            assets = [assets]

        # Extract yukka_ids from assets
        entities = [asset.yukka_id for asset in assets]

        return self._client.sentiment(entities, date_from, date_to, batch_size)

    def close(self) -> None:
        """Close the session and release resources."""
        self._client.close()

    def __enter__(self) -> Self:
        """Enter the context manager."""
        return self

    def __exit__(self, exc_type, exc_val, exc_tb) -> None:
        """Exit the context manager and close the session."""
        self.close()
