"""AutomationProfileService for resolving profiles with precedence.

Resolves the effective automation profile for a plan execution using a
four-level precedence hierarchy:

1. **Plan-level** -- explicitly set via ``--automation-profile`` on use.
2. **Action-level** -- default profile set on the action template.
3. **Project-level** -- set via project configuration.
4. **Global-level** -- set via config key or ``CLEVERAGENTS_AUTOMATION_PROFILE``
   environment variable.

Based on ``docs/specification.md`` Section "Automation Profiles".
"""

from __future__ import annotations

import logging
import os
from pathlib import Path
from typing import TYPE_CHECKING, Any

from cleveragents.core.exceptions import (
    NotFoundError,
    ValidationError,
)
from cleveragents.domain.models.core.automation_guard import GuardScope
from cleveragents.domain.models.core.automation_profile import (
    BUILTIN_PROFILES,
    AutomationProfile,
    GuardResult,
)

if TYPE_CHECKING:
    from cleveragents.infrastructure.database.repositories import (
        AutomationProfileRepository,
    )

logger = logging.getLogger(__name__)

_ENV_VAR = "CLEVERAGENTS_AUTOMATION_PROFILE"
_DEFAULT_PROFILE = "manual"


class AutomationProfileService:
    """Application service for automation profile resolution.

    Resolves the effective profile with precedence:
    plan > action > project > global.

    Supports CRUD via the underlying repository.
    """

    def __init__(
        self,
        repo: AutomationProfileRepository | None = None,
        global_default: str | None = None,
    ) -> None:
        """Initialise with optional repository and global default.

        Args:
            repo: Repository for persisted custom profiles.
                When ``None``, only built-in profiles are available.
            global_default: Global default profile name from config.
                Falls back to ``CLEVERAGENTS_AUTOMATION_PROFILE``
                env var, then ``"manual"``.
        """
        self._repo = repo
        self._global_default = global_default

    @property
    def effective_global_default(self) -> str:
        """Return the resolved global default profile name.

        Precedence: explicit config > env var > ``"manual"``.
        """
        if self._global_default:
            return self._global_default
        env_val = os.environ.get(_ENV_VAR)
        if env_val:
            return env_val
        return _DEFAULT_PROFILE

    def resolve_profile(
        self,
        plan_profile: str | None = None,
        action_profile: str | None = None,
        project_profile: str | None = None,
    ) -> AutomationProfile:
        """Resolve the effective profile using precedence.

        Precedence: plan > action > project > global.

        Args:
            plan_profile: Profile name set at plan level.
            action_profile: Profile name set on the action.
            project_profile: Profile name from project config.

        Returns:
            The resolved ``AutomationProfile``.

        Raises:
            NotFoundError: If the resolved name is not a
                known built-in and not found in the repository.
        """
        resolved_name = (
            plan_profile
            or action_profile
            or project_profile
            or self.effective_global_default
        )
        logger.debug(
            "Resolving profile '%s' (plan=%s, action=%s, project=%s, global=%s)",
            resolved_name,
            plan_profile,
            action_profile,
            project_profile,
            self.effective_global_default,
        )
        return self.get_profile(resolved_name)

    def get_profile(self, name: str) -> AutomationProfile:
        """Look up a profile by name.

        Checks built-in profiles first, then the persistence
        layer.

        Args:
            name: Profile name (bare or namespaced).

        Returns:
            The ``AutomationProfile``.

        Raises:
            NotFoundError: If the profile is not found.
        """
        # Check built-ins first
        if name in BUILTIN_PROFILES:
            return BUILTIN_PROFILES[name]

        # Check repository
        if self._repo is not None:
            profile = self._repo.get_by_name(name)
            if profile is not None:
                return profile

        raise NotFoundError(
            f"Automation profile '{name}' not found. "
            f"Available built-ins: "
            f"{', '.join(sorted(BUILTIN_PROFILES))}",
            resource_type="automation_profile",
            resource_id=name,
        )

    # ----- CRUD operations via repository ----- #

    def list_profiles(self) -> list[AutomationProfile]:
        """List all profiles (built-in + persisted).

        Returns:
            Combined list of built-in and custom profiles.
        """
        profiles = list(BUILTIN_PROFILES.values())
        if self._repo is not None:
            custom = self._repo.list_all()
            profiles.extend(custom)
        return profiles

    def create_profile(self, config: dict[str, Any]) -> AutomationProfile:
        """Create and persist a custom profile.

        Args:
            config: Profile configuration dict (must include
                ``name``).

        Returns:
            The created ``AutomationProfile``.

        Raises:
            ValidationError: If the config is invalid or name
                conflicts with a built-in.
        """
        name = config.get("name", "")
        if name in BUILTIN_PROFILES:
            raise ValidationError(
                f"Cannot create profile '{name}': conflicts with built-in profile"
            )
        profile = AutomationProfile.from_config(config)
        if self._repo is not None:
            self._repo.upsert(profile)
        logger.info("Created profile '%s'", profile.name)
        return profile

    def update_profile(
        self,
        name: str,
        config: dict[str, Any],
    ) -> AutomationProfile:
        """Update a persisted custom profile.

        Args:
            name: Profile name to update.
            config: Updated configuration.

        Returns:
            The updated ``AutomationProfile``.

        Raises:
            ValidationError: If the config is invalid.
            NotFoundError: If the profile is not found or is
                built-in.
        """
        if name in BUILTIN_PROFILES:
            raise ValidationError(f"Cannot update built-in profile '{name}'")
        config["name"] = name
        profile = AutomationProfile.from_config(config)
        if self._repo is not None:
            self._repo.upsert(profile)
        else:
            raise NotFoundError(
                f"No repository configured; cannot update profile '{name}'",
                resource_type="automation_profile",
                resource_id=name,
            )
        logger.info("Updated profile '%s'", name)
        return profile

    def delete_profile(self, name: str) -> None:
        """Delete a persisted custom profile.

        Args:
            name: Profile name to delete.

        Raises:
            ValidationError: If the name is a built-in.
            NotFoundError: If the profile does not exist.
        """
        if name in BUILTIN_PROFILES:
            raise ValidationError(f"Cannot delete built-in profile '{name}'")
        if self._repo is not None:
            self._repo.delete(name)
        else:
            raise NotFoundError(
                f"No repository configured; cannot delete profile '{name}'",
                resource_type="automation_profile",
                resource_id=name,
            )
        logger.info("Deleted profile '%s'", name)

    # ----- Guard evaluation ----- #

    def evaluate_guard(
        self,
        profile_name: str,
        tool_name: str,
        context: dict[str, Any] | None = None,
    ) -> GuardResult:
        """Evaluate guard constraints for a tool invocation.

        Looks up the profile by *profile_name*, then delegates to
        :meth:`AutomationProfile.check_guard`.

        Args:
            profile_name: Name of the automation profile to use.
            tool_name: Name of the tool being invoked.
            context: Optional context dict with keys ``is_write``
                (bool), ``cost_so_far`` (float), ``calls_so_far``
                (int), and ``scope`` (:class:`GuardScope`).

        Returns:
            A :class:`GuardResult` indicating the decision.

        Raises:
            NotFoundError: If the profile is not found.
        """
        if not profile_name:
            raise ValidationError("profile_name must not be empty")
        if not tool_name:
            raise ValidationError("tool_name must not be empty")

        profile = self.get_profile(profile_name)
        ctx = context or {}
        return profile.check_guard(
            tool_name=tool_name,
            is_write=ctx.get("is_write", False),
            cost_so_far=ctx.get("cost_so_far", 0.0),
            calls_so_far=ctx.get("calls_so_far", 0),
            scope=ctx.get("scope", GuardScope.PLAN),
        )

    def get_effective_profile(
        self,
        plan_profile: str | None = None,
        action_profile: str | None = None,
        default_profile: str | None = None,
    ) -> AutomationProfile:
        """Resolve the effective profile from plan -> action -> default.

        This is a convenience wrapper around :meth:`resolve_profile`
        that uses the three-level precedence: plan > action > default.

        Args:
            plan_profile: Profile name set at plan level.
            action_profile: Profile name set on the action.
            default_profile: Fallback default profile name.

        Returns:
            The resolved ``AutomationProfile``.
        """
        return self.resolve_profile(
            plan_profile=plan_profile,
            action_profile=action_profile,
            project_profile=default_profile,
        )

    @staticmethod
    def from_yaml(path: str | Path) -> AutomationProfile:
        """Load an AutomationProfile from a YAML config file.

        Delegates to :meth:`AutomationProfile.from_yaml`.

        Args:
            path: Path to the YAML configuration file.

        Returns:
            The loaded ``AutomationProfile``.
        """
        return AutomationProfile.from_yaml(path)
