"""Configuration utilities for DSPy RLM with Modal.

This module handles environment configuration, including loading .env files,
finding project roots, and guarding against module shadowing issues with Modal.
"""

from __future__ import annotations

import logging
import os
from pathlib import Path

import dspy

from dotenv import load_dotenv

from fleet_rlm._env_utils import env_bool as _env_bool

load_dotenv()  # Load .env from current working directory by default
logger = logging.getLogger(__name__)


def _find_project_root(start: Path) -> Path:
    """Find the project root by locating pyproject.toml.

    Searches upward from the given path through parent directories until
    a pyproject.toml file is found.

    Args:
        start: The starting path to begin searching from.

    Returns:
        Path to the directory containing pyproject.toml, or the start path
        if no pyproject.toml is found in any parent directory.
    """
    for path in [start, *start.parents]:
        if (path / "pyproject.toml").exists():
            return path
    return start


def _load_dotenv(path: Path) -> None:
    """Load environment variables from a .env file.

    Parses the file line by line, handling comments (lines starting with #),
    empty lines, and quoted values. Only sets variables that are not already
    present in the environment.

    Args:
        path: Path to the .env file.
    """
    if not path.exists():
        return

    for raw in path.read_text().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()
        if len(value) >= 2 and (
            (value[0] == value[-1] == '"') or (value[0] == value[-1] == "'")
        ):
            value = value[1:-1]

        if key and key not in os.environ:
            os.environ[key] = value


# _env_bool imported from fleet_rlm._env_utils


def load_posthog_settings_from_env() -> dict[str, object]:
    """Load PostHog analytics settings from environment variables."""
    from fleet_rlm.analytics.config import (
        PROJECT_POSTHOG_DEFAULT_API_KEY,
        PROJECT_POSTHOG_DEFAULT_HOST,
    )

    api_key = (
        os.getenv("POSTHOG_API_KEY") or ""
    ).strip() or PROJECT_POSTHOG_DEFAULT_API_KEY
    host = (os.getenv("POSTHOG_HOST") or "").strip() or PROJECT_POSTHOG_DEFAULT_HOST
    enabled_raw = os.getenv("POSTHOG_ENABLED")
    return {
        "enabled": _env_bool(enabled_raw, default=bool(api_key)),
        "api_key": api_key,
        "host": host,
        "flush_interval": float(os.getenv("POSTHOG_FLUSH_INTERVAL", "10.0")),
        "flush_at": max(1, int(os.getenv("POSTHOG_FLUSH_AT", "10"))),
        "enable_dspy_optimization": _env_bool(
            os.getenv("POSTHOG_ENABLE_DSPY_OPTIMIZATION"), default=False
        ),
        "input_truncation_chars": max(
            1, int(os.getenv("POSTHOG_INPUT_TRUNCATION", "10000"))
        ),
        "output_truncation_chars": max(
            1, int(os.getenv("POSTHOG_OUTPUT_TRUNCATION", "5000"))
        ),
        "redact_sensitive": _env_bool(
            os.getenv("POSTHOG_REDACT_SENSITIVE"), default=True
        ),
        "distinct_id": os.getenv("POSTHOG_DISTINCT_ID") or None,
    }


def configure_posthog_analytics_from_env() -> object | None:
    """Best-effort env-driven analytics setup (non-blocking and idempotent)."""
    settings = load_posthog_settings_from_env()
    if not settings.get("enabled") or not settings.get("api_key"):
        return None

    try:
        from fleet_rlm.analytics import configure_analytics
    except Exception:
        return None

    try:
        return configure_analytics(
            api_key=settings["api_key"]
            if isinstance(settings["api_key"], str)
            else None,
            host=settings["host"]
            if isinstance(settings["host"], str)
            else "https://eu.i.posthog.com",
            distinct_id=settings["distinct_id"]
            if isinstance(settings["distinct_id"], str)
            else None,
            enabled=True,
        )
    except Exception:
        return None


def _prepare_env(*, env_file: Path | None = None) -> None:
    """Load env defaults and shared runtime guards for LM configuration helpers."""
    dotenv_path = env_file
    if dotenv_path is None:
        project_root = _find_project_root(Path.cwd())
        dotenv_path = project_root / ".env"

    _load_dotenv(dotenv_path)
    _guard_modal_shadowing()
    configure_posthog_analytics_from_env()


def _guard_modal_shadowing() -> None:
    """Guard against module shadowing that can break Modal imports.

    Checks for and handles shadowing issues:
    - A local 'modal.py' file that shadows the modal package
    - Compiled bytecode files (__pycache__/modal.*.pyc) from previous shadowing

    Raises:
        RuntimeError: If a modal.py shadow file exists (user must rename/delete),
            or if bytecode files exist but cannot be removed.
    """
    shadow_py = Path.cwd() / "modal.py"
    shadow_pyc_dir = Path.cwd() / "__pycache__"
    shadow_pycs = (
        list(shadow_pyc_dir.glob("modal.*.pyc")) if shadow_pyc_dir.exists() else []
    )

    if shadow_py.exists():
        raise RuntimeError(
            f"Found {shadow_py} which shadows the 'modal' package. "
            "Rename/delete it and restart your shell or kernel."
        )

    failed: list[str] = []
    for pyc in shadow_pycs:
        try:
            pyc.unlink()
        except OSError:
            failed.append(str(pyc))

    if failed:
        raise RuntimeError(
            "Found shadowing bytecode files but could not remove them:\n"
            + "\n".join(failed)
            + "\nDelete them manually and retry."
        )


