#!/usr/bin/env python3
# File: src/noveler/domain/interfaces/writing_step_protocol.py
# Purpose: Define abstract specification for writing step execution in Domain layer
# Context: Foundational contracts for stepwise processors; enables compatibility
#          with legacy services while maintaining clean architecture

"""Writing step execution contract and response container.

Purpose:
    Define abstract specification for writing step processors and response containers
    that enable legacy service compatibility within Domain layer architecture.

Context:
    Foundational specifications for all writing step processors. Implements compatibility
    with legacy stepwise execution services while maintaining clean architecture.

Side Effects:
    Defines abstract base classes and data structures; importing this module has no
    side effects. The base classes are designed for subclassing by concrete implementations
    in Application layer that may perform I/O and state modifications.
"""

from __future__ import annotations

import inspect
import time
from dataclasses import dataclass
from typing import Any


@dataclass
class WritingStepResponse:
    """Generic response container for stepwise writing services.

    Purpose:
        Provides a tolerant response wrapper that allows legacy writing services
        to attach arbitrary metadata without raising errors. Ensures downstream
        compatibility when services attach custom attributes beyond the core fields.

    Side Effects:
        Modifies internal state by accepting and storing arbitrary keyword arguments
        in object attributes via __init__. Preserves unknown attributes for downstream
        consumer compatibility (intentional design for backward compatibility).
    """

    success: bool
    step_number: int
    step_name: str
    execution_time_ms: float = 0.0
    error_message: str | None = None
    data: dict[str, Any] | None = None
    metadata: dict[str, Any] | None = None

    def __init__(
        self,
        success: bool,
        step_number: int,
        step_name: str,
        execution_time_ms: float = 0.0,
        error_message: str | None = None,
        data: dict[str, Any] | None = None,
        metadata: dict[str, Any] | None = None,
        **extra: Any,
    ) -> None:
        """Initialize writing step response with core and optional fields.

        Purpose:
            Create a response container that holds step execution results and
            allows legacy services to attach arbitrary metadata without validation.

        Args:
            success: Whether the step execution succeeded
            step_number: Sequential step number in the writing workflow
            step_name: Human-readable name of the writing step
            execution_time_ms: Elapsed time for step execution (milliseconds), defaults to 0.0
            error_message: Error description if step failed, defaults to None
            data: Structured result data from step execution, defaults to None
            metadata: Additional metadata dictionary, defaults to empty dict
            **extra: Arbitrary keyword arguments preserved as object attributes
                     for downstream consumer compatibility

        Side Effects:
            Stores all arguments as instance attributes. Iterates over extra dict
            to attach additional attributes dynamically (intentional for compatibility).
        """
        self.success = success
        self.step_number = step_number
        self.step_name = step_name
        self.execution_time_ms = execution_time_ms
        self.error_message = error_message
        self.data = data
        self.metadata = metadata or {}
        # Preserve unknown attributes for downstream compatibility.
        for key, value in extra.items():
            setattr(self, key, value)


