import numpy as np
from typing import Tuple
from finRav.utils import TimeScaler
from finRav.stoch import fBM
from finRav.price_models._price_model_base import PriceModel
from finRav.price_models._lognormal_model import LogNormalPriceModel
from fontTools.unicodedata import block


class MonteCarloSimulator:
    """
    Monte Carlo Simulator
    =====================

    General-purpose Monte Carlo engine for simulating price paths using
    a stochastic driver (fractional Brownian motion) and a provided
    price model (e.g., LogNormal or Normal).

    This class acts as a wrapper that handles:
      • time scaling (`dt`) based on trading intervals and units,
      • the generation of stochastic paths (via `fBM`),
      • evaluation of the price model on those paths.

    Examples
    --------
    >>> from finRav.price_models._lognormal_model import LogNormalPriceModel
    >>> model = LogNormalPriceModel(S0=100, mu=0.05, sigma=0.2, H=0.5)
    >>> sim = MonteCarloSimulator(model, n_paths=3, path_length=252, interval="1D", unit="1Y")
    >>> t, S = sim.simulate_paths()
    >>> S.shape
    (3, 252)

    Notes
    -----
    - The model defines the price dynamics:
        S_t = S₀ * exp(μt - ½σ²t^{2H} + σB_t^H)  for LogNormal,
        S_t = S₀ + μt + σB_t^H                    for Normal.
    - For H = 0.5, fBM reduces to standard Brownian motion (GBM / Bachelier).

    Parameters
    ----------
    model : PriceModel
        The price model instance (e.g., LogNormalPriceModel, NormalPriceModel).
    n_paths : int
        Number of Monte Carlo sample paths to simulate.
    path_length : int
        Number of time steps in each path.
    interval : str, default='1D'
        Bar size (e.g. '1m', '5m', '1H', '1D').
    unit : {'1D', '1Y'}, default='1D'
        Base time unit — fraction of a trading day or year.
    fbm_method : {'Hosking', 'DaviesHarte'}, default='DaviesHarte'
        Backend for fractional Brownian motion generation.
    """

    def __init__(self, model: PriceModel, n_paths: int, path_length: int, interval: str = "1D", unit: str = "1D",
                 fbm_method: str='DaviesHarte'):

        self.price_model: PriceModel = model
        self.N: int = path_length
        self.M: int = n_paths

        ts = TimeScaler(interval=interval, unit=unit)
        self.dt: float = ts.dt
        self.fbm_method: str = fbm_method

    @property
    def N(self) -> int: return self._N
    @property
    def M(self) -> int: return self._M
    @property
    def price_model(self) -> PriceModel: return self._price_model
    @property
    def dt(self) -> float: return self._dt
    @property
    def fbm_method(self) -> str: return self._fbm_method

    @N.setter
    def N(self, value) -> None:
        if value <= 0:
            raise ValueError("path_length (N) must be positive.")
        self._N = value
    @M.setter
    def M(self, value) -> None:
        if value <= 0:
            raise ValueError("n_paths (M) must be positive.")
        self._M = value
    @dt.setter
    def dt(self, value) -> None:
        if value <= 0:
            raise ValueError("dt must be positive.")
        self._dt = value
    @fbm_method.setter
    def fbm_method(self, value) -> None:
        if value not in ['Hosking', 'DaviesHarte']:
            raise ValueError("fbm_method must be one of ['Hosking', 'DaviesHarte']")
        self._fbm_method = value
    @price_model.setter
    def price_model(self, value) -> None:
        if not isinstance(value, PriceModel):
            raise ValueError("Price Model must be one of [LogNormal, Normal]")
        self._price_model = value

    def simulate_paths(self) -> Tuple[np.ndarray, np.ndarray]:
        """
        Simulate multiple stochastic price paths.

        Returns
        -------
        t : np.ndarray
            Time grid of shape (N,).
        paths : np.ndarray
            Simulated price paths of shape (M, N).
        """
        t: np.ndarray = np.arange(self.N) * self.dt
        paths = np.empty((self.M, self.N))

        for i in range(self.M):
            _, B = fBM(N=self.N, H=self.price_model.H, dt=self.dt, method=self.fbm_method).simulate()
            paths[i, :] = self.price_model.generate(t=t, B=B)
        return t, paths
