"""
Incremental execution engine.

Implements FR-7: Incremental Recomputation
Implements FR-8: Impact Analysis

Orchestrates: find dependents → check cycles → assess impact →
execute or suppress → record debt.
"""

import sqlite3
from collections import deque
from pathlib import Path
from typing import Callable, List, Optional, Set

from markitect.prompts.dependencies.queries import DependencyQueryService
from markitect.prompts.execution.models import PromptRun
from markitect.prompts.incremental.impact import ImpactAnalyzer
from markitect.prompts.incremental.models import (
    ArtifactChange,
    ImpactDebt,
    RecomputeConfig,
    RecomputeResult,
)


# SQL schema for impact_debt table
DEBT_TABLE_SQL = """
CREATE TABLE IF NOT EXISTS impact_debt (
    id TEXT PRIMARY KEY,
    artifact_id TEXT NOT NULL,
    dependent_run_id TEXT NOT NULL,
    change_magnitude REAL NOT NULL,
    suppression_reason TEXT NOT NULL,
    recorded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_debt_artifact ON impact_debt(artifact_id);
CREATE INDEX IF NOT EXISTS idx_debt_run ON impact_debt(dependent_run_id);
"""


class IncrementalExecutionEngine:
    """
    Engine for incremental recomputation of dependent artifacts.

    Orchestrates the full recompute flow:
    1. Find dependents at configured depth (BFS)
    2. Check for circular dependencies
    3. Assess impact magnitude
    4. Execute or suppress recomputation
    5. Record impact debt for suppressed items
    """

    def __init__(
        self,
        db_path: str,
        query_service: DependencyQueryService,
        impact_analyzer: Optional[ImpactAnalyzer] = None,
    ):
        """
        Initialize engine with dependencies.

        Args:
            db_path: Path to SQLite database for debt persistence
            query_service: Service for dependency graph queries
            impact_analyzer: Impact analyzer (created if not provided)
        """
        self.db_path = db_path
        self.query_service = query_service
        self.impact_analyzer = impact_analyzer or ImpactAnalyzer()
        self._initialize_tables()

    def _initialize_tables(self) -> None:
        """Initialize impact_debt table if not exists."""
        db_dir = Path(self.db_path).parent
        if db_dir and not db_dir.exists():
            db_dir.mkdir(parents=True, exist_ok=True)

        conn = sqlite3.connect(self.db_path)
        try:
            conn.executescript(DEBT_TABLE_SQL)
            conn.commit()
        finally:
            conn.close()

    def _get_connection(self) -> sqlite3.Connection:
        """Get a database connection."""
        conn = sqlite3.connect(self.db_path)
        conn.row_factory = sqlite3.Row
        return conn

    def find_dependents_at_depth(
        self,
        artifact_id: str,
        max_depth: int = 1,
    ) -> Set[str]:
        """
        Find dependents up to a configured BFS depth.

        Implements FR-7.2: Configurable depth control.

        Args:
            artifact_id: Artifact to find dependents of
            max_depth: Maximum BFS layers to traverse

        Returns:
            Set of dependent artifact IDs within depth limit
        """
        visited: Set[str] = set()
        queue: deque[tuple] = deque([(artifact_id, 0)])

        while queue:
            current, depth = queue.popleft()
            if depth >= max_depth:
                continue

            direct_dependents = self.query_service.find_dependents(current)
            for dep in direct_dependents:
                if dep not in visited:
                    visited.add(dep)
                    queue.append((dep, depth + 1))

        return visited

    def recompute(
        self,
        change: ArtifactChange,
        config: Optional[RecomputeConfig] = None,
        execution_callback: Optional[Callable[[str], Optional[PromptRun]]] = None,
        old_content: Optional[str] = None,
        new_content: Optional[str] = None,
    ) -> RecomputeResult:
        """
        Perform incremental recomputation for a detected change.

        Orchestrates the full recompute flow:
        1. Find dependents at configured depth
        2. For each dependent, check cycles, assess impact
        3. Execute or suppress and record debt

        Args:
            change: Detected artifact change
            config: Recompute configuration
            execution_callback: Callable that takes a run_id and re-executes.
                If None, records what would be recomputed without executing.
            old_content: Old content for magnitude calculation
            new_content: New content for magnitude calculation

        Returns:
            RecomputeResult summarizing the recomputation
        """
        config = config or RecomputeConfig()

        # Calculate change magnitude
        magnitude = self.impact_analyzer.calculate_magnitude(
            old_content, new_content
        )

        # Find dependents within configured depth
        dependents = self.find_dependents_at_depth(
            change.artifact_id, config.max_depth
        )

        result = RecomputeResult(
            changed_artifact_id=change.artifact_id,
            change=change,
            total_dependents=len(dependents),
        )

        recompute_count = 0

        for dependent_id in dependents:
            # Budget check
            if recompute_count >= config.max_recomputes:
                debt = ImpactDebt.create(
                    artifact_id=change.artifact_id,
                    dependent_run_id=dependent_id,
                    change_magnitude=magnitude,
                    suppression_reason="budget_exhausted",
                )
                self._record_debt(debt)
                result.suppressed.append(debt)
                result.suppressed_count += 1
                continue

            # Circular dependency check
            if config.suppress_circular:
                if self.query_service.would_create_cycle(
                    dependent_id, change.artifact_id
                ):
                    debt = ImpactDebt.create(
                        artifact_id=change.artifact_id,
                        dependent_run_id=dependent_id,
                        change_magnitude=magnitude,
                        suppression_reason="circular_dependency",
                    )
                    self._record_debt(debt)
                    result.suppressed.append(debt)
                    result.suppressed_count += 1
                    continue

            # Impact threshold check
            if not self.impact_analyzer.should_recompute(magnitude, config):
                debt = ImpactDebt.create(
                    artifact_id=change.artifact_id,
                    dependent_run_id=dependent_id,
                    change_magnitude=magnitude,
                    suppression_reason="below_threshold",
                )
                self._record_debt(debt)
                result.suppressed.append(debt)
                result.suppressed_count += 1
                continue

            # Execute recomputation
            if execution_callback is not None:
                run = execution_callback(dependent_id)
                if run is not None:
                    result.executed_run_ids.append(run.id)
            else:
                # Dry-run mode: just record the ID
                result.executed_run_ids.append(dependent_id)

            result.recomputed_count += 1
            recompute_count += 1

        return result

    def _record_debt(self, debt: ImpactDebt) -> ImpactDebt:
        """
        Persist an ImpactDebt record to the database.

        Args:
            debt: Debt record to persist

        Returns:
            Persisted debt record
        """
        conn = self._get_connection()
        try:
            conn.execute(
                """
                INSERT INTO impact_debt (
                    id, artifact_id, dependent_run_id,
                    change_magnitude, suppression_reason, recorded_at
                ) VALUES (?, ?, ?, ?, ?, ?)
                """,
                (
                    debt.id,
                    debt.artifact_id,
                    debt.dependent_run_id,
                    debt.change_magnitude,
                    debt.suppression_reason,
                    debt.recorded_at.isoformat(),
                ),
            )
            conn.commit()
            return debt
        finally:
            conn.close()

    def get_debt_for_artifact(self, artifact_id: str) -> List[ImpactDebt]:
        """
        Get all impact debt records for a given artifact.

        Args:
            artifact_id: Artifact identifier

        Returns:
            List of ImpactDebt records
        """
        conn = self._get_connection()
        try:
            cursor = conn.execute(
                "SELECT * FROM impact_debt WHERE artifact_id = ?",
                (artifact_id,),
            )
            return [self._row_to_debt(row) for row in cursor.fetchall()]
        finally:
            conn.close()

    def get_debt_for_run(self, run_id: str) -> List[ImpactDebt]:
        """
        Get all impact debt records for a given run.

        Args:
            run_id: Run identifier

        Returns:
            List of ImpactDebt records
        """
        conn = self._get_connection()
        try:
            cursor = conn.execute(
                "SELECT * FROM impact_debt WHERE dependent_run_id = ?",
                (run_id,),
            )
            return [self._row_to_debt(row) for row in cursor.fetchall()]
        finally:
            conn.close()

    def get_all_debt(self) -> List[ImpactDebt]:
        """
        Get all impact debt records.

        Returns:
            List of all ImpactDebt records
        """
        conn = self._get_connection()
        try:
            cursor = conn.execute("SELECT * FROM impact_debt")
            return [self._row_to_debt(row) for row in cursor.fetchall()]
        finally:
            conn.close()

    def _row_to_debt(self, row: sqlite3.Row) -> ImpactDebt:
        """Convert database row to ImpactDebt instance."""
        from datetime import datetime

        return ImpactDebt(
            id=row["id"],
            artifact_id=row["artifact_id"],
            dependent_run_id=row["dependent_run_id"],
            change_magnitude=row["change_magnitude"],
            suppression_reason=row["suppression_reason"],
            recorded_at=datetime.fromisoformat(row["recorded_at"]),
        )