def configure_planner_from_env(*, env_file: Path | None = None) -> bool:
    """Configure DSPy's planner LM from environment variables.

    Loads environment variables from a .env file (if found) and configures
    DSPy with a language model based on the loaded configuration.

    Required environment variables:
        - DSPY_LM_MODEL: The model identifier (e.g., "openai/gemini/gemini-3.1-pro-preview")
        - DSPY_LLM_API_KEY or DSPY_LM_API_KEY: API key for the model provider

    Optional environment variables:
        - DSPY_LM_API_BASE: Custom API base URL
        - DSPY_LM_MAX_TOKENS: Maximum tokens for generation (default: 16000)

    Also guards against modal module shadowing issues.

    Args:
        env_file: Optional path to a specific .env file. If not provided,
            searches for .env in the project root (directory containing
            pyproject.toml) or current working directory.

    Returns:
        True if the planner was successfully configured, False if required
        environment variables (DSPY_LM_MODEL and API key) are not set.

    Example:
        >>> from fleet_rlm import configure_planner_from_env
        >>> success = configure_planner_from_env()
        >>> if not success:
        ...     print("Failed to configure planner - check environment variables")
    """

    _prepare_env(env_file=env_file)

    api_key = os.environ.get("DSPY_LLM_API_KEY") or os.environ.get("DSPY_LM_API_KEY")
    model = os.environ.get("DSPY_LM_MODEL")

    if not model or not api_key:
        return False

    planner_lm = dspy.LM(
        model,
        api_base=os.environ.get("DSPY_LM_API_BASE"),
        api_key=api_key,
        max_tokens=int(os.environ.get("DSPY_LM_MAX_TOKENS", "64000")),
    )
    dspy.configure(lm=planner_lm)
    return True


def get_planner_lm_from_env(
    *, env_file: Path | None = None, model_name: str | None = None
) -> dspy.LM | None:
    """Create and return a DSPy LM from environment.

    This is the async-safe version of configure_planner_from_env(). It creates
    and returns the LM object without calling dspy.configure(), allowing the
    caller to use dspy.context() for thread-local configuration instead.

    Args:
        env_file: Optional path to a specific .env file.
        model_name: Optional explicit model identifier to use, overriding environment.

    Returns:
        A configured dspy.LM instance if configuration is available, None otherwise.
    """
    _prepare_env(env_file=env_file)

    api_key = os.environ.get("DSPY_LLM_API_KEY") or os.environ.get("DSPY_LM_API_KEY")
    model = model_name or os.environ.get("DSPY_LM_MODEL")

    if not model or not api_key:
        return None

    return dspy.LM(
        model,
        api_base=os.environ.get("DSPY_LM_API_BASE"),
        api_key=api_key,
        max_tokens=int(os.environ.get("DSPY_LM_MAX_TOKENS", "64000")),
    )


def get_delegate_lm_from_env(
    *,
    env_file: Path | None = None,
    model_name: str | None = None,
    default_api_key: str | None = None,
    default_api_base: str | None = None,
    default_max_tokens: int | None = None,
) -> dspy.LM | None:
    """Create and return an optional delegate DSPy LM from environment.

    Resolution policy:
    - model: explicit ``model_name`` -> ``DSPY_DELEGATE_LM_MODEL`` -> ``None``
    - api key: ``DSPY_DELEGATE_LM_API_KEY`` -> ``default_api_key`` -> planner key envs
    - api base: ``DSPY_DELEGATE_LM_API_BASE`` -> ``default_api_base`` -> planner base env

    This helper is intentionally best-effort and returns ``None`` on missing
    inputs or init failures so callers can fall back to the parent planner LM.
    """
    _prepare_env(env_file=env_file)

    model = model_name or os.environ.get("DSPY_DELEGATE_LM_MODEL")
    if not model:
        return None

    api_key = (
        os.environ.get("DSPY_DELEGATE_LM_API_KEY")
        or default_api_key
        or os.environ.get("DSPY_LLM_API_KEY")
        or os.environ.get("DSPY_LM_API_KEY")
    )
    if not api_key:
        logger.warning(
            "Delegate LM model is configured but no API key is available; using planner fallback."
        )
        return None

    raw_max_tokens = default_max_tokens
    if raw_max_tokens is None:
        try:
            raw_max_tokens = int(os.environ.get("DSPY_LM_MAX_TOKENS", "64000"))
        except (TypeError, ValueError):
            raw_max_tokens = 64000

    try:
        return dspy.LM(
            model,
            api_base=(
                os.environ.get("DSPY_DELEGATE_LM_API_BASE")
                or default_api_base
                or os.environ.get("DSPY_LM_API_BASE")
            ),
            api_key=api_key,
            max_tokens=int(raw_max_tokens),
        )
    except Exception:
        logger.warning(
            "Failed to initialize delegate LM '%s'; using planner fallback.",
            model,
            exc_info=True,
        )
        return None
