"""Automation Profile domain model for CleverAgents v3.

Defines threshold values controlling plan-execution autonomy.
Each threshold is a float in [0.0, 1.0]: 0.0 = automatic,
1.0 = requires human approval.  Eight built-in profiles ship
with every installation.  Custom profiles use ``namespace/name``.

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

Field Name Mapping
------------------

The specification defines 11 threshold fields using **task-type**
semantics (what the operation *does*) rather than
**phase-transition** semantics (which lifecycle phase it gates).
The mapping between old and new names is:

Old (phase-transition)        -> New (spec task-type)
``auto_strategize``           -> ``decompose_task``
    Enter Strategize after plan use.
``auto_execute``              -> ``create_tool``
    Strategize -> Execute transition.
``auto_apply``                -> ``select_tool``
    Execute -> Apply transition.
``auto_decisions_strategize`` -> ``edit_code``
    Decisions during Strategize.
``auto_decisions_execute``    -> ``execute_command``
    Decisions during Execute.
``auto_validation_fix``       -> ``create_file``
    Auto-fix validation failures.
``auto_strategy_revision``    -> ``delete_content``
    Revise strategy from Execute constraints.
``auto_reversion_from_apply`` -> ``access_network``
    Revert from Apply to Strategize.
``auto_child_plans``          -> ``install_dependency``
    Spawn and execute child plans.
``auto_retry_transient``      -> ``modify_config``
    Retry on transient failures.
``auto_checkpoint_restore``   -> ``approve_plan``
    Restore from checkpoint on failure.

The ``_LEGACY_FIELD_MAP`` dict below encodes this mapping for
runtime detection of old field names in user-supplied YAML
configs.  See also
``PlanLifecycleService.should_auto_progress()`` for how the
task-type names map back to phase-transition gates at runtime.
"""

from __future__ import annotations

import re
from pathlib import Path
from typing import Any, ClassVar

import yaml
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator

from cleveragents.domain.models.core.automation_guard import (
    REMEDIATION_ALLOWLIST,
    REMEDIATION_APPLY_APPROVAL,
    REMEDIATION_BUDGET,
    REMEDIATION_DENYLIST,
    REMEDIATION_TOOL_CALL_LIMIT,
    REMEDIATION_WRITE_APPROVAL,
    AutomationGuard,
    GuardResult,
    GuardScope,
)
from cleveragents.domain.models.core.safety_profile import (
    DEFAULT_SAFETY_PROFILE,
    SafetyProfile,
)

# ---------------------------------------------------------------------------
# Regex patterns
# ---------------------------------------------------------------------------

_BARE_NAME = re.compile(r"^[a-zA-Z0-9_-]+$")
_NAMESPACED_NAME = re.compile(r"^[a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+$")

# Mapping from legacy (pre-rename) field names to the current
# spec-defined task-type names.  Used by the ``@model_validator`` to
# produce an actionable error when users supply YAML configs with
# the old phase-transition field names.
_LEGACY_FIELD_MAP: dict[str, str] = {
    "auto_strategize": "decompose_task",
    "auto_execute": "create_tool",
    "auto_apply": "select_tool",
    "auto_decisions_strategize": "edit_code",
    "auto_decisions_execute": "execute_command",
    "auto_validation_fix": "create_file",
    "auto_strategy_revision": "delete_content",
    "auto_reversion_from_apply": "access_network",
    "auto_child_plans": "install_dependency",
    "auto_retry_transient": "modify_config",
    "auto_checkpoint_restore": "approve_plan",
}


# ---------------------------------------------------------------------------
# AutomationProfile
# ---------------------------------------------------------------------------


