"""
koci Live Pipeline Display

Real-time pipeline progress with spinners and checkmarks.
Shows full pipeline tree upfront and updates as execution progresses.
"""

import time
import threading
from enum import Enum
from typing import Optional, Dict
from dataclasses import dataclass, field
from contextlib import contextmanager

try:
    from rich.console import Console, Group
    from rich.live import Live
    from rich.tree import Tree
    from rich.text import Text
    from rich.table import Table
    RICH_AVAILABLE = True
except ImportError:
    RICH_AVAILABLE = False


class ItemStatus(Enum):
    """Status of a pipeline item."""
    PENDING = "pending"
    RUNNING = "running"
    SUCCESS = "success"
    FAILED = "failed"
    SKIPPED = "skipped"
    CANCELLED = "cancelled"


@dataclass
class StepState:
    """State of a step."""
    name: str
    job_name: str = ""
    status: ItemStatus = ItemStatus.PENDING
    duration: float = 0.0
    output: str = ""  # Capture output for failed steps


@dataclass
class ReportSummary:
    """Summary of test reports for a job."""
    tests_passed: int = 0
    tests_failed: int = 0
    tests_skipped: int = 0
    coverage_percent: float = 0.0
    coverage_threshold: float = 0.0
    has_junit: bool = False
    has_coverage: bool = False


@dataclass
class JobState:
    """State of a job."""
    name: str
    image: str
    status: ItemStatus = ItemStatus.PENDING
    duration: float = 0.0
    steps: list[StepState] = field(default_factory=list)
    report: ReportSummary = field(default_factory=ReportSummary)


@dataclass
class StageState:
    """State of a stage."""
    name: str
    status: ItemStatus = ItemStatus.PENDING
    duration: float = 0.0
    jobs: list[JobState] = field(default_factory=list)


