"""NYSE breakpoint computation for portfolio sorting.

Computes quantile breakpoints using only NYSE-listed stocks (exchcd == 1),
following the standard Fama-French methodology.

References
----------
Fama, E. F., & French, K. R. (1993). Common risk factors in the returns
    on stocks and bonds. Journal of Financial Economics, 33(1), 3-56.
"""

from __future__ import annotations

import numpy as np
import pandas as pd


def nyse_breakpoints(
    data: pd.DataFrame,
    char_col: str,
    n_portfolios: int = 10,
    date_col: str = "date",
    exchange_col: str = "exchcd",
) -> pd.DataFrame:
    """Compute quantile breakpoints using only NYSE stocks.

    Filters the data to NYSE-listed firms (exchcd == 1), then computes
    quantile cutoffs for ``char_col`` within each date. The resulting
    breakpoints are typically applied to the full cross-section so that
    non-NYSE stocks are assigned to portfolios defined by NYSE quantiles.

    Parameters
    ----------
    data : pd.DataFrame
        Panel data with at least ``date_col``, ``exchange_col``, and
        ``char_col`` columns.
    char_col : str
        Name of the characteristic column to compute breakpoints for.
    n_portfolios : int, default 10
        Number of portfolios (e.g., 10 for deciles). This produces
        ``n_portfolios - 1`` breakpoint columns.
    date_col : str, default "date"
        Name of the date column.
    exchange_col : str, default "exchcd"
        Name of the exchange code column. NYSE stocks have exchcd == 1.

    Returns
    -------
    pd.DataFrame
        DataFrame with columns [date_col, q_1, q_2, ..., q_{n-1}].
        Each q_i is the i-th quantile breakpoint computed from NYSE stocks.
    """
    # Filter to NYSE stocks only (exchcd == 1)
    nyse_mask = data[exchange_col] == 1
    nyse_data = data.loc[nyse_mask, [date_col, char_col]].dropna(subset=[char_col])

    # Quantile fractions: e.g., for n_portfolios=10 -> [0.1, 0.2, ..., 0.9]
    quantile_fracs = np.linspace(0.0, 1.0, n_portfolios + 1)[1:-1]
    labels = [f"q_{i + 1}" for i in range(len(quantile_fracs))]

    # Compute breakpoints per date using a loop to avoid groupby.apply type issues
    rows: list[dict[str, object]] = []
    for dt, group in nyse_data.groupby(date_col, sort=True):
        values = group[char_col].dropna()
        if len(values) == 0:
            continue
        quantiles = np.quantile(values, quantile_fracs)
        row: dict[str, object] = {date_col: dt}
        for lbl, qval in zip(labels, quantiles):
            row[lbl] = float(qval)
        rows.append(row)

    bp = pd.DataFrame(rows)
    return bp
