"""Plan Generation Pre-flight Guardrails — 7 checks before execution.

Implements the spec-mandated pre-flight guardrail checks that validate
plan readiness before entering the Strategize phase.  Each check receives
the plan context and returns a ``PreflightCheckResult``.  All checks are
run, and the first failure causes a ``PreflightRejection``.

The 7 checks:

1. **Action schema validation** — verify the action exists and is well-formed.
2. **Actor availability** — confirm all 4 actor roles are registered.
3. **Skill/tool existence** — transitively resolve all tools/skills.
4. **Automation policy** — verify profile permits execution.
5. **Rollback feasibility** — ensure all tools checkpointable if required.
6. **Resource accessibility** — shallow connectivity check on linked resources.
7. **Validation attachment resolution** — pre-resolve all applicable validations.
"""

from __future__ import annotations

import os
from collections.abc import Callable
from enum import Enum
from typing import ClassVar

from cleveragents.actor.schema import RoleHint, actor_role_warnings


class PreflightCheckName(Enum):
    """Names of the 7 pre-flight guardrail checks."""

    ACTION_SCHEMA = "action_schema_validation"
    ACTOR_AVAILABILITY = "actor_availability"
    SKILL_TOOL_EXISTENCE = "skill_tool_existence"
    AUTOMATION_POLICY = "automation_policy"
    ROLLBACK_FEASIBILITY = "rollback_feasibility"
    RESOURCE_ACCESSIBILITY = "resource_accessibility"
    VALIDATION_RESOLUTION = "validation_attachment_resolution"


class PreflightCheckResult:
    """Result of a single pre-flight check."""

    def __init__(
        self,
        check: PreflightCheckName,
        passed: bool,
        message: str = "",
    ) -> None:
        self.check = check
        self.passed = passed
        self.message = message

    def __repr__(self) -> str:
        status = "PASS" if self.passed else "FAIL"
        return f"PreflightCheckResult({self.check.value}: {status} — {self.message})"


class PreflightRejection(Exception):
    """Raised when a pre-flight check fails and the plan is rejected."""

    def __init__(self, check: PreflightCheckName, message: str) -> None:
        self.check = check
        super().__init__(f"Pre-flight check '{check.value}' failed: {message}")


class PreflightReport:
    """Aggregated report of all pre-flight check results."""

    def __init__(self) -> None:
        self.results: list[PreflightCheckResult] = []
        self.warnings: list[PreflightWarning] = []

    def add(self, result: PreflightCheckResult) -> None:
        """Append a check result to the report."""
        self.results.append(result)

    def add_warning(
        self, message: str, *, category: str = "actor_role_compatibility"
    ) -> None:
        """Append a non-fatal warning to the report."""
        self.warnings.append(PreflightWarning(category=category, message=message))

    @property
    def all_passed(self) -> bool:
        """Return ``True`` when every check passed."""
        return all(r.passed for r in self.results)

    @property
    def failures(self) -> list[PreflightCheckResult]:
        """Return the subset of results that did not pass."""
        return [r for r in self.results if not r.passed]

    def format_report(self) -> str:
        """Render a human-readable summary of the report."""
        lines: list[str] = ["Pre-flight Guardrail Report:"]
        for r in self.results:
            status = "PASS" if r.passed else "FAIL"
            lines.append(f"  [{status}] {r.check.value}: {r.message}")
        for warning in self.warnings:
            lines.append(f"  [WARN] {warning.category}: {warning.message}")
        return "\n".join(lines)


class PreflightWarning:
    """Structured warning record for non-fatal preflight findings."""

    def __init__(self, category: str, message: str) -> None:
        self.category = category
        self.message = message