class AutomationProfile(BaseModel):
    """Domain model for an Automation Profile.

    Controls autonomy thresholds for every phase of plan execution.
    All threshold fields are floats in [0.0, 1.0] where 0.0 means
    fully automatic and 1.0 means human approval is always required.
    """

    # -- Identity ----------------------------------------------------------

    name: str = Field(
        ...,
        min_length=1,
        description=(
            "Profile name: bare built-in name (e.g. 'manual') "
            "or namespaced 'namespace/name' (e.g. 'acme/strict')"
        ),
    )
    description: str = Field(
        "",
        description="Human-readable description of the profile",
    )

    # -- Schema version ----------------------------------------------------

    schema_version: str = Field(
        default="1.0",
        description="Schema version for forward compatibility",
    )

    # -- Task-type confidence thresholds (0.0 = auto, 1.0 = human) ---------

    decompose_task: float = Field(
        0.0,
        ge=0.0,
        le=1.0,
        description="Threshold for automatic task decomposition",
    )
    create_tool: float = Field(
        0.0,
        ge=0.0,
        le=1.0,
        description="Threshold for automatic tool creation",
    )
    select_tool: float = Field(
        0.0,
        ge=0.0,
        le=1.0,
        description="Threshold for automatic tool selection",
    )

    edit_code: float = Field(
        0.0,
        ge=0.0,
        le=1.0,
        description="Threshold for automatic code editing",
    )
    execute_command: float = Field(
        0.0,
        ge=0.0,
        le=1.0,
        description="Threshold for automatic command execution",
    )

    create_file: float = Field(
        0.0,
        ge=0.0,
        le=1.0,
        description="Threshold for automatic file creation",
    )
    delete_content: float = Field(
        0.0,
        ge=0.0,
        le=1.0,
        description="Threshold for automatic content deletion",
    )
    access_network: float = Field(
        0.0,
        ge=0.0,
        le=1.0,
        description="Threshold for automatic network access",
    )

    install_dependency: float = Field(
        0.0,
        ge=0.0,
        le=1.0,
        description="Threshold for automatic dependency installation",
    )
    modify_config: float = Field(
        0.0,
        ge=0.0,
        le=1.0,
        description="Threshold for automatic configuration modification",
    )
    approve_plan: float = Field(
        0.0,
        ge=0.0,
        le=1.0,
        description="Threshold for automatic plan approval",
    )

    # -- Safety profile (composed sub-model) --------------------------------

    safety: SafetyProfile = Field(
        default=DEFAULT_SAFETY_PROFILE,
        description=(
            "Composed safety profile controlling sandbox, checkpoint, "
            "and tool constraints. See ADR-041."
        ),
    )

    # -- Guards ------------------------------------------------------------

    guards: AutomationGuard | None = Field(
        default=None,
        description="Optional enforcement hooks for runtime constraints",
    )

    # -- Name validation ---------------------------------------------------

    @field_validator("name")
    @classmethod
    def validate_name_format(cls: type[AutomationProfile], v: str) -> str:
        """Validate profile name is bare or namespaced."""
        if not (_BARE_NAME.match(v) or _NAMESPACED_NAME.match(v)):
            raise ValueError(
                "Profile name must be a bare name "
                "(e.g. 'manual') or namespaced "
                "'namespace/name' (e.g. 'acme/strict'): "
                f"got '{v}'"
            )
        return v

    # -- Threshold field validation ----------------------------------------

    _THRESHOLD_FIELDS: ClassVar[list[str]] = [
        "decompose_task",
        "create_tool",
        "select_tool",
        "edit_code",
        "execute_command",
        "create_file",
        "delete_content",
        "access_network",
        "install_dependency",
        "modify_config",
        "approve_plan",
    ]

    @field_validator(
        "decompose_task",
        "create_tool",
        "select_tool",
        "edit_code",
        "execute_command",
        "create_file",
        "delete_content",
        "access_network",
        "install_dependency",
        "modify_config",
        "approve_plan",
    )
    @classmethod
    def validate_threshold(cls: type[AutomationProfile], v: float) -> float:
        """Validate threshold is in [0.0, 1.0]."""
        if v < 0.0 or v > 1.0:
            raise ValueError(f"Threshold must be between 0.0 and 1.0, got {v}")
        return v

    # -- Guard checking ----------------------------------------------------

    def check_guard(
        self,
        tool_name: str,
        is_write: bool = False,
        cost_so_far: float = 0.0,
        calls_so_far: int = 0,
        scope: GuardScope = GuardScope.PLAN,
    ) -> GuardResult:
        """Evaluate guard constraints for a tool invocation."""
        if tool_name is None:
            raise ValueError("tool_name cannot be None")
        if not isinstance(tool_name, str):
            raise TypeError("tool_name must be a string")
        if cost_so_far < 0.0:
            raise ValueError(f"cost_so_far must be >= 0.0, got {cost_so_far}")
        if calls_so_far < 0:
            raise ValueError(f"calls_so_far must be >= 0, got {calls_so_far}")
        if self.guards is None:
            return GuardResult(allowed=True)
        guards = self.guards
        if guards.tool_denylist and tool_name in guards.tool_denylist:
            return GuardResult(
                allowed=False,
                requires_approval=True,
                reason=(
                    f"Tool '{tool_name}' is on the denylist for this "
                    f"{scope.value}. {REMEDIATION_DENYLIST}"
                ),
            )
        if guards.tool_allowlist is not None and tool_name not in guards.tool_allowlist:
            return GuardResult(
                allowed=False,
                requires_approval=True,
                reason=(
                    f"Tool '{tool_name}' is not on the allowlist for this "
                    f"{scope.value}. {REMEDIATION_ALLOWLIST}"
                ),
            )
        if (
            guards.max_tool_calls_per_step is not None
            and calls_so_far >= guards.max_tool_calls_per_step
        ):
            limit = guards.max_tool_calls_per_step
            return GuardResult(
                allowed=False,
                requires_approval=True,
                reason=(
                    f"{scope.value.capitalize()} tool call limit reached "
                    f"({calls_so_far}/{limit}). {REMEDIATION_TOOL_CALL_LIMIT}"
                ),
            )
        if guards.max_total_cost is not None and cost_so_far >= guards.max_total_cost:
            return GuardResult(
                allowed=False,
                requires_approval=True,
                reason=(
                    f"Cost budget exceeded for this {scope.value} "
                    f"({cost_so_far:.2f} >= {guards.max_total_cost:.2f}). "
                    f"{REMEDIATION_BUDGET}"
                ),
            )
        if guards.require_approval_for_writes and is_write:
            return GuardResult(
                allowed=False,
                requires_approval=True,
                reason=(
                    f"Write operations require approval in this "
                    f"{scope.value}. {REMEDIATION_WRITE_APPROVAL}"
                ),
            )
        if guards.require_approval_for_apply and tool_name == "__apply__":
            return GuardResult(
                allowed=False,
                requires_approval=True,
                reason=(f"Apply phase requires approval. {REMEDIATION_APPLY_APPROVAL}"),
            )
        return GuardResult(allowed=True)

    # -- Factories ---------------------------------------------------------

    @classmethod
    def from_config(cls, config: dict[str, Any]) -> AutomationProfile:
        """Create an AutomationProfile from a YAML config dict."""
        if "name" not in config:
            raise ValueError("AutomationProfile config must include 'name'")
        return cls.model_validate(config)

    @classmethod
    def from_yaml(cls, path: str | Path) -> AutomationProfile:
        """Create an AutomationProfile from a YAML file.

        Args:
            path: Path to the YAML configuration file.

        Returns:
            The loaded ``AutomationProfile``.

        Raises:
            FileNotFoundError: If the file does not exist.
            ValueError: If the YAML content is invalid.
        """
        file_path = Path(path)
        if not file_path.exists():
            raise FileNotFoundError(f"Profile YAML file not found: {file_path}")
        with open(file_path) as fh:
            config = yaml.safe_load(fh)
        if not isinstance(config, dict):
            raise ValueError("Profile YAML must contain a mapping (dict)")
        return cls.from_config(config)

    # -- Legacy field detection ----------------------------------------------

    @model_validator(mode="before")
    @classmethod
    def reject_legacy_field_names(
        cls: type[AutomationProfile],
        data: Any,
    ) -> Any:
        """Detect legacy phase-transition field names and raise a clear error.

        After the rename to spec-defined task-type semantics, YAML
        configs using the old ``auto_*`` keys are rejected with an
        actionable message that lists the required renames.
        """
        if not isinstance(data, dict):
            return data

        found: list[str] = [k for k in data if k in _LEGACY_FIELD_MAP]
        if found:
            lines = [f"  '{old}' -> '{_LEGACY_FIELD_MAP[old]}'" for old in found]
            raise ValueError(
                "Automation profile contains legacy field names that were "
                "renamed to spec-defined task-type semantics. Please update "
                "your YAML config:\n" + "\n".join(lines)
            )
        return data

    model_config = ConfigDict(
        str_strip_whitespace=True,
        validate_assignment=True,
        extra="forbid",
    )


