"""
Pipeline Runner - Executes pipelines and tracks execution history.

The runner handles pipeline execution, tracking execution status in the database,
and providing execution history for monitoring and debugging.
"""

from __future__ import annotations

import logging
import time
import uuid
from datetime import datetime
from typing import TYPE_CHECKING, Any

from pipeline_core.base import PipelineResult
from pipeline_core.contracts import ExecutionRequest, ExecutionStatus

if TYPE_CHECKING:
    import asyncpg

    from pipeline_core.base import BasePipeline
    from pipeline_core.registry import PipelineRegistry

logger = logging.getLogger(__name__)


class PipelineRunner:
    """
    Executes pipelines and tracks execution history.

    The runner:
    - Gets pipeline instances from the registry
    - Tracks execution status in the database
    - Handles errors and updates status
    - Provides execution history queries

    Usage:
        registry = PipelineRegistry(pool)
        runner = PipelineRunner(pool, registry)

        # Execute a pipeline
        result = await runner.execute(
            pipeline_id="reviewiq",
            request=ExecutionRequest(
                job_id="job-123",
                stages=["normalize", "classify"],
            )
        )

        # Get execution status
        status = await runner.get_execution("exec-123")

        # List executions
        executions = await runner.list_executions(pipeline_id="reviewiq")
    """

    def __init__(
        self,
        pool: asyncpg.Pool,
        registry: PipelineRegistry,
    ):
        """
        Initialize the runner.

        Args:
            pool: asyncpg connection pool
            registry: Pipeline registry for getting pipeline instances
        """
        self._pool = pool
        self._registry = registry

    async def execute(
        self,
        pipeline_id: str,
        request: ExecutionRequest,
    ) -> tuple[str, PipelineResult]:
        """
        Execute a pipeline.

        Args:
            pipeline_id: Pipeline identifier
            request: Execution request with input data and options

        Returns:
            Tuple of (execution_id, PipelineResult)

        Raises:
            ValueError: If pipeline not found or disabled
        """
        # Get pipeline instance
        pipeline = await self._registry.get_pipeline(pipeline_id)
        if not pipeline:
            raise ValueError(f"Pipeline not found or disabled: {pipeline_id}")

        # Create execution record
        execution_id = str(uuid.uuid4())
        stages = request.get("stages") or pipeline.get_stage_names()

        await self._create_execution(
            execution_id=execution_id,
            pipeline_id=pipeline_id,
            job_id=request.get("job_id"),
            business_id=request.get("business_id"),
            stages_requested=stages,
        )

        # Update status to running
        await self._update_execution_status(
            execution_id=execution_id,
            status="running",
            started_at=datetime.utcnow(),
        )

        try:
            # Prepare input data
            input_data = request.get("input_data") or {}
            if request.get("job_id"):
                input_data["job_id"] = request["job_id"]
            if request.get("business_id"):
                input_data["business_id"] = request["business_id"]

            # Validate input
            validation_errors = await pipeline.validate_input(input_data)
            if validation_errors:
                error_msg = "; ".join(validation_errors)
                await self._update_execution_status(
                    execution_id=execution_id,
                    status="failed",
                    error_message=f"Validation failed: {error_msg}",
                    completed_at=datetime.utcnow(),
                )
                return execution_id, PipelineResult.from_error(
                    pipeline_id, f"Validation failed: {error_msg}"
                )

            # Execute pipeline
            start_time = time.time()
            result = await pipeline.process(input_data, stages=stages)
            duration_ms = int((time.time() - start_time) * 1000)

            # Update execution with result
            if result.success:
                await self._update_execution_status(
                    execution_id=execution_id,
                    status="completed",
                    stages_completed=result.stages_run,
                    result_summary=self._summarize_result(result),
                    completed_at=datetime.utcnow(),
                )
            else:
                await self._update_execution_status(
                    execution_id=execution_id,
                    status="failed",
                    stages_completed=result.stages_run,
                    error_message=result.error,
                    completed_at=datetime.utcnow(),
                )

            logger.info(
                f"Pipeline {pipeline_id} execution {execution_id} "
                f"completed in {duration_ms}ms: success={result.success}"
            )

            return execution_id, result

        except Exception as e:
            logger.exception(f"Pipeline {pipeline_id} execution failed: {e}")

            await self._update_execution_status(
                execution_id=execution_id,
                status="failed",
                error_message=str(e),
                completed_at=datetime.utcnow(),
            )

            return execution_id, PipelineResult.from_error(pipeline_id, str(e))

    async def cancel(self, execution_id: str) -> bool:
        """
        Cancel a running execution.

        Args:
            execution_id: Execution identifier

        Returns:
            True if cancelled, False if not found or already completed
        """
        async with self._pool.acquire() as conn:
            result = await conn.execute(
                """
                UPDATE pipeline.executions
                SET status = 'cancelled', completed_at = NOW()
                WHERE id = $1 AND status IN ('pending', 'running')
                """,
                uuid.UUID(execution_id),
            )

        cancelled = result.split()[-1] != "0"
        if cancelled:
            logger.info(f"Cancelled execution: {execution_id}")
        return cancelled

    async def get_execution(self, execution_id: str) -> ExecutionStatus | None:
        """
        Get execution status.

        Args:
            execution_id: Execution identifier

        Returns:
            ExecutionStatus or None if not found
        """
        async with self._pool.acquire() as conn:
            row = await conn.fetchrow(
                """
                SELECT id, pipeline_id, job_id, business_id, status,
                       stages_requested, stages_completed, current_stage,
                       input_summary, result_summary, error_message,
                       started_at, completed_at, created_at
                FROM pipeline.executions
                WHERE id = $1
                """,
                uuid.UUID(execution_id),
            )

        if not row:
            return None

        return self._row_to_execution_status(row)

    async def list_executions(
        self,
        pipeline_id: str | None = None,
        job_id: str | None = None,
        business_id: str | None = None,
        status: str | None = None,
        limit: int = 50,
        offset: int = 0,
    ) -> list[ExecutionStatus]:
        """
        List execution history.

        Args:
            pipeline_id: Filter by pipeline
            job_id: Filter by job
            business_id: Filter by business
            status: Filter by status
            limit: Maximum results
            offset: Result offset

        Returns:
            List of ExecutionStatus
        """
        conditions = []
        params = []
        param_idx = 1

        if pipeline_id:
            conditions.append(f"pipeline_id = ${param_idx}")
            params.append(pipeline_id)
            param_idx += 1

        if job_id:
            conditions.append(f"job_id = ${param_idx}")
            params.append(uuid.UUID(job_id))
            param_idx += 1

        if business_id:
            conditions.append(f"business_id = ${param_idx}")
            params.append(business_id)
            param_idx += 1

        if status:
            conditions.append(f"status = ${param_idx}")
            params.append(status)
            param_idx += 1

        where_clause = "WHERE " + " AND ".join(conditions) if conditions else ""

        query = f"""
            SELECT id, pipeline_id, job_id, business_id, status,
                   stages_requested, stages_completed, current_stage,
                   input_summary, result_summary, error_message,
                   started_at, completed_at, created_at
            FROM pipeline.executions
            {where_clause}
            ORDER BY created_at DESC
            LIMIT ${param_idx} OFFSET ${param_idx + 1}
        """
        params.extend([limit, offset])

        async with self._pool.acquire() as conn:
            rows = await conn.fetch(query, *params)

        return [self._row_to_execution_status(row) for row in rows]

    async def get_execution_count(
        self,
        pipeline_id: str | None = None,
        status: str | None = None,
    ) -> int:
        """
        Get execution count.

        Args:
            pipeline_id: Filter by pipeline
            status: Filter by status

        Returns:
            Count of executions matching filters
        """
        conditions = []
        params = []
        param_idx = 1

        if pipeline_id:
            conditions.append(f"pipeline_id = ${param_idx}")
            params.append(pipeline_id)
            param_idx += 1

        if status:
            conditions.append(f"status = ${param_idx}")
            params.append(status)
            param_idx += 1

        where_clause = "WHERE " + " AND ".join(conditions) if conditions else ""

        async with self._pool.acquire() as conn:
            result = await conn.fetchval(
                f"SELECT COUNT(*) FROM pipeline.executions {where_clause}",
                *params,
            )

        return result or 0

    async def _create_execution(
        self,
        execution_id: str,
        pipeline_id: str,
        job_id: str | None,
        business_id: str | None,
        stages_requested: list[str],
    ) -> None:
        """Create an execution record."""
        async with self._pool.acquire() as conn:
            await conn.execute(
                """
                INSERT INTO pipeline.executions (
                    id, pipeline_id, job_id, business_id,
                    status, stages_requested, created_at
                )
                VALUES ($1, $2, $3, $4, 'pending', $5, NOW())
                """,
                uuid.UUID(execution_id),
                pipeline_id,
                uuid.UUID(job_id) if job_id else None,
                business_id,
                stages_requested,
            )

    async def _update_execution_status(
        self,
        execution_id: str,
        status: str,
        current_stage: str | None = None,
        stages_completed: list[str] | None = None,
        input_summary: dict[str, Any] | None = None,
        result_summary: dict[str, Any] | None = None,
        error_message: str | None = None,
        started_at: datetime | None = None,
        completed_at: datetime | None = None,
    ) -> None:
        """Update execution status."""
        updates = ["status = $2"]
        params: list[Any] = [uuid.UUID(execution_id), status]
        param_idx = 3

        if current_stage is not None:
            updates.append(f"current_stage = ${param_idx}")
            params.append(current_stage)
            param_idx += 1

        if stages_completed is not None:
            updates.append(f"stages_completed = ${param_idx}")
            params.append(stages_completed)
            param_idx += 1

        if input_summary is not None:
            updates.append(f"input_summary = ${param_idx}")
            params.append(input_summary)
            param_idx += 1

        if result_summary is not None:
            updates.append(f"result_summary = ${param_idx}")
            params.append(result_summary)
            param_idx += 1

        if error_message is not None:
            updates.append(f"error_message = ${param_idx}")
            params.append(error_message)
            param_idx += 1

        if started_at is not None:
            updates.append(f"started_at = ${param_idx}")
            params.append(started_at)
            param_idx += 1

        if completed_at is not None:
            updates.append(f"completed_at = ${param_idx}")
            params.append(completed_at)
            param_idx += 1

        query = f"""
            UPDATE pipeline.executions
            SET {", ".join(updates)}
            WHERE id = $1
        """

        async with self._pool.acquire() as conn:
            await conn.execute(query, *params)

    def _row_to_execution_status(self, row: Any) -> ExecutionStatus:
        """Convert database row to ExecutionStatus."""
        # Calculate progress
        stages_requested = row["stages_requested"] or []
        stages_completed = row["stages_completed"] or []
        progress = (
            len(stages_completed) / len(stages_requested)
            if stages_requested
            else 0.0
        )

        return ExecutionStatus(
            id=str(row["id"]),
            pipeline_id=row["pipeline_id"],
            job_id=str(row["job_id"]) if row["job_id"] else None,
            business_id=row["business_id"],
            status=row["status"],
            stages_requested=stages_requested,
            stages_completed=stages_completed,
            current_stage=row["current_stage"],
            progress=progress,
            input_summary=row["input_summary"],
            result_summary=row["result_summary"],
            error_message=row["error_message"],
            started_at=row["started_at"].isoformat() if row["started_at"] else None,
            completed_at=row["completed_at"].isoformat() if row["completed_at"] else None,
            created_at=row["created_at"].isoformat() if row["created_at"] else None,
        )

    def _summarize_result(self, result: PipelineResult) -> dict[str, Any]:
        """Create a summary of the pipeline result for storage."""
        summary: dict[str, Any] = {
            "success": result.success,
            "stages_run": result.stages_run,
        }

        # Add stage-specific summaries
        for stage, stage_result in result.stage_results.items():
            if stage_result.get("data"):
                # Extract stats if available
                data = stage_result["data"]
                if "stats" in data:
                    summary[f"{stage}_stats"] = data["stats"]

        return summary
