from __future__ import annotations

import json
import os
from dataclasses import dataclass
from pathlib import Path
from typing import Any


class ConfigurationError(RuntimeError):
    pass


def load_dotenv_safely(paths: list[Path]) -> None:
    """Load simple KEY=VALUE pairs without ever printing values."""
    for path in paths:
        if not path.is_file():
            continue
        for raw in path.read_text(encoding="utf-8", errors="ignore").splitlines():
            line = raw.strip()
            if not line or line.startswith("#") or "=" not in line:
                continue
            key, value = line.split("=", 1)
            key = key.strip()
            value = value.strip().strip("'").strip('"')
            if key and key not in os.environ:
                os.environ[key] = value


@dataclass(frozen=True)
class ExperimentConfig:
    root: Path
    raw: dict[str, Any]

    @classmethod
    def load(cls, root: Path) -> "ExperimentConfig":
        root = root.resolve()
        load_dotenv_safely(
            [
                root / ".env.local",
                root.parent / ".env.local",
                root / ".env",
                root.parent / ".env",
            ]
        )
        path = root / "config" / "experiment.json"
        try:
            raw = json.loads(path.read_text(encoding="utf-8"))
        except (OSError, json.JSONDecodeError) as exc:
            raise ConfigurationError(f"Cannot load {path}: {exc}") from exc
        return cls(root=root, raw=raw)

    @property
    def experiment_id(self) -> str:
        return str(self.raw["experiment_id"])

    @property
    def max_agent_steps(self) -> int:
        return int(self.raw["max_agent_steps"])

    @property
    def live_enabled(self) -> bool:
        file_gate = bool(self.raw.get("live_enabled", False))
        env_gate = os.getenv("PFB_LIVE_ENABLED", "").strip().lower() == "true"
        return file_gate and env_gate

    def model(self, provider: str) -> str:
        value = str(self.raw.get("models", {}).get(provider, "")).strip()
        if not value:
            raise ConfigurationError(f"No frozen model configured for {provider}")
        return value

    def provider_budget(self, provider: str) -> float:
        return float(self.raw["hard_budget_usd"].get(provider, 0.0))

    @property
    def total_budget(self) -> float:
        return float(self.raw["hard_budget_usd"]["total"])

    def pricing(self, provider: str) -> tuple[float, float]:
        entry = self.raw["pricing_usd_per_million_tokens"].get(provider, {})
        return float(entry.get("input", 0.0)), float(entry.get("output", 0.0))

    def api_key(self, provider: str) -> str:
        names = {
            "openai": "OPENAI_API_KEY",
            "anthropic": "ANTHROPIC_API_KEY",
            "gemini": "GEMINI_API_KEY",
        }
        env_name = names[provider]
        value = os.getenv(env_name, "").strip()
        if not value:
            raise ConfigurationError(f"{env_name} is not configured")
        return value

    def assert_live_ready(self, provider: str) -> None:
        if not self.live_enabled:
            raise ConfigurationError(
                "Live execution is disabled by the two-part configuration gate"
            )
        if not bool(self.raw.get("debug_only", True)):
            raise ConfigurationError("Pre-pilot package permits DEBUG runs only")
        if self.provider_budget(provider) <= 0 and provider != "gemini":
            raise ConfigurationError(f"No positive budget configured for {provider}")
        input_price, output_price = self.pricing(provider)
        if provider != "gemini" and (input_price <= 0 or output_price <= 0):
            raise ConfigurationError(
                f"Frozen nonzero pricing is required before live {provider} runs"
            )
        self.model(provider)
        self.api_key(provider)

    def assert_study_ready(self, provider: str) -> None:
        self.assert_live_ready(provider)
        file_gate = bool(self.raw.get("study_enabled", False))
        env_gate = os.getenv("PFB_STUDY_ENABLED", "").strip().lower() == "true"
        if not (file_gate and env_gate):
            raise ConfigurationError(
                "Definitive study execution is disabled by its two-part gate"
            )
        study = self.raw.get("definitive_study", {})
        if int(study.get("repetitions", 0)) <= 0:
            raise ConfigurationError("Definitive study repetitions are not frozen")
        if str(study.get("scenario_scope", "")) != "all_40":
            raise ConfigurationError("Definitive study must use the frozen all_40 scope")
