"""
scenarios.py — Fluent builder API for constructing Open FAIR scenarios.

:class:`ScenarioBuilder` provides a readable, chainable interface for
assembling :class:`~pycrq.ontology.FAIRScenario` objects without having
to manually wire together all the dataclasses.

Example
-------
>>> from pycrq import ScenarioBuilder, pert
>>> scenario = (
...     ScenarioBuilder("Ransomware Attack")
...     .asset("Customer Database", "Information", "PII and payment records")
...     .threat("External Hacker", "External Adversarial", "Financially motivated group")
...     .tef(pert(1, 3, 8))
...     .vulnerability(pert(0.1, 0.3, 0.6))
...     .primary_loss(pert(50_000, 250_000, 1_500_000))
...     .time_horizon(1)
...     .build()
... )
"""

from __future__ import annotations

from typing import List, Optional, Union

from .distributions import Distribution
from .ontology import (
    Asset,
    AssetType,
    Control,
    EffectType,
    FAIRScenario,
    FrequencyFactors,
    LossMagnitudeFactors,
    ThreatAgent,
    ThreatType,
)


class ScenarioBuilder:
    """Fluent builder for :class:`~pycrq.ontology.FAIRScenario` objects.

    All setter methods return *self* to allow method chaining.

    Parameters
    ----------
    name: Unique name for the scenario being built.
    """

    def __init__(self, name: str) -> None:
        if not name:
            raise ValueError("Scenario name cannot be empty.")
        self._name = name
        self._asset: Optional[Asset] = None
        self._threat: Optional[ThreatAgent] = None
        self._controls: List[Control] = []
        self._description: str = ""
        self._tags: List[str] = []
        self._effect_type: EffectType = EffectType.CONFIDENTIALITY
        self._time_horizon: float = 1.0

        # Frequency factor distributions
        self._tef: Optional[Distribution] = None
        self._contact_frequency: Optional[Distribution] = None
        self._probability_of_action: Optional[Distribution] = None
        self._threat_capability: Optional[Distribution] = None
        self._control_strength: Optional[Distribution] = None
        self._vulnerability: Optional[Distribution] = None

        # Loss magnitude distributions
        self._primary_loss: Optional[Distribution] = None
        self._productivity: Optional[Distribution] = None
        self._response: Optional[Distribution] = None
        self._replacement: Optional[Distribution] = None
        self._competitive_advantage: Optional[Distribution] = None
        self._fines_judgments: Optional[Distribution] = None
        self._reputation: Optional[Distribution] = None
        self._secondary_loss_ef: Optional[Distribution] = None
        self._secondary_loss_magnitude: Optional[Distribution] = None

    # ------------------------------------------------------------------
    # Asset
    # ------------------------------------------------------------------

    def asset(
        self,
        name: str,
        asset_type: Union[str, AssetType] = AssetType.INFORMATION,
        description: str = "",
    ) -> "ScenarioBuilder":
        """Create and attach an :class:`~pycrq.ontology.Asset`.

        Parameters
        ----------
        name:       Asset name.
        asset_type: :class:`~pycrq.ontology.AssetType` value or string.
        description: Optional free-text description.
        """
        if isinstance(asset_type, str):
            asset_type = AssetType(asset_type)
        self._asset = Asset(name=name, asset_type=asset_type, description=description)
        return self

    def with_asset(self, asset: Asset) -> "ScenarioBuilder":
        """Attach a pre-constructed :class:`~pycrq.ontology.Asset`."""
        if not isinstance(asset, Asset):
            raise TypeError(f"Expected Asset, got {type(asset).__name__}")
        self._asset = asset
        return self

    # ------------------------------------------------------------------
    # Threat Agent
    # ------------------------------------------------------------------

    def threat(
        self,
        name: str,
        threat_type: Union[str, ThreatType] = ThreatType.EXTERNAL_ADVERSARIAL,
        description: str = "",
    ) -> "ScenarioBuilder":
        """Create and attach a :class:`~pycrq.ontology.ThreatAgent`.

        Parameters
        ----------
        name:        Threat agent name.
        threat_type: :class:`~pycrq.ontology.ThreatType` value or string.
        description: Optional free-text description.
        """
        if isinstance(threat_type, str):
            threat_type = ThreatType(threat_type)
        self._threat = ThreatAgent(
            name=name, threat_type=threat_type, description=description
        )
        return self

    def with_threat(self, threat_agent: ThreatAgent) -> "ScenarioBuilder":
        """Attach a pre-constructed :class:`~pycrq.ontology.ThreatAgent`."""
        if not isinstance(threat_agent, ThreatAgent):
            raise TypeError(f"Expected ThreatAgent, got {type(threat_agent).__name__}")
        self._threat = threat_agent
        return self

    # ------------------------------------------------------------------
    # Controls
    # ------------------------------------------------------------------

    def add_control(
        self,
        name: str,
        description: str = "",
        control_type: str = "Preventive",
    ) -> "ScenarioBuilder":
        """Create and append a :class:`~pycrq.ontology.Control`.

        Parameters
        ----------
        name:         Control name.
        description:  What the control does.
        control_type: Category string (e.g., 'Preventive', 'Detective').
        """
        self._controls.append(
            Control(name=name, description=description, control_type=control_type)
        )
        return self

    def with_control(self, control: Control) -> "ScenarioBuilder":
        """Attach a pre-constructed :class:`~pycrq.ontology.Control`."""
        if not isinstance(control, Control):
            raise TypeError(f"Expected Control, got {type(control).__name__}")
        self._controls.append(control)
        return self

    # ------------------------------------------------------------------
    # Metadata helpers
    # ------------------------------------------------------------------

    def describe(self, description: str) -> "ScenarioBuilder":
        """Set the scenario description narrative."""
        self._description = str(description)
        return self

    def tag(self, *tags: str) -> "ScenarioBuilder":
        """Add one or more string tags to the scenario."""
        self._tags.extend(tags)
        return self

    def effect(self, effect_type: Union[str, EffectType]) -> "ScenarioBuilder":
        """Set the CIA effect type."""
        if isinstance(effect_type, str):
            effect_type = EffectType(effect_type)
        self._effect_type = effect_type
        return self

    def time_horizon(self, years: float) -> "ScenarioBuilder":
        """Set the simulation time horizon in years (default 1.0)."""
        if years <= 0:
            raise ValueError(f"time_horizon must be positive; got {years}")
        self._time_horizon = float(years)
        return self

    # ------------------------------------------------------------------
    # Frequency factor setters
    # ------------------------------------------------------------------

    def tef(self, dist: Distribution) -> "ScenarioBuilder":
        """Set the direct Threat Event Frequency distribution."""
        self._tef = dist
        return self

    def contact_frequency(self, dist: Distribution) -> "ScenarioBuilder":
        """Set the Contact Frequency distribution."""
        self._contact_frequency = dist
        return self

    def probability_of_action(self, dist: Distribution) -> "ScenarioBuilder":
        """Set the Probability of Action distribution."""
        self._probability_of_action = dist
        return self

    def threat_capability(self, dist: Distribution) -> "ScenarioBuilder":
        """Set the Threat Capability distribution (0-100 score)."""
        self._threat_capability = dist
        return self

    def control_strength(self, dist: Distribution) -> "ScenarioBuilder":
        """Set the Control Strength distribution (0-100 score)."""
        self._control_strength = dist
        return self

    def vulnerability(self, dist: Distribution) -> "ScenarioBuilder":
        """Set the direct Vulnerability distribution (fraction 0-1)."""
        self._vulnerability = dist
        return self

    # ------------------------------------------------------------------
    # Loss magnitude setters
    # ------------------------------------------------------------------

    def primary_loss(self, dist: Distribution) -> "ScenarioBuilder":
        """Set the aggregate primary loss magnitude distribution."""
        self._primary_loss = dist
        return self

    def loss_productivity(self, dist: Distribution) -> "ScenarioBuilder":
        """Set the Productivity loss category distribution."""
        self._productivity = dist
        return self

    def loss_response(self, dist: Distribution) -> "ScenarioBuilder":
        """Set the Response loss category distribution."""
        self._response = dist
        return self

    def loss_replacement(self, dist: Distribution) -> "ScenarioBuilder":
        """Set the Replacement loss category distribution."""
        self._replacement = dist
        return self

    def loss_competitive_advantage(self, dist: Distribution) -> "ScenarioBuilder":
        """Set the Competitive Advantage loss category distribution."""
        self._competitive_advantage = dist
        return self

    def loss_fines_judgments(self, dist: Distribution) -> "ScenarioBuilder":
        """Set the Fines & Judgments loss category distribution."""
        self._fines_judgments = dist
        return self

    def loss_reputation(self, dist: Distribution) -> "ScenarioBuilder":
        """Set the Reputation loss category distribution."""
        self._reputation = dist
        return self

    def secondary_loss(
        self, slef: Distribution, slm: Distribution
    ) -> "ScenarioBuilder":
        """Set secondary loss event frequency and magnitude distributions.

        Parameters
        ----------
        slef: Secondary Loss Event Frequency — probability (0-1) or annual rate.
        slm:  Secondary Loss Magnitude — cost per secondary loss event.
        """
        self._secondary_loss_ef = slef
        self._secondary_loss_magnitude = slm
        return self

    # ------------------------------------------------------------------
    # Build
    # ------------------------------------------------------------------

    def build(self) -> FAIRScenario:
        """Validate inputs and construct the :class:`~pycrq.ontology.FAIRScenario`.

        Raises
        ------
        ValueError: If required fields (asset, threat, frequency, or loss) are missing.
        """
        errors: List[str] = []

        if self._asset is None:
            errors.append("Asset is required. Use .asset() or .with_asset().")

        if self._threat is None:
            errors.append("ThreatAgent is required. Use .threat() or .with_threat().")

        # Validate frequency inputs
        has_tef = self._tef is not None
        has_cf = self._contact_frequency is not None
        has_vuln = (
            self._vulnerability is not None
            or self._threat_capability is not None
            or self._control_strength is not None
        )
        if not has_tef and not has_cf:
            errors.append(
                "At least one frequency input is required: "
                "tef(), contact_frequency(), or contact_frequency() + probability_of_action()."
            )

        # Validate loss inputs
        has_pl = self._primary_loss is not None
        has_categories = any([
            self._productivity, self._response, self._replacement,
            self._competitive_advantage, self._fines_judgments, self._reputation,
        ])
        if not has_pl and not has_categories:
            errors.append(
                "At least one loss input is required: "
                "primary_loss() or at least one loss category method."
            )

        if errors:
            raise ValueError(
                f"ScenarioBuilder validation failed for '{self._name}':\n"
                + "\n".join(f"  - {e}" for e in errors)
            )

        ff = FrequencyFactors(
            tef=self._tef,
            contact_frequency=self._contact_frequency,
            probability_of_action=self._probability_of_action,
            threat_capability=self._threat_capability,
            control_strength=self._control_strength,
            vulnerability=self._vulnerability,
        )

        lmf = LossMagnitudeFactors(
            primary_loss=self._primary_loss,
            productivity=self._productivity,
            response=self._response,
            replacement=self._replacement,
            competitive_advantage=self._competitive_advantage,
            fines_judgments=self._fines_judgments,
            reputation=self._reputation,
            secondary_loss_event_frequency=self._secondary_loss_ef,
            secondary_loss_magnitude=self._secondary_loss_magnitude,
        )

        return FAIRScenario(
            name=self._name,
            asset=self._asset,
            threat_agent=self._threat,
            frequency_factors=ff,
            loss_magnitude_factors=lmf,
            description=self._description,
            controls=list(self._controls),
            effect_type=self._effect_type,
            time_horizon_years=self._time_horizon,
            tags=list(self._tags),
        )
