"""Ticker class for accessing stock data (yfinance-style API)."""

from __future__ import annotations

from datetime import date, timedelta
from typing import TYPE_CHECKING

import pandas as pd

from pyjquants.adapters.endpoints import (
    DAILY_QUOTES,
    DAILY_QUOTES_AM,
    DIVIDENDS,
    FINANCIAL_DETAILS,
    LISTED_INFO,
    STATEMENTS,
)
from pyjquants.domain.info import TickerInfo
from pyjquants.domain.utils import parse_date, parse_period
from pyjquants.infra.client import JQuantsClient
from pyjquants.infra.exceptions import TickerNotFoundError
from pyjquants.infra.session import _get_global_session

if TYPE_CHECKING:
    from pyjquants.domain.models import StockInfo
    from pyjquants.infra.session import Session


class Ticker:
    """Stock ticker for J-Quants API (yfinance-style API).

    Example:
        >>> ticker = Ticker("7203")
        >>> print(ticker.info.name)  # トヨタ自動車
        >>> df = ticker.history(period="30d")
    """

    def __init__(self, code: str, session: Session | None = None) -> None:
        """Initialize ticker with stock code.

        Args:
            code: Stock code (e.g., "7203" for Toyota)
        """
        self.code = str(code)
        self._session = session or _get_global_session()
        self._client = JQuantsClient(self._session)
        self._info_cache: StockInfo | None = None
        self._ticker_info_cache: TickerInfo | None = None

    def __repr__(self) -> str:
        return f"Ticker('{self.code}')"

    # === INFO ===

    def _load_stock_info(self) -> StockInfo:
        """Load stock info from API."""
        if self._info_cache is None:
            infos = self._client.fetch_list(LISTED_INFO, {"code": self.code})
            if not infos:
                raise TickerNotFoundError(self.code)
            self._info_cache = infos[0]
        return self._info_cache

    @property
    def info(self) -> TickerInfo:
        """Stock information (lazy loaded, cached).

        Returns:
            TickerInfo object with name, sector, market, etc.
        """
        if self._ticker_info_cache is None:
            stock_info = self._load_stock_info()
            self._ticker_info_cache = TickerInfo.from_stock_info(stock_info)
        return self._ticker_info_cache

    # === HISTORY ===

    def history(
        self,
        period: str | None = "30d",
        start: str | date | None = None,
        end: str | date | None = None,
    ) -> pd.DataFrame:
        """Get price history (yfinance-style).

        Args:
            period: Time period (e.g., "30d", "1y"). Ignored if start/end provided.
            start: Start date (YYYY-MM-DD string or date object)
            end: End date (YYYY-MM-DD string or date object)

        Returns:
            DataFrame with columns: date, open, high, low, close, volume, adjusted_close
        """
        # Parse dates
        start_date = parse_date(start) if start is not None else None
        end_date = parse_date(end) if end is not None else None

        # If no explicit dates, use period
        if start_date is None and end_date is None:
            days = parse_period(period or "30d")
            end_date = date.today()
            start_date = end_date - timedelta(days=days + 15)  # Buffer for non-trading days

        params = self._client.date_params(code=self.code, start=start_date, end=end_date)
        df = self._client.fetch_dataframe(DAILY_QUOTES, params)

        if df.empty:
            return df

        # Trim to requested period if using period parameter
        if period and start is None and end is None:
            days = parse_period(period)
            df = df.tail(days)

        return df.reset_index(drop=True)

    def history_am(
        self,
        period: str | None = "30d",
        start: str | date | None = None,
        end: str | date | None = None,
    ) -> pd.DataFrame:
        """Get morning session (AM) price history.

        Returns prices from the morning trading session only (9:00-11:30 JST).
        Useful for intraday analysis or when you need morning-only prices.

        Args:
            period: Time period (e.g., "30d", "1y"). Ignored if start/end provided.
            start: Start date (YYYY-MM-DD string or date object)
            end: End date (YYYY-MM-DD string or date object)

        Returns:
            DataFrame with columns: date, open, high, low, close, volume, adjusted_close
        """
        start_date = parse_date(start) if start is not None else None
        end_date = parse_date(end) if end is not None else None

        if start_date is None and end_date is None:
            days = parse_period(period or "30d")
            end_date = date.today()
            start_date = end_date - timedelta(days=days + 15)

        params = self._client.date_params(code=self.code, start=start_date, end=end_date)
        df = self._client.fetch_dataframe(DAILY_QUOTES_AM, params)

        if df.empty:
            return df

        if period and start is None and end is None:
            days = parse_period(period)
            df = df.tail(days)

        return df.reset_index(drop=True)

    # === FINANCIALS ===

    @property
    def financials(self) -> pd.DataFrame:
        """Financial statements."""
        return self._client.fetch_dataframe(STATEMENTS, {"code": self.code})

    @property
    def dividends(self) -> pd.DataFrame:
        """Dividend history."""
        return self._client.fetch_dataframe(DIVIDENDS, {"code": self.code})

    @property
    def financial_details(self) -> pd.DataFrame:
        """Full financial statement data (BS/PL/CF).

        Provides detailed balance sheet, income statement, and cash flow data.
        More comprehensive than the `financials` property.
        """
        return self._client.fetch_dataframe(FINANCIAL_DETAILS, {"code": self.code})


    # === CACHE CONTROL ===

    def refresh(self) -> None:
        """Clear cached data to force fresh fetch on next access."""
        self._info_cache = None
        self._ticker_info_cache = None



# === MODULE-LEVEL FUNCTIONS ===


def download(
    codes: list[str],
    period: str | None = "30d",
    start: str | date | None = None,
    end: str | date | None = None,
    session: Session | None = None,
) -> pd.DataFrame:
    """Download price data for multiple tickers (yfinance-style).

    Args:
        codes: List of stock codes
        period: Time period (e.g., "30d", "1y")
        start: Start date
        end: End date
        session: Optional session

    Returns:
        Wide-format DataFrame with date index and columns for each ticker's close price
    """
    if not codes:
        return pd.DataFrame()

    dfs = []
    for code in codes:
        ticker = Ticker(code, session=session)
        df = ticker.history(period=period, start=start, end=end)
        if not df.empty:
            df = df[["date", "close"]].copy()
            df = df.rename(columns={"close": code})
            dfs.append(df.set_index("date"))

    if not dfs:
        return pd.DataFrame()

    result = dfs[0]
    for df in dfs[1:]:
        result = result.join(df, how="outer")

    return result.reset_index()


def search(
    query: str,
    session: Session | None = None,
) -> list[Ticker]:
    """Search for tickers by name or code.

    Args:
        query: Search query (matches company name or code)
        session: Optional session

    Returns:
        List of matching Ticker objects
    """
    session = session or _get_global_session()
    client = JQuantsClient(session)

    infos = client.fetch_list(LISTED_INFO)
    query_lower = query.lower()

    matching = [
        info
        for info in infos
        if query_lower in info.code.lower()
        or query_lower in info.company_name.lower()
        or (info.company_name_english and query_lower in info.company_name_english.lower())
    ]

    return [Ticker(info.code, session) for info in matching]