# ---------------------------------------------------------------------------
# Built-in profiles
# ---------------------------------------------------------------------------

BUILTIN_PROFILES: dict[str, AutomationProfile] = {
    "manual": AutomationProfile(
        name="manual",
        description="Human approves every action",
        schema_version="1.0",
        decompose_task=1.0,
        create_tool=1.0,
        select_tool=1.0,
        edit_code=1.0,
        execute_command=1.0,
        create_file=1.0,
        delete_content=1.0,
        access_network=1.0,
        install_dependency=1.0,
        modify_config=1.0,
        approve_plan=1.0,
        safety=SafetyProfile(
            require_sandbox=True,
            require_checkpoints=True,
            require_human_approval=False,
            allow_unsafe_tools=False,
            max_retries_per_step=3,
        ),
    ),
    "review": AutomationProfile(
        name="review",
        description="Human reviews before apply",
        schema_version="1.0",
        decompose_task=0.0,
        create_tool=0.0,
        select_tool=1.0,
        edit_code=1.0,
        execute_command=1.0,
        create_file=1.0,
        delete_content=1.0,
        access_network=1.0,
        install_dependency=0.0,
        modify_config=0.0,
        approve_plan=1.0,
        safety=SafetyProfile(
            require_sandbox=True,
            require_checkpoints=True,
            require_human_approval=False,
            allow_unsafe_tools=False,
            max_retries_per_step=3,
        ),
    ),
    "supervised": AutomationProfile(
        name="supervised",
        description=("Human reviews strategy and execution"),
        schema_version="1.0",
        decompose_task=0.0,
        create_tool=1.0,
        select_tool=1.0,
        edit_code=0.0,
        execute_command=1.0,
        create_file=1.0,
        delete_content=1.0,
        access_network=1.0,
        install_dependency=1.0,
        modify_config=0.0,
        approve_plan=1.0,
        safety=SafetyProfile(
            require_sandbox=True,
            require_checkpoints=True,
            require_human_approval=False,
            allow_unsafe_tools=False,
            max_retries_per_step=3,
        ),
    ),
    "cautious": AutomationProfile(
        name="cautious",
        description=("Probabilistic gates on most actions"),
        schema_version="1.0",
        decompose_task=0.7,
        create_tool=0.7,
        select_tool=1.0,
        edit_code=0.6,
        execute_command=0.8,
        create_file=0.7,
        delete_content=0.8,
        access_network=0.9,
        install_dependency=0.7,
        modify_config=0.0,
        approve_plan=0.6,
        safety=SafetyProfile(
            require_sandbox=True,
            require_checkpoints=True,
            require_human_approval=False,
            allow_unsafe_tools=False,
            max_retries_per_step=3,
        ),
    ),
    "trusted": AutomationProfile(
        name="trusted",
        description=("Auto for most, human for apply and revert"),
        schema_version="1.0",
        decompose_task=0.0,
        create_tool=0.0,
        select_tool=1.0,
        edit_code=0.0,
        execute_command=0.0,
        create_file=0.0,
        delete_content=1.0,
        access_network=1.0,
        install_dependency=0.0,
        modify_config=0.0,
        approve_plan=1.0,
        safety=SafetyProfile(
            require_sandbox=True,
            require_checkpoints=True,
            require_human_approval=False,
            allow_unsafe_tools=False,
            max_retries_per_step=3,
        ),
    ),
    "auto": AutomationProfile(
        name="auto",
        description="Fully automatic except apply",
        schema_version="1.0",
        decompose_task=0.0,
        create_tool=0.0,
        select_tool=1.0,
        edit_code=0.0,
        execute_command=0.0,
        create_file=0.0,
        delete_content=0.0,
        access_network=1.0,
        install_dependency=0.0,
        modify_config=0.0,
        approve_plan=0.0,
        safety=SafetyProfile(
            require_sandbox=True,
            require_checkpoints=True,
            require_human_approval=False,
            allow_unsafe_tools=False,
            max_retries_per_step=3,
        ),
    ),
    "ci": AutomationProfile(
        name="ci",
        description="Designed for CI pipelines",
        schema_version="1.0",
        decompose_task=0.0,
        create_tool=0.0,
        select_tool=0.0,
        edit_code=0.0,
        execute_command=0.0,
        create_file=0.0,
        delete_content=0.0,
        access_network=0.0,
        install_dependency=0.0,
        modify_config=0.0,
        approve_plan=0.0,
        safety=SafetyProfile(
            require_sandbox=True,
            require_checkpoints=True,
            require_human_approval=False,
            allow_unsafe_tools=False,
            max_retries_per_step=3,
        ),
    ),
    "full-auto": AutomationProfile(
        name="full-auto",
        description=("No gates, no sandbox, no checkpoints"),
        schema_version="1.0",
        decompose_task=0.0,
        create_tool=0.0,
        select_tool=0.0,
        edit_code=0.0,
        execute_command=0.0,
        create_file=0.0,
        delete_content=0.0,
        access_network=0.0,
        install_dependency=0.0,
        modify_config=0.0,
        approve_plan=0.0,
        safety=SafetyProfile(
            require_sandbox=False,
            require_checkpoints=False,
            require_human_approval=False,
            allow_unsafe_tools=True,
            max_retries_per_step=3,
        ),
    ),
}


def get_builtin_profile(name: str) -> AutomationProfile:
    """Return a built-in profile by name.

    Raises ``KeyError`` if the name is not a recognised built-in.
    """
    if name not in BUILTIN_PROFILES:
        raise KeyError(
            f"Unknown built-in profile '{name}'. "
            f"Available: {', '.join(sorted(BUILTIN_PROFILES))}"
        )
    return BUILTIN_PROFILES[name]
