"""
Step reconciliation module for orchestration workflows.

Provides utilities for reconciling missing steps with fallback strategies
and escalating unrecoverable failures to operators.

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

from __future__ import annotations

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

from returns.result import Failure, Result, Success

if TYPE_CHECKING:
    pass


class ReconciliationStrategy(str, Enum):
    """Strategy for reconciling missing steps.

    >>> ReconciliationStrategy.FALLBACK.value
    'fallback'
    """

    FALLBACK = "fallback"
    PHASE_DEFAULT = "phase_default"
    OPERATOR_ESCALATION = "operator_escalation"
    FAIL_FAST = "fail_fast"


@dataclass(frozen=True)
class ReconciliationContext:
    """Context for a step reconciliation operation.

    Attributes:
        workflow_id: Identifier for the workflow being executed.
        phase: Current phase name.
        step_id: The step that failed to resolve.
        fallback_registry: Available fallback steps.
        strategy: Chosen reconciliation strategy.

    >>> ctx = ReconciliationContext(
    ...     workflow_id="test-workflow",
    ...     phase="core",
    ...     step_id="core.missing",
    ...     fallback_registry={},
    ...     strategy=ReconciliationStrategy.FAIL_FAST
    ... )
    >>> ctx.step_id
    'core.missing'
    """

    workflow_id: str
    phase: str
    step_id: str
    fallback_registry: dict[str, str]
    strategy: ReconciliationStrategy


@dataclass(frozen=True)
class ReconciliationOutcome:
    """Outcome of a reconciliation operation.

    Attributes:
        success: Whether reconciliation succeeded.
        resolved_step_id: The resolved step ID if successful.
        message: Human-readable outcome message.
        actions: Actions taken during reconciliation.

    >>> outcome = ReconciliationOutcome(
    ...     success=True,
    ...     resolved_step_id="core.fallback",
    ...     message="Resolved via fallback",
    ...     actions=["Checked fallback registry"]
    ... )
    >>> outcome.success
    True
    """

    success: bool
    resolved_step_id: str | None
    message: str
    actions: list[str]


# =============================================================================
# Core Functions - Pure reconciliation logic
# =============================================================================


# @invar:allow shell_result: Internal registry builder returns plain dict for orchestration wiring
# @shell_orchestration: Pure mapping helper intentionally co-located with reconciliation flow
def create_fallback_registry(
    phase_defaults: dict[str, str],
    step_overrides: dict[str, str] | None = None,
) -> dict[str, str]:
    """Create a fallback registry from phase defaults and step overrides.

    The fallback registry maps step IDs to their fallback step IDs.
    Phase defaults provide a fallback for all steps in a phase.
    Step overrides provide specific fallbacks for individual steps.

    Args:
        phase_defaults: Mapping from phase name to default fallback step.
        step_overrides: Optional mapping from step ID to fallback step.

    Returns:
        Combined fallback registry.

    >>> registry = create_fallback_registry({"core": "core.default"}, {"core.test": "core.backup"})
    >>> registry["core.default"]
    'core.default'
    >>> registry["core.test"]
    'core.backup'
    """
    registry: dict[str, str] = {}

    # Add phase defaults
    for phase, default_step in phase_defaults.items():
        registry[f"{phase}.default"] = default_step

    # Add step overrides
    if step_overrides:
        registry.update(step_overrides)

    return registry


# @invar:allow shell_result: Strategy selector returns enum for reconciliation control flow
# @shell_orchestration: Pure decision helper intentionally co-located with reconciliation flow
def select_reconciliation_strategy(
    step_id: str,
    phase: str,
    fallback_registry: dict[str, str],
) -> ReconciliationStrategy:
    """Select the appropriate reconciliation strategy for a missing step.

    Strategy selection order:
    1. FALLBACK if step has direct fallback
    2. PHASE_DEFAULT if phase has default
    3. OPERATOR_ESCALATION for recoverable situations
    4. FAIL_FAST for critical steps

    Args:
        step_id: The missing step ID.
        phase: The phase the step belongs to.
        fallback_registry: Available fallback steps.

    Returns:
        The selected strategy.

    >>> registry = {"core.test": "core.backup", "core.default": "core.default"}
    >>> select_reconciliation_strategy("core.test", "core", registry)
    <ReconciliationStrategy.FALLBACK: 'fallback'>
    >>> select_reconciliation_strategy("core.missing", "core", registry)
    <ReconciliationStrategy.PHASE_DEFAULT: 'phase_default'>
    >>> select_reconciliation_strategy("shell.missing", "shell", {})
    <ReconciliationStrategy.OPERATOR_ESCALATION: 'operator_escalation'>
    """
    # Check for direct fallback
    if step_id in fallback_registry:
        return ReconciliationStrategy.FALLBACK

    # Check for phase default
    if f"{phase}.default" in fallback_registry:
        return ReconciliationStrategy.PHASE_DEFAULT

    # Default to operator escalation (human intervention)
    return ReconciliationStrategy.OPERATOR_ESCALATION


# =============================================================================
# Shell Functions - Reconciliation with explicit outcomes
# =============================================================================


def reconcile_missing_step(
    step_id: str,
    phase: str,
    workflow_id: str,
    fallback_registry: dict[str, str],
    critical_steps: set[str] | None = None,
) -> Result[ReconciliationOutcome, str]:
    """Attempt to reconcile a missing step with explicit outcome.

    This is the main entry point for step reconciliation. It:
    1. Selects an appropriate strategy
    2. Attempts resolution
    3. Returns either a success with outcome or a failure with error message

    IMPORTANT: This function never silently succeeds. If no resolution
    is possible, it returns a Failure with a descriptive error.

    Args:
        step_id: The missing step ID.
        phase: The phase the step belongs to.
        workflow_id: The workflow being executed.
        fallback_registry: Available fallback steps.
        critical_steps: Optional set of steps that cannot have fallbacks.

    Returns:
        Success with ReconciliationOutcome if resolved,
        Failure with error message if not.

    >>> registry = {"core.test": "core.backup", "core.default": "core.default"}
    >>> result = reconcile_missing_step("core.test", "core", "wf-1", registry)
    >>> isinstance(result, Success)
    True
    >>> result.unwrap().resolved_step_id
    'core.backup'
    >>> missing = reconcile_missing_step("critical.step", "core", "wf-1", registry, {"critical.step"})
    >>> isinstance(missing, Failure)
    True
    """
    actions: list[str] = []
    critical_steps = critical_steps or set()

    # Check if this is a critical step (no fallbacks allowed)
    if step_id in critical_steps:
        actions.append("Step is critical, no fallbacks allowed")
        return Failure(
            f"Critical step '{step_id}' not found. "
            f"Critical steps cannot be substituted. "
            f"Workflow: {workflow_id}"
        )

    # Select strategy
    strategy = select_reconciliation_strategy(step_id, phase, fallback_registry)
    actions.append(f"Selected strategy: {strategy.value}")

    # Apply strategy
    if strategy == ReconciliationStrategy.FALLBACK:
        resolved_step_id = fallback_registry[step_id]
        actions.append(f"Applied direct fallback: {step_id} -> {resolved_step_id}")
        return Success(
            ReconciliationOutcome(
                success=True,
                resolved_step_id=resolved_step_id,
                message=f"Resolved missing step {step_id} via fallback to {resolved_step_id}",
                actions=actions,
            )
        )

    if strategy == ReconciliationStrategy.PHASE_DEFAULT:
        resolved_step_id = fallback_registry[f"{phase}.default"]
        actions.append(f"Applied phase default: {phase} -> {resolved_step_id}")
        return Success(
            ReconciliationOutcome(
                success=True,
                resolved_step_id=resolved_step_id,
                message=f"Resolved missing step {step_id} via phase default {resolved_step_id}",
                actions=actions,
            )
        )

    # Operator escalation
    actions.append("No automatic resolution available")
    actions.append("Escalating to operator")
    return Failure(
        f"Step '{step_id}' not found in workflow '{workflow_id}' (phase: {phase}). "
        f"No fallback available. "
        f"Actions attempted: {'; '.join(actions)}"
    )


# @invar:allow shell_result: Report formatter returns display string for escalation output
# @shell_orchestration: Pure formatter intentionally co-located with reconciliation flow
def format_reconciliation_report(outcome: ReconciliationOutcome) -> str:
    """Format a reconciliation outcome as a human-readable report.

    Args:
        outcome: The reconciliation outcome to format.

    Returns:
        Multi-line formatted report.

    >>> outcome = ReconciliationOutcome(
    ...     success=True,
    ...     resolved_step_id="core.backup",
    ...     message="Resolved via fallback",
    ...     actions=["Step resolved"]
    ... )
    >>> report = format_reconciliation_report(outcome)
    >>> "RECONCILIATION REPORT" in report
    True
    """
    status = "SUCCESS" if outcome.success else "FAILURE"
    lines = [
        "=" * 60,
        "RECONCILIATION REPORT",
        "=" * 60,
        f"Status: {status}",
        f"Resolved Step: {outcome.resolved_step_id or 'N/A'}",
        f"Message: {outcome.message}",
        "",
        "Actions Taken:",
    ]
    for action in outcome.actions:
        lines.append(f"  - {action}")
    lines.append("=" * 60)
    return "\n".join(lines)