class PlanPreflightGuardrail:
    """Implements the 7 pre-flight guardrail checks per spec Guardrails.

    Each check receives the plan context and returns a
    ``PreflightCheckResult``.  All checks are run, and the first failure
    causes a ``PreflightRejection``.
    """

    ACTOR_ROLES: ClassVar[tuple[str, ...]] = tuple(
        role.value for role in RoleHint if role is not RoleHint.REVIEW
    )

    def run_all_checks(
        self,
        *,
        action_name: str | None = None,
        action_registry: dict[str, object] | None = None,
        actor_registry: dict[str, object] | None = None,
        actor_resolver: Callable[[str], object | None] | None = None,
        tool_names: tuple[str, ...] = (),
        tool_registry: dict[str, object] | None = None,
        skill_names: tuple[str, ...] = (),
        skill_registry: dict[str, object] | None = None,
        automation_profile: object | None = None,
        require_checkpoints: bool = False,
        tool_capabilities: dict[str, bool] | None = None,
        resource_paths: tuple[str, ...] = (),
        validation_names: tuple[str, ...] = (),
        validation_registry: dict[str, object] | None = None,
    ) -> PreflightReport:
        """Run all 7 pre-flight checks and return a report.

        On the first failure a ``PreflightRejection`` is raised so that
        the plan is rejected before entering the Strategize phase.
        """
        report = PreflightReport()

        report.add(self.check_action_schema(action_name, action_registry))
        report.add(self.check_actor_availability(actor_registry))
        for warning in self.check_estimation_actor_compatibility_warnings(
            actor_registry,
            actor_resolver=actor_resolver,
        ):
            report.add_warning(warning)
        report.add(
            self.check_skill_tool_existence(
                tool_names, tool_registry, skill_names, skill_registry
            )
        )
        report.add(self.check_automation_policy(automation_profile))
        report.add(
            self.check_rollback_feasibility(require_checkpoints, tool_capabilities)
        )
        report.add(self.check_resource_accessibility(resource_paths))
        report.add(
            self.check_validation_resolution(validation_names, validation_registry)
        )

        if not report.all_passed:
            first_failure = report.failures[0]
            raise PreflightRejection(
                check=first_failure.check,
                message=first_failure.message,
            )

        return report

    # -- Individual checks -------------------------------------------------

    def check_action_schema(
        self,
        action_name: str | None,
        action_registry: dict[str, object] | None,
    ) -> PreflightCheckResult:
        """Check 1: Verify the action exists and is well-formed."""
        if action_name is None:
            return PreflightCheckResult(
                PreflightCheckName.ACTION_SCHEMA,
                False,
                "No action specified for the plan",
            )
        registry = action_registry or {}
        if action_name not in registry:
            return PreflightCheckResult(
                PreflightCheckName.ACTION_SCHEMA,
                False,
                f"Action '{action_name}' not found in registry",
            )
        return PreflightCheckResult(
            PreflightCheckName.ACTION_SCHEMA,
            True,
            f"Action '{action_name}' found and valid",
        )

    def check_actor_availability(
        self,
        actor_registry: dict[str, object] | None,
    ) -> PreflightCheckResult:
        """Check 2: Verify all 4 actor roles are registered."""
        registry = actor_registry or {}
        missing = [role for role in self.ACTOR_ROLES if role not in registry]
        if missing:
            return PreflightCheckResult(
                PreflightCheckName.ACTOR_AVAILABILITY,
                False,
                f"Missing actor roles: {', '.join(missing)}",
            )
        return PreflightCheckResult(
            PreflightCheckName.ACTOR_AVAILABILITY,
            True,
            "All 4 actor roles registered",
        )

    def check_skill_tool_existence(
        self,
        tool_names: tuple[str, ...],
        tool_registry: dict[str, object] | None,
        skill_names: tuple[str, ...],
        skill_registry: dict[str, object] | None,
    ) -> PreflightCheckResult:
        """Check 3: Verify all referenced tools and skills exist."""
        t_reg = tool_registry or {}
        s_reg = skill_registry or {}

        missing_tools = [t for t in tool_names if t not in t_reg]
        missing_skills = [s for s in skill_names if s not in s_reg]

        issues: list[str] = []
        if missing_tools:
            issues.append(f"Missing tools: {', '.join(missing_tools)}")
        if missing_skills:
            issues.append(f"Missing skills: {', '.join(missing_skills)}")

        if issues:
            return PreflightCheckResult(
                PreflightCheckName.SKILL_TOOL_EXISTENCE,
                False,
                "; ".join(issues),
            )
        return PreflightCheckResult(
            PreflightCheckName.SKILL_TOOL_EXISTENCE,
            True,
            f"All {len(tool_names)} tools and {len(skill_names)} skills verified",
        )

    def check_estimation_actor_compatibility_warnings(
        self,
        actor_registry: dict[str, object] | None,
        *,
        actor_resolver: Callable[[str], object | None] | None = None,
    ) -> list[str]:
        """Emit non-fatal warnings for estimation actor role compatibility."""
        registry = actor_registry or {}
        estimation_actor = registry.get("estimation")
        if isinstance(estimation_actor, str):
            if estimation_actor.startswith("__"):
                return []
            if actor_resolver is not None:
                estimation_actor = actor_resolver(estimation_actor)
            else:
                return []

        if estimation_actor is None:
            return []

        if isinstance(estimation_actor, dict):
            actor_payload: object = dict(estimation_actor)
        else:
            actor_payload = estimation_actor

        if isinstance(actor_payload, dict):
            actor_payload.setdefault("role_hint", "estimation")
            return actor_role_warnings(actor_payload)

        model_dump = getattr(actor_payload, "model_dump", None)
        if callable(model_dump):
            dumped = model_dump(mode="json")
            if isinstance(dumped, dict):
                dumped.setdefault("role_hint", RoleHint.ESTIMATION.value)
                return actor_role_warnings(dumped)

        return []

    def check_automation_policy(
        self,
        automation_profile: object | None,
    ) -> PreflightCheckResult:
        """Check 4: Verify the automation profile permits the requested autonomy."""
        if automation_profile is None:
            return PreflightCheckResult(
                PreflightCheckName.AUTOMATION_POLICY,
                False,
                "No automation profile provided",
            )
        return PreflightCheckResult(
            PreflightCheckName.AUTOMATION_POLICY,
            True,
            "Automation profile permits execution",
        )

    def check_rollback_feasibility(
        self,
        require_checkpoints: bool,
        tool_capabilities: dict[str, bool] | None,
    ) -> PreflightCheckResult:
        """Check 5: If require_checkpoints=true, verify all tools are checkpointable."""
        if not require_checkpoints:
            return PreflightCheckResult(
                PreflightCheckName.ROLLBACK_FEASIBILITY,
                True,
                "Checkpoints not required; rollback check skipped",
            )
        caps = tool_capabilities or {}
        non_checkpointable = [
            tool for tool, checkpointable in caps.items() if not checkpointable
        ]
        if non_checkpointable:
            return PreflightCheckResult(
                PreflightCheckName.ROLLBACK_FEASIBILITY,
                False,
                f"Non-checkpointable tools: {', '.join(non_checkpointable)} "
                f"(require_checkpoints=true)",
            )
        return PreflightCheckResult(
            PreflightCheckName.ROLLBACK_FEASIBILITY,
            True,
            "All tools are checkpointable",
        )

    def check_resource_accessibility(
        self,
        resource_paths: tuple[str, ...],
    ) -> PreflightCheckResult:
        """Check 6: Shallow connectivity check on linked resources."""
        inaccessible: list[str] = []
        for path in resource_paths:
            if not os.path.exists(path):
                inaccessible.append(path)
        if inaccessible:
            return PreflightCheckResult(
                PreflightCheckName.RESOURCE_ACCESSIBILITY,
                False,
                f"Inaccessible resources: {', '.join(inaccessible)}",
            )
        return PreflightCheckResult(
            PreflightCheckName.RESOURCE_ACCESSIBILITY,
            True,
            f"All {len(resource_paths)} resources accessible"
            if resource_paths
            else "No resources to check",
        )

    def check_validation_resolution(
        self,
        validation_names: tuple[str, ...],
        validation_registry: dict[str, object] | None,
    ) -> PreflightCheckResult:
        """Check 7: Pre-resolve all applicable validations."""
        v_reg = validation_registry or {}
        unresolved = [v for v in validation_names if v not in v_reg]
        if unresolved:
            return PreflightCheckResult(
                PreflightCheckName.VALIDATION_RESOLUTION,
                False,
                f"Unresolved validations: {', '.join(unresolved)}",
            )
        return PreflightCheckResult(
            PreflightCheckName.VALIDATION_RESOLUTION,
            True,
            f"All {len(validation_names)} validations resolved"
            if validation_names
            else "No validations to resolve",
        )
