"""
Orchestration error handling - step-not-found safeguard and escalation paths.

This module provides explicit error types and reconciliation logic for
orchestration workflows, ensuring no silent success when steps are missing.

Reference: Gate Remediation - step-not-found safeguard
"""

from __future__ import annotations

from dataclasses import dataclass
from enum import Enum
from typing import TYPE_CHECKING, Callable

from returns.result import Failure, Result, Success

if TYPE_CHECKING:
    pass


class StepResolutionStatus(str, Enum):
    """Status of a step resolution attempt.

    >>> StepResolutionStatus.NOT_FOUND.value
    'not_found'
    >>> StepResolutionStatus.RECONCILED.value
    'reconciled'
    """

    NOT_FOUND = "not_found"
    RECONCILED = "reconciled"
    ESCALATED = "escalated"
    FAILED = "failed"


@dataclass(frozen=True)
class StepReference:
    """Reference to a workflow step.

    Attributes:
        step_id: Unique identifier for the step.
        phase: Phase the step belongs to (e.g., 'core', 'shell').
        description: Human-readable description.

    >>> ref = StepReference(step_id="core.validate", phase="core", description="Validate inputs")
    >>> ref.step_id
    'core.validate'
    """

    step_id: str
    phase: str
    description: str


@dataclass(frozen=True)
class StepNotFoundError:
    """Error details for a step that could not be found.

    Attributes:
        step_ref: Reference to the missing step.
        context: Additional context about the resolution attempt.
        suggested_actions: List of possible recovery actions.

    >>> error = StepNotFoundError(
    ...     step_ref=StepReference("core.missing", "core", "Missing step"),
    ...     context="Step lookup in workflow registry",
    ...     suggested_actions=["Check workflow configuration", "Verify phase registration"]
    ... )
    >>> error.step_ref.step_id
    'core.missing'
    """

    step_ref: StepReference
    context: str
    suggested_actions: list[str]


@dataclass(frozen=True)
class ReconciliationResult:
    """Result of a step reconciliation attempt.

    Attributes:
        status: Resolution status after reconciliation.
        resolved_step: The resolved step reference if found.
        escalation_message: Message for escalation if needed.
        actions_taken: List of actions taken during reconciliation.

    >>> result = ReconciliationResult(
    ...     status=StepResolutionStatus.RECONCILED,
    ...     resolved_step=StepReference("core.found", "core", "Found step"),
    ...     escalation_message=None,
    ...     actions_taken=["Checked phase registry", "Found in fallback"]
    ... )
    >>> result.status
    <StepResolutionStatus.RECONCILED: 'reconciled'>
    """

    status: StepResolutionStatus
    resolved_step: StepReference | None
    escalation_message: str | None
    actions_taken: list[str]


# =============================================================================
# Core Functions - Step Resolution
# =============================================================================


# @invar:allow shell_result: Formatting helper returns string for CLI display
# @shell_orchestration: Error formatting co-located with orchestration callers
def format_step_not_found_error(error: StepNotFoundError) -> str:
    """Format a step-not-found error for display.

    Args:
        error: The step-not-found error to format.

    Returns:
        Formatted multi-line error message.

    >>> ref = StepReference("test.step", "test", "Test step")
    >>> error = StepNotFoundError(ref, "Test context", ["Action 1"])
    >>> formatted = format_step_not_found_error(error)
    >>> "Step not found: test.step" in formatted
    True
    """
    lines = [
        f"Step not found: {error.step_ref.step_id}",
        f"  Phase: {error.step_ref.phase}",
        f"  Description: {error.step_ref.description}",
        f"  Context: {error.context}",
    ]
    if error.suggested_actions:
        lines.append("  Suggested actions:")
        for action in error.suggested_actions:
            lines.append(f"    - {action}")
    return "\n".join(lines)


# @invar:allow shell_result: Factory returns dataclass, no Result wrapping needed
# @shell_orchestration: Factory co-located with orchestration callers
def create_step_not_found(
    step_id: str,
    phase: str,
    description: str,
    context: str,
    suggested_actions: list[str] | None = None,
) -> StepNotFoundError:
    """Create a step-not-found error with standard formatting.

    Args:
        step_id: Unique identifier for the missing step.
        phase: Phase the step belongs to.
        description: Human-readable description.
        context: Additional context about the resolution attempt.
        suggested_actions: Optional list of recovery actions.

    Returns:
        A StepNotFoundError instance.

    >>> error = create_step_not_found("core.missing", "core", "Missing", "Test")
    >>> error.step_ref.step_id
    'core.missing'
    >>> error.suggested_actions
    ['Check workflow configuration', 'Verify step registration']
    """
    step_ref = StepReference(
        step_id=step_id,
        phase=phase,
        description=description,
    )
    if suggested_actions is None:
        suggested_actions = [
            "Check workflow configuration",
            "Verify step registration",
        ]
    return StepNotFoundError(
        step_ref=step_ref,
        context=context,
        suggested_actions=suggested_actions,
    )


# =============================================================================
# Shell Functions - Reconciliation with I/O
# =============================================================================


