"""
Prompt execution engine.

Implements FR-4: PromptRun Lifecycle
Three-stage execution with idempotent runs.
"""

import time
from typing import Optional, Dict

from markitect.prompts.templates.models import PromptTemplate
from markitect.prompts.templates.analyzer import TemplateAnalyzer
from markitect.prompts.resolver.resolver import PromptResolver
from markitect.prompts.resolver.compiler import ContextCompiler
from markitect.prompts.resolver.strategy import ResolutionConfig
from markitect.prompts.execution.models import (
    PromptRun,
    ExecutionStage,
    RunConfig,
    RunStatus,
    InputBundle,
)
from markitect.prompts.execution.manifest import RunManifest
from markitect.prompts.execution.llm_adapter import LLMAdapter
from markitect.prompts.models import calculate_content_digest, ArtifactType
from markitect.prompts.services.artifact_service import ArtifactService


class PromptExecutionEngine:
    """
    Engine for executing prompt templates.

    Implements FR-4: PromptRun Lifecycle
    - Stage 1: Template Analysis
    - Stage 2: Context Compilation
    - Stage 3: Prompt Processing

    Implements FR-4.4: Idempotent execution via InputBundleHash
    """

    def __init__(
        self,
        artifact_service: ArtifactService,
        template_analyzer: TemplateAnalyzer,
        resolver: PromptResolver,
        compiler: ContextCompiler,
        llm_adapter: LLMAdapter,
    ):
        """
        Initialize execution engine.

        Args:
            artifact_service: For artifact operations
            template_analyzer: For template analysis
            resolver: For macro resolution
            compiler: For context compilation
            llm_adapter: For LLM execution
        """
        self.artifact_service = artifact_service
        self.template_analyzer = template_analyzer
        self.resolver = resolver
        self.compiler = compiler
        self.llm_adapter = llm_adapter
        self.run_cache: Dict[str, PromptRun] = {}  # Cache by input_bundle_hash

    def execute(
        self,
        template: PromptTemplate,
        template_content: str,
        resolution_config: ResolutionConfig,
        run_config: Optional[RunConfig] = None,
    ) -> PromptRun:
        """
        Execute a prompt template.

        Implements FR-4.1: Three-stage execution
        1. Analysis: Analyze template and extract macros
        2. Compilation: Resolve macros and compile prompt
        3. Processing: Execute with LLM

        Implements FR-4.4: Skip if identical InputBundleHash exists

        Args:
            template: Template to execute
            template_content: Template content
            resolution_config: Resolution configuration
            run_config: Execution configuration

        Returns:
            PromptRun with execution results
        """
        config = run_config or RunConfig()

        # Stage 1: Template Analysis
        start_time = time.time()
        if not template.analyzed:
            self.template_analyzer.analyze(template, template_content)
        analysis_time = time.time() - start_time

        # Stage 2: Context Compilation
        start_time = time.time()
        resolution_result = self.resolver.resolve_template(template, resolution_config)

        if not resolution_result.success:
            # Resolution failed - create failed run
            run = self._create_failed_run(
                template,
                "Resolution failed: " + ", ".join(resolution_result.context.errors),
                config,
            )
            return run

        compiled = self.compiler.compile(template, template_content, resolution_result)
        compilation_time = time.time() - start_time

        # Calculate InputBundleHash (FR-4.3)
        input_bundle = InputBundle(
            template_digest=template.content_digest,
            dependency_digests=compiled.dependency_digests,
            resolution_config_hash=calculate_content_digest(
                str(resolution_config.to_dict())
            ),
            model_config={
                "model_name": config.model_name,
                "temperature": config.temperature,
                "max_tokens": config.max_tokens,
            },
        )
        input_bundle_hash = input_bundle.calculate_hash()

        # Check for existing run (FR-4.4)
        if config.skip_if_exists and input_bundle_hash in self.run_cache:
            existing_run = self.run_cache[input_bundle_hash]
            # Create skipped run referencing existing
            skipped_run = PromptRun.create(
                template_id=template.id,
                input_bundle_hash=input_bundle_hash,
                config=config,
            )
            skipped_run.mark_skipped()
            skipped_run.metadata["skipped_due_to"] = existing_run.id
            return skipped_run

        # Create run
        run = PromptRun.create(
            template_id=template.id,
            input_bundle_hash=input_bundle_hash,
            config=config,
        )

        # Create manifest
        manifest = RunManifest.create(
            run_id=run.id,
            template_id=template.id,
            template_name=template.name,
            template_digest=template.content_digest,
        )
        manifest.compiled_prompt_digest = compiled.content_digest
        manifest.model_config = config.to_dict()

        # Add resolved inputs to manifest
        for resolved in resolution_result.context.resolved_macros:
            if resolved.artifact:
                manifest.add_resolved_input(
                    name=resolved.artifact.name,
                    artifact_id=resolved.artifact.id,
                    space_id=resolved.space_id or "",
                    digest=resolved.artifact.content_digest,
                )
                # Add dependency edge
                manifest.add_dependency_edge(
                    source_id=resolved.artifact.id,
                    target_id=run.id,
                    edge_type="requires",
                )

        # Record timing
        manifest.set_timing("analysis", analysis_time)
        manifest.set_timing("compilation", compilation_time)

        # Stage 3: Prompt Processing
        run.advance_stage(ExecutionStage.PROCESSING)

        try:
            start_time = time.time()
            llm_response = self.llm_adapter.execute_prompt(
                compiled.content,
                config,
            )
            processing_time = time.time() - start_time
            manifest.set_timing("processing", processing_time)

            # Store output as generated artifact
            output_artifact = self.artifact_service.create_artifact(
                space_id=template.space_id,
                name=f"{template.name}-output-{run.id[:8]}",
                content=llm_response.content,
                artifact_type=ArtifactType.GENERATED,
            )

            manifest.add_output_artifact(
                artifact_id=output_artifact.id,
                name=output_artifact.name,
                digest=output_artifact.content_digest,
                artifact_type=output_artifact.artifact_type.value,
            )

            # Add generation edge
            manifest.add_dependency_edge(
                source_id=run.id,
                target_id=output_artifact.id,
                edge_type="generates",
            )

            # Mark complete
            run.mark_complete()
            run.metadata["manifest"] = manifest.to_dict()
            run.metadata["output_artifact_id"] = output_artifact.id

            # Cache run
            self.run_cache[input_bundle_hash] = run

        except Exception as e:
            run.mark_failed(str(e))
            run.metadata["manifest"] = manifest.to_dict()

        return run

    def _create_failed_run(
        self,
        template: PromptTemplate,
        error: str,
        config: RunConfig,
    ) -> PromptRun:
        """
        Create a failed run.

        Args:
            template: Template
            error: Error message
            config: Config

        Returns:
            Failed PromptRun
        """
        run = PromptRun.create(
            template_id=template.id,
            input_bundle_hash="failed",
            config=config,
        )
        run.mark_failed(error)
        return run

    def get_run_by_hash(self, input_bundle_hash: str) -> Optional[PromptRun]:
        """
        Retrieve cached run by input bundle hash.

        Args:
            input_bundle_hash: Hash to lookup

        Returns:
            PromptRun if found, None otherwise
        """
        return self.run_cache.get(input_bundle_hash)

    def clear_cache(self) -> None:
        """Clear the run cache."""
        self.run_cache.clear()
