import numpy as np
from abc import ABC, abstractmethod


class PriceModel(ABC):
    """
    Base class for all price models (normal / lognormal, fractional or not).

    Parameters
    ----------
    S0 : float
        Initial asset price.
    mu : float
        Drift (per chosen time unit).
    sigma : float
        Volatility (per chosen time unit).
    H : float, default=0.5
        Hurst exponent. H=0.5 ⇒ standard Brownian; H≠0.5 ⇒ fractional Brownian.

    Notes
    -----
    Subclasses must implement:
        generate(B, t) → np.ndarray
    where B is the driver path (Brownian or fractional Brownian motion)
    and t is the corresponding time grid.
    """

    def __init__(self, S0: float, mu: float, sigma: float, H: float = 0.5):
        self.S0: float = S0
        self.mu: float = mu
        self.sigma: float = sigma
        self.H: float = H

    @property
    def S0(self) -> float: return self._S0

    @property
    def H(self) -> float: return self._H

    @property
    def mu(self) -> float: return self._mu

    @property
    def sigma(self) -> float: return self._sigma

    @S0.setter
    def S0(self, value) -> None:
        if value < 0:
            raise ValueError("Stock/Asset price must be >= 0 $")
        self._S0 = value

    @mu.setter
    def mu(self, value) -> None:
        self._mu = value

    @H.setter
    def H(self, value: float) -> None:
        """Validate and set H (must be in range (0,1))."""
        if not (0 <= value <= 1):
            raise ValueError("Hurst exponent must be in range (0, 1)")
        self._H = value

    @sigma.setter
    def sigma(self, value) -> None:
        if value < 0:
            raise ValueError("Volatility must be >= 0")
        self._sigma = value

    @abstractmethod
    def generate(self, B: np.ndarray, t: np.ndarray) -> np.ndarray:
        """Generate price path S_t given driver path B_t and time grid t."""
        pass