def resolve_step_with_fallback(
    step_id: str,
    phase: str,
    description: str,
    fallback_steps: dict[str, StepReference],
    context: str = "Step resolution",
) -> Result[StepReference, StepNotFoundError]:
    """Resolve a step with fallback to alternative steps.

    This function implements the step-not-found safeguard:
    - First tries direct resolution
    - Then checks fallback registry
    - Returns explicit error if not found

    Args:
        step_id: Step identifier to resolve.
        phase: Phase the step belongs to.
        description: Human-readable description.
        fallback_steps: Registry of fallback steps.
        context: Context for error messages.

    Returns:
        Success with resolved step, or Failure with StepNotFoundError.

    >>> fallbacks = {"fallback.step": StepReference("fallback.step", "fallback", "Fallback")}
    >>> result = resolve_step_with_fallback("fallback.step", "fallback", "Test", fallbacks)
    >>> isinstance(result, Success)
    True
    >>> result.unwrap().step_id
    'fallback.step'
    >>> missing = resolve_step_with_fallback("missing.step", "test", "Test", fallbacks)
    >>> isinstance(missing, Failure)
    True
    """
    # Check fallback registry first (simulates checking workflow registry)
    if step_id in fallback_steps:
        return Success(fallback_steps[step_id])

    # Step not found - return explicit error
    error = create_step_not_found(
        step_id=step_id,
        phase=phase,
        description=description,
        context=context,
        suggested_actions=[
            f"Check if step '{step_id}' is registered in phase '{phase}'",
            "Verify workflow configuration",
            "Check for typos in step identifier",
        ],
    )
    return Failure(error)


# @invar:allow shell_result: Returns ReconciliationResult (dataclass), no Result wrapping needed
# @shell_orchestration: Reconciliation logic co-located with orchestration callers
def reconcile_step(
    error: StepNotFoundError,
    fallback_steps: dict[str, StepReference],
    escalation_handler: Callable[[StepNotFoundError], str] | None = None,
) -> ReconciliationResult:
    """Attempt to reconcile a step-not-found error.

    Reconciliation strategy:
    1. Check phase-level fallbacks
    2. Check global fallbacks
    3. Escalate if still not found

    Args:
        error: The step-not-found error to reconcile.
        fallback_steps: Registry of fallback steps to check.
        escalation_handler: Optional callback for escalation.
            Signature: (StepNotFoundError) -> str

    Returns:
        ReconciliationResult with status and details.

    >>> ref = StepReference("missing.step", "test", "Missing")
    >>> error = StepNotFoundError(ref, "Test", ["Action 1"])
    >>> fallbacks = {"missing.step": StepReference("missing.step", "test", "Found!")}
    >>> result = reconcile_step(error, fallbacks)
    >>> result.status
    <StepResolutionStatus.RECONCILED: 'reconciled'>
    """
    actions_taken: list[str] = []

    # Try phase-level fallback
    phase_fallback_id = f"{error.step_ref.phase}.default"
    if phase_fallback_id in fallback_steps:
        actions_taken.append(f"Found phase-level fallback: {phase_fallback_id}")
        return ReconciliationResult(
            status=StepResolutionStatus.RECONCILED,
            resolved_step=fallback_steps[phase_fallback_id],
            escalation_message=None,
            actions_taken=actions_taken,
        )

    # Try direct step ID fallback
    if error.step_ref.step_id in fallback_steps:
        actions_taken.append(f"Found direct fallback: {error.step_ref.step_id}")
        return ReconciliationResult(
            status=StepResolutionStatus.RECONCILED,
            resolved_step=fallback_steps[error.step_ref.step_id],
            escalation_message=None,
            actions_taken=actions_taken,
        )

    # Escalate
    actions_taken.append("No fallback found, escalating")
    escalation_message = None
    if escalation_handler is not None:
        escalation_message = escalation_handler(error)
    else:
        escalation_message = format_step_not_found_error(error)

    return ReconciliationResult(
        status=StepResolutionStatus.ESCALATED,
        resolved_step=None,
        escalation_message=escalation_message,
        actions_taken=actions_taken,
    )


# @invar:allow shell_result: Formatting helper returns string for CLI/escalation display
# @shell_orchestration: Escalation handler co-located with orchestration callers
def escalate_to_operator(error: StepNotFoundError) -> str:
    """Escalate a step-not-found error to the operator.

    This is the default escalation handler that formats the error
    for operator intervention.

    Args:
        error: The step-not-found error to escalate.

    Returns:
        Formatted escalation message for operator review.

    >>> ref = StepReference("test.step", "test", "Test step")
    >>> error = StepNotFoundError(ref, "Test", ["Action 1", "Action 2"])
    >>> escalation = escalate_to_operator(error)
    >>> "ESCALATION REQUIRED" in escalation
    True
    """
    return (
        f"ESCALATION REQUIRED: Step not found\n"
        f"Step: {error.step_ref.step_id}\n"
        f"Phase: {error.step_ref.phase}\n"
        f"Context: {error.context}\n"
        f"Suggested actions:\n" + "\n".join(f"  - {a}" for a in error.suggested_actions)
    )