class BaseWritingStep:
    """Compatibility layer for legacy stepwise writing services.

    Purpose:
        Provides an abstract base class that bridges legacy stepwise service
        implementations with modern async execution infrastructure. Legacy services
        override synchronous hooks; this class wraps them to provide async execution.

    Side Effects:
        Stores step_number and step_name as instance attributes during __init__.
        The async execute() method calls external execute_step() hook (delegating
        to subclass implementation) and may perform I/O operations with timing.
    """

    STEP_NUMBER: int = -1
    STEP_NAME: str = "step"

    def __init__(
        self,
        step_number: int | None = None,
        step_name: str | None = None,
        **_: Any,
    ) -> None:
        """Initialize writing step processor with step identification.

        Purpose:
            Store step metadata (number and name) used throughout the execution
            lifecycle. Resolves step identification from parameters or class defaults.

        Args:
            step_number: Step number override; uses STEP_NUMBER class attribute if None
            step_name: Step name override; uses STEP_NAME class attribute if None
            **_: Additional keyword arguments (ignored for compatibility)

        Side Effects:
            Sets self.step_number and self.step_name instance attributes from
            parameters or class-level defaults.
        """
        self.step_number = step_number if step_number is not None else getattr(self, "STEP_NUMBER", -1)
        self.step_name = step_name if step_name is not None else getattr(self, "STEP_NAME", self.__class__.__name__)

    async def execute(
        self,
        episode_number: int,
        previous_results: dict[int, Any] | None = None,
    ) -> WritingStepResponse:
        """Asynchronous wrapper delegating to legacy synchronous implementation.

        Purpose:
            Execute the writing step by calling subclass's synchronous execute_step()
            method (which subclasses override) and wrap the result in WritingStepResponse.
            Handles both sync and async legacy implementations with timing measurement.

        Args:
            episode_number: Episode identifier for this step execution
            previous_results: Results from prior steps (dict mapping step_number to result)

        Returns:
            WritingStepResponse containing success status, execution time, and result data
            (or error details if execution failed). Always returns a response object.

        Raises:
            None - all exceptions caught and returned as error responses

        Side Effects:
            Calls self.execute_step() which may perform I/O and modify state (subclass-dependent).
            Calls self._prepare_context() to build context dict from previous_results.
            Records timing via time.time() calls. Creates WritingStepResponse with
            execution results or error information.
        """
        start_time = time.time()
        context = self._prepare_context(episode_number, previous_results or {})

        try:
            result = self.execute_step(context)
            if inspect.isawaitable(result):
                result = await result

            execution_time = (time.time() - start_time) * 1000

            if isinstance(result, WritingStepResponse):
                # Ensure timing metadata is populated for downstream consumers.
                if not getattr(result, "execution_time_ms", 0.0):
                    result.execution_time_ms = execution_time
                return result

            payload: dict[str, Any]
            if isinstance(result, dict):
                payload = result
            else:
                payload = {"result": result}

            return WritingStepResponse(
                success=True,
                step_number=self.step_number,
                step_name=self.step_name,
                execution_time_ms=execution_time,
                data=payload,
            )

        except Exception as exc:  # pragma: no cover - defensive compatibility path
            execution_time = (time.time() - start_time) * 1000
            return WritingStepResponse(
                success=False,
                step_number=self.step_number,
                step_name=self.step_name,
                execution_time_ms=execution_time,
                error_message=str(exc),
            )

    def get_step_name(self) -> str:
        """Retrieve human-readable step name.

        Purpose:
            Return the step name used for logging and reporting. Resolves from
            instance attribute or STEP_NAME class constant.

        Returns:
            String containing the step name for display/logging purposes

        Side Effects:
            None - pure accessor
        """
        return getattr(self, "STEP_NAME", self.step_name)

    def get_step_description(self) -> str:
        """Retrieve step description if provided by subclass.

        Purpose:
            Return optional step description for documentation/help. Resolves from
            STEP_DESCRIPTION class attribute if defined by subclass.

        Returns:
            Description string, or empty string if not defined by subclass

        Side Effects:
            None - pure accessor
        """
        return getattr(self, "STEP_DESCRIPTION", "")

    def execute_step(self, context: dict[str, Any]) -> Any:  # pragma: no cover - abstract shim
        """Legacy synchronous hook implemented by subclasses.

        Purpose:
            Abstract hook that subclasses override to implement step-specific logic.
            Called by async execute() wrapper; subclass implements actual step work.

        Args:
            context: Execution context dict containing episode_number, previous_results,
                     and project info recovered from prior steps

        Returns:
            Step execution result - can be WritingStepResponse, dict, or other value
            (async execute() wraps return value if needed)

        Raises:
            NotImplementedError: If subclass does not override this method

        Side Effects:
            Subclass-dependent; this base implementation raises error (abstract hook)
        """
        raise NotImplementedError("execute_step must be implemented by subclasses.")

    def _prepare_context(
        self,
        episode_number: int,
        previous_results: dict[int, Any],
    ) -> dict[str, Any]:
        """Assemble context dict expected by legacy services.

        Purpose:
            Construct the execution context dict for the step by extracting
            commonly used data (project info) from prior step results.
            Handles recovery of project information when available.

        Args:
            episode_number: Episode identifier for this execution
            previous_results: Results dict mapping step numbers to their outputs

        Returns:
            Context dict with episode_number, previous_results, and project
            (if recovered from prior results)

        Side Effects:
            Iterates through previous_results to locate project object, examining
            result attributes and data dicts (read-only analysis).
        """
        context: dict[str, Any] = {
            "episode_number": episode_number,
            "previous_results": previous_results,
        }

        # Attempt to recover commonly used data (e.g., project info) from prior steps.
        project = None
        for result in previous_results.values():
            if hasattr(result, "project_root"):
                project = result.project_root
                break
            if hasattr(result, "data") and isinstance(result.data, dict) and "project" in result.data:
                project = result.data["project"]
                break

        if project:
            context["project"] = project

        return context