def format_duration(seconds: float) -> str:
    """Format duration in human-readable form."""
    if seconds < 0.01:
        return ""
    elif seconds < 1:
        return f"{seconds*1000:.0f}ms"
    elif seconds < 60:
        return f"{seconds:.1f}s"
    elif seconds < 3600:
        minutes = int(seconds // 60)
        secs = seconds % 60
        return f"{minutes}m {secs:.0f}s"
    else:
        hours = int(seconds // 3600)
        minutes = int((seconds % 3600) // 60)
        return f"{hours}h {minutes}m"


class LivePipelineDisplay:
    """
    Live updating pipeline display.

    Shows all stages, jobs, and steps upfront with:
    - ○ pending items (dim)
    - ◐ running items (with spinner)
    - ✓ completed items (green)
    - ✗ failed items (red)
    - ⊘ skipped items (dim)
    """

    # Status icons and colors
    ICONS = {
        ItemStatus.PENDING: "○",
        ItemStatus.RUNNING: "◐",
        ItemStatus.SUCCESS: "✓",
        ItemStatus.FAILED: "✗",
        ItemStatus.SKIPPED: "⊘",
        ItemStatus.CANCELLED: "⚠",
    }

    COLORS = {
        ItemStatus.PENDING: "dim",
        ItemStatus.RUNNING: "yellow",
        ItemStatus.SUCCESS: "green",
        ItemStatus.FAILED: "red",
        ItemStatus.SKIPPED: "dim",
        ItemStatus.CANCELLED: "yellow",
    }

    def __init__(self, pipeline_name: str):
        """Initialize live display."""
        self.pipeline_name = pipeline_name
        self.stages: list[StageState] = []
        self.stage_map: Dict[str, StageState] = {}
        self.job_map: Dict[str, JobState] = {}
        self.step_map: Dict[str, StepState] = {}  # key: "job_name:step_name"
        self.console = Console() if RICH_AVAILABLE else None
        self._live: Optional[Live] = None
        self._start_time = 0.0
        self._refresh_thread: Optional[threading.Thread] = None
        self._stop_refresh = False
        self._shutdown_requested = False

    def add_stage(self, name: str, jobs: list[dict]):
        """
        Add a stage with its jobs and steps.

        Args:
            name: Stage name
            jobs: List of job dicts with 'name', 'image', 'steps' keys
        """
        stage = StageState(name=name)

        for job_data in jobs:
            job = JobState(
                name=job_data['name'],
                image=job_data.get('image', 'default'),
            )
            for step_data in job_data.get('steps', []):
                step = StepState(name=step_data['name'], job_name=job_data['name'])
                job.steps.append(step)
                self.step_map[f"{job.name}:{step.name}"] = step

            stage.jobs.append(job)
            self.job_map[job.name] = job

        self.stages.append(stage)
        self.stage_map[name] = stage

    def start(self):
        """Start the live display."""
        if not RICH_AVAILABLE or not self.console:
            return

        self._start_time = time.time()
        self._stop_refresh = False
        self._live = Live(
            self._render(),
            console=self.console,
            refresh_per_second=20,
            transient=False,
        )
        self._live.start()

        # Start background refresh thread for smooth animations
        self._refresh_thread = threading.Thread(target=self._refresh_loop, daemon=True)
        self._refresh_thread.start()

    def _refresh_loop(self):
        """Background thread to refresh display for smooth animations."""
        while not self._stop_refresh:
            if self._live:
                try:
                    self._live.update(self._render())
                except Exception:
                    pass
            time.sleep(0.1)  # 10 FPS refresh

    def stop(self):
        """Stop the live display."""
        self._stop_refresh = True
        if self._refresh_thread:
            self._refresh_thread.join(timeout=0.5)
            self._refresh_thread = None
        if self._live:
            # Final render before stopping
            self._live.update(self._render())
            self._live.stop()
            self._live = None

    def update(self):
        """Update the live display."""
        # The background thread handles continuous updates
        pass

    # Status update methods

    def stage_start(self, name: str):
        """Mark a stage as running."""
        if name in self.stage_map:
            self.stage_map[name].status = ItemStatus.RUNNING
            self.update()

    def stage_end(self, name: str, success: bool, duration: float):
        """Mark a stage as completed."""
        if name in self.stage_map:
            stage = self.stage_map[name]
            stage.status = ItemStatus.SUCCESS if success else ItemStatus.FAILED
            stage.duration = duration
            self.update()

    def job_start(self, name: str):
        """Mark a job as running."""
        if name in self.job_map:
            self.job_map[name].status = ItemStatus.RUNNING
            self.update()

    def job_end(self, name: str, success: bool, duration: float):
        """Mark a job as completed."""
        if name in self.job_map:
            job = self.job_map[name]
            job.status = ItemStatus.SUCCESS if success else ItemStatus.FAILED
            job.duration = duration
            self.update()

    def job_skipped(self, name: str):
        """Mark a job as skipped."""
        if name in self.job_map:
            self.job_map[name].status = ItemStatus.SKIPPED
            self.update()

    def step_start(self, job_name: str, step_name: str):
        """Mark a step as running."""
        key = f"{job_name}:{step_name}"
        if key in self.step_map:
            self.step_map[key].status = ItemStatus.RUNNING
            self.update()

    def step_end(self, job_name: str, step_name: str, success: bool, duration: float, output: str = ""):
        """Mark a step as completed."""
        key = f"{job_name}:{step_name}"
        if key in self.step_map:
            step = self.step_map[key]
            step.status = ItemStatus.SUCCESS if success else ItemStatus.FAILED
            step.duration = duration
            if not success and output:
                step.output = output
            self.update()

    def job_report(self, job_name: str, report_result):
        """Set report summary for a job."""
        if job_name in self.job_map:
            job = self.job_map[job_name]
            if report_result:
                if report_result.junit and report_result.junit.tests > 0:
                    job.report.has_junit = True
                    job.report.tests_passed = report_result.junit.passed
                    job.report.tests_failed = report_result.junit.failures + report_result.junit.errors
                    job.report.tests_skipped = report_result.junit.skipped
                if report_result.coverage and report_result.coverage.lines_total > 0:
                    job.report.has_coverage = True
                    job.report.coverage_percent = report_result.coverage.line_percent
            self.update()

    def shutdown(self, signal_name: str):
        """Handle graceful shutdown - mark running items as cancelled."""
        self._shutdown_requested = True

        # Mark all running jobs as cancelled
        for job in self.job_map.values():
            if job.status == ItemStatus.RUNNING:
                job.status = ItemStatus.CANCELLED
                # Mark running steps as cancelled too
                for step in job.steps:
                    if step.status == ItemStatus.RUNNING:
                        step.status = ItemStatus.CANCELLED

        # Mark running stages as cancelled
        for stage in self.stages:
            if stage.status == ItemStatus.RUNNING:
                stage.status = ItemStatus.CANCELLED

        self.update()

    def _get_spinner_char(self) -> str:
        """Get current spinner character based on time."""
        spinner_chars = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
        # Use time-based frame for smooth animation
        frame = int(time.time() * 10) % len(spinner_chars)
        return spinner_chars[frame]

    def _render(self) -> Group:
        """Render the pipeline display."""
        if not RICH_AVAILABLE:
            return None

        # Calculate elapsed time
        elapsed = time.time() - self._start_time if self._start_time else 0

        # Count status
        total_jobs = sum(len(s.jobs) for s in self.stages)
        completed_jobs = sum(
            1 for s in self.stages for j in s.jobs
            if j.status in (ItemStatus.SUCCESS, ItemStatus.FAILED, ItemStatus.SKIPPED)
        )
        failed_jobs = sum(
            1 for s in self.stages for j in s.jobs
            if j.status == ItemStatus.FAILED
        )

        # Header with progress
        progress_color = "red" if failed_jobs > 0 else "cyan"
        header = Text()

        if self._shutdown_requested:
            header.append("⚠ ", style="bold yellow")
            header.append("SHUTTING DOWN ", style="bold yellow")
            header.append("(Ctrl+C to force) ", style="dim yellow")
        else:
            header.append("🚀 ", style="bold")

        header.append(self.pipeline_name, style="bold white")
        header.append(f"  {completed_jobs}/{total_jobs}", style=progress_color)
        header.append(f"  {format_duration(elapsed)}", style="dim")

        # Build the tree
        tree = Tree(header)

        spinner = self._get_spinner_char()

        for stage in self.stages:
            stage_icon = self.ICONS[stage.status]
            stage_color = self.COLORS[stage.status]

            if stage.status == ItemStatus.RUNNING:
                stage_icon = spinner

            stage_duration = f" ({format_duration(stage.duration)})" if stage.duration else ""
            stage_label = f"[{stage_color}]{stage_icon} {stage.name}[/][dim]{stage_duration}[/]"
            stage_tree = tree.add(stage_label)

            for job in stage.jobs:
                job_icon = self.ICONS[job.status]
                job_color = self.COLORS[job.status]

                if job.status == ItemStatus.RUNNING:
                    job_icon = spinner

                job_duration = f" ({format_duration(job.duration)})" if job.duration else ""
                job_label = f"[{job_color}]{job_icon} {job.name}[/] [dim]{job.image}{job_duration}[/]"
                job_tree = stage_tree.add(job_label)

                for step in job.steps:
                    step_icon = self.ICONS[step.status]
                    step_color = self.COLORS[step.status]

                    if step.status == ItemStatus.RUNNING:
                        step_icon = spinner

                    step_duration = f" {format_duration(step.duration)}" if step.duration else ""
                    step_label = f"[{step_color}]{step_icon}[/] [dim]{step.name}{step_duration}[/]"
                    job_tree.add(step_label)

                # Show report summary if available
                if job.report.has_junit or job.report.has_coverage:
                    report_parts = []
                    if job.report.has_junit:
                        if job.report.tests_failed > 0:
                            report_parts.append(f"[green]{job.report.tests_passed} passed[/]")
                            report_parts.append(f"[red]{job.report.tests_failed} failed[/]")
                        else:
                            report_parts.append(f"[green]{job.report.tests_passed} passed[/]")
                        if job.report.tests_skipped > 0:
                            report_parts.append(f"[yellow]{job.report.tests_skipped} skipped[/]")
                    if job.report.has_coverage:
                        cov = job.report.coverage_percent
                        cov_color = "green" if cov >= 70 else ("yellow" if cov >= 50 else "red")
                        report_parts.append(f"[{cov_color}]{cov:.0f}% cov[/]")
                    if report_parts:
                        report_label = f"[dim]├─[/] {' · '.join(report_parts)}"
                        job_tree.add(report_label)

        return Group(tree)

    def print_summary(self, result):
        """Print final summary table."""
        if not RICH_AVAILABLE or not self.console:
            return

        table = Table(title="Pipeline Summary", show_header=True, header_style="bold")
        table.add_column("Stage", style="cyan")
        table.add_column("Job", style="white")
        table.add_column("Status")
        table.add_column("Tests", justify="right")
        table.add_column("Coverage", justify="right")
        table.add_column("Duration", justify="right")

        for stage_result in result.stage_results:
            first_job = True
            for job_result in stage_result.job_results:
                stage_name = stage_result.stage.name if first_job else ""

                # Determine status style based on status
                if job_result.success:
                    status_style = "green"
                elif job_result.status.value == "skipped":
                    status_style = "dim"
                else:
                    status_style = "red"
                status_text = job_result.status.value

                # Get report info from job state
                job_state = self.job_map.get(job_result.job.name)
                tests_text = ""
                coverage_text = ""

                if job_state and job_state.report.has_junit:
                    r = job_state.report
                    if r.tests_failed > 0:
                        tests_text = f"[green]{r.tests_passed}[/]/[red]{r.tests_failed}[/]"
                    else:
                        tests_text = f"[green]{r.tests_passed}[/]"

                if job_state and job_state.report.has_coverage:
                    cov = job_state.report.coverage_percent
                    cov_color = "green" if cov >= 70 else ("yellow" if cov >= 50 else "red")
                    coverage_text = f"[{cov_color}]{cov:.0f}%[/]"

                table.add_row(
                    stage_name,
                    job_result.job.name,
                    f"[{status_style}]{status_text}[/]",
                    tests_text,
                    coverage_text,
                    format_duration(job_result.duration_seconds),
                )
                first_job = False

        self.console.print()
        self.console.print(table)
        self.console.print(
            f"\n[bold]Total duration:[/bold] {format_duration(result.duration_seconds)}"
        )

        # Show failed step logs
        failed_steps = [
            step for step in self.step_map.values()
            if step.status == ItemStatus.FAILED and step.output
        ]

        if failed_steps:
            self.console.print()
            self.console.print("[bold red]Failed Step Logs:[/]")
            for step in failed_steps:
                self.console.print(f"\n[bold red]✗ {step.job_name} → {step.name}[/]")
                self.console.print("[dim]" + "─" * 60 + "[/]")
                # Show last 30 lines of output
                lines = step.output.strip().split('\n')
                if len(lines) > 30:
                    self.console.print("[dim]... (truncated, showing last 30 lines)[/]")
                    lines = lines[-30:]
                for line in lines:
                    self.console.print(f"  [dim]{line}[/]")

        # Final status
        self.console.print()
        if self._shutdown_requested:
            self.console.print("[bold yellow]⚠ Pipeline cancelled (shutdown requested)[/]")
        elif result.success:
            self.console.print("[bold green]✓ Pipeline completed successfully[/]")
        else:
            self.console.print("[bold red]✗ Pipeline failed[/]")


@contextmanager
def live_pipeline_display(pipeline):
    """
    Context manager for live pipeline display.

    Args:
        pipeline: Pipeline object

    Yields:
        LivePipelineDisplay instance
    """
    display = LivePipelineDisplay(pipeline.name)

    # Populate the display with pipeline structure
    for stage in pipeline.stages:
        jobs = []
        for job in stage.jobs:
            # Expand matrix jobs
            expanded = job.get_expanded_jobs()
            for exp_job in expanded:
                jobs.append({
                    'name': exp_job.name,
                    'image': exp_job.image,
                    'steps': [{'name': s.name} for s in exp_job.steps],
                })
        display.add_stage(stage.name, jobs)

    try:
        display.start()
        yield display
    finally:
        display.stop()
