"""Orphan jobs screen for the TUI"""

from typing import Optional
from textual.app import ComposeResult
from textual.containers import Vertical
from textual.widgets import Header, Footer, DataTable, Static
from textual.screen import Screen
from textual.binding import Binding

from experimaestro.scheduler.state_provider import StateProvider
from experimaestro.tui.utils import get_status_icon
from experimaestro.tui.dialogs import DeleteConfirmScreen
from experimaestro.tui.messages import SizeCalculated


class OrphanJobsScreen(Screen):
    """Screen for viewing and managing orphan jobs"""

    BINDINGS = [
        Binding("ctrl+d", "delete_selected", "Delete"),
        Binding("ctrl+shift+d", "delete_all", "Delete All", key_display="^D"),
        Binding("escape", "go_back", "Back"),
        Binding("q", "go_back", "Quit"),
        Binding("r", "refresh", "Refresh"),
        Binding("f", "copy_path", "Copy Path", show=False),
        Binding("T", "sort_by_task", "Sort Task", show=False),
        Binding("Z", "sort_by_size", "Sort Size", show=False),
    ]

    _size_cache: dict = {}  # Class-level cache (formatted strings)
    _size_bytes_cache: dict = {}  # Class-level cache (raw bytes for sorting)

    def __init__(self, state_provider: StateProvider) -> None:
        super().__init__()
        self.state_provider = state_provider
        self.orphan_jobs = []
        self._pending_jobs = []  # Jobs waiting for size calculation
        self._sort_column: Optional[str] = None
        self._sort_reverse: bool = False

    def compose(self) -> ComposeResult:
        yield Header()
        with Vertical(id="orphan-container"):
            yield Static("Orphan Jobs", id="orphan-title")
            yield Static("", id="orphan-stats")
            yield DataTable(id="orphan-table", cursor_type="row")
            yield Static("", id="orphan-job-info")
        yield Footer()

    def on_mount(self) -> None:
        """Initialize the orphan jobs table"""
        table = self.query_one("#orphan-table", DataTable)
        table.add_column("⚑", key="status", width=3)
        table.add_column("Job ID", key="job_id", width=10)
        table.add_column("Task", key="task")
        table.add_column("Size", key="size", width=10)
        self.refresh_orphans()

    def action_sort_by_task(self) -> None:
        """Sort by task name"""
        if self._sort_column == "task":
            self._sort_reverse = not self._sort_reverse
        else:
            self._sort_column = "task"
            self._sort_reverse = False
        self._rebuild_table()
        order = "desc" if self._sort_reverse else "asc"
        self.notify(f"Sorted by task ({order})", severity="information")

    def action_sort_by_size(self) -> None:
        """Sort by size"""
        if self._sort_column == "size":
            self._sort_reverse = not self._sort_reverse
        else:
            self._sort_column = "size"
            self._sort_reverse = True  # Default: largest first
        self._rebuild_table()
        order = "largest first" if self._sort_reverse else "smallest first"
        self.notify(f"Sorted by size ({order})", severity="information")

    def _get_sorted_jobs(self):
        """Return jobs sorted by current sort column"""
        jobs = self.orphan_jobs[:]
        if self._sort_column == "task":
            jobs.sort(key=lambda j: j.task_id or "", reverse=self._sort_reverse)
        elif self._sort_column == "size":
            # Sort by raw bytes, jobs not in cache go to end
            jobs.sort(
                key=lambda j: self._size_bytes_cache.get(j.identifier, -1),
                reverse=self._sort_reverse,
            )
        return jobs

    def _rebuild_table(self) -> None:
        """Rebuild the table with current sort order"""
        table = self.query_one("#orphan-table", DataTable)
        table.clear()

        for job in self._get_sorted_jobs():
            failure_reason = getattr(job, "failure_reason", None)
            transient = getattr(job, "transient", None)
            status_icon = get_status_icon(
                job.state.name if job.state else "unknown", failure_reason, transient
            )
            if job.identifier in self._size_cache:
                size_text = self._size_cache[job.identifier]
            else:
                size_text = "waiting"
            table.add_row(
                status_icon,
                job.identifier[:7],
                job.task_id,
                size_text,
                key=job.identifier,
            )

    def refresh_orphans(self) -> None:
        """Refresh the orphan jobs list"""
        # Only include orphan jobs that have an existing folder
        all_orphans = self.state_provider.get_orphan_jobs()
        self.orphan_jobs = [j for j in all_orphans if j.path and j.path.exists()]

        # Update stats
        stats = self.query_one("#orphan-stats", Static)
        stats.update(f"Found {len(self.orphan_jobs)} orphan jobs")

        # Collect jobs needing size calculation
        self._pending_jobs = [
            j for j in self.orphan_jobs if j.identifier not in self._size_cache
        ]

        # Rebuild table
        self._rebuild_table()

        # Start calculating sizes
        if self._pending_jobs:
            self._calculate_next_size()

    def _calculate_next_size(self) -> None:
        """Calculate size for the next pending job using a worker"""
        if not self._pending_jobs:
            return

        job = self._pending_jobs.pop(0)
        # Update to "calc..."
        self._update_size_cell(job.identifier, "calc...")
        # Run calculation in worker thread
        self.run_worker(
            self._calc_size_worker(job.identifier, job.path),
            thread=True,
        )

    async def _calc_size_worker(self, job_id: str, path):
        """Worker to calculate folder size"""
        size_bytes = await self._get_folder_size_async(path)
        size_str = self._format_size(size_bytes)
        self._size_cache[job_id] = size_str
        self._size_bytes_cache[job_id] = size_bytes
        self.post_message(SizeCalculated(job_id, size_str, size_bytes))

    def on_size_calculated(self, message: SizeCalculated) -> None:
        """Handle size calculation completion"""
        self._size_bytes_cache[message.job_id] = message.size_bytes
        self._update_size_cell(message.job_id, message.size)
        # Calculate next one
        self._calculate_next_size()

    @staticmethod
    async def _get_folder_size_async(path) -> int:
        """Calculate total size of a folder using du command if available"""
        import asyncio
        import shutil
        import sys

        # Try using du command for better performance
        if shutil.which("du"):
            try:
                if sys.platform == "darwin":
                    # macOS: du -sk gives size in KB
                    proc = await asyncio.create_subprocess_exec(
                        "du",
                        "-sk",
                        str(path),
                        stdout=asyncio.subprocess.PIPE,
                        stderr=asyncio.subprocess.DEVNULL,
                    )
                    stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=30)
                    if proc.returncode == 0 and stdout:
                        # Output format: "SIZE\tPATH"
                        size_kb = int(stdout.decode().split()[0])
                        return size_kb * 1024
                else:
                    # Linux: du -sb gives size in bytes
                    proc = await asyncio.create_subprocess_exec(
                        "du",
                        "-sb",
                        str(path),
                        stdout=asyncio.subprocess.PIPE,
                        stderr=asyncio.subprocess.DEVNULL,
                    )
                    stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=30)
                    if proc.returncode == 0 and stdout:
                        # Output format: "SIZE\tPATH"
                        return int(stdout.decode().split()[0])
            except (asyncio.TimeoutError, ValueError, IndexError, OSError):
                pass  # Fall back to Python implementation

        # Fallback: Python implementation
        return OrphanJobsScreen._get_folder_size_sync(path)

    @staticmethod
    def _get_folder_size_sync(path) -> int:
        """Calculate total size of a folder using Python (fallback)"""
        total = 0
        try:
            for entry in path.rglob("*"):
                if entry.is_file():
                    total += entry.stat().st_size
        except (OSError, PermissionError):
            pass
        return total

    @staticmethod
    def _format_size(size: int) -> str:
        """Format size in human-readable format"""
        for unit in ["B", "KB", "MB", "GB"]:
            if size < 1024:
                return f"{size:.1f}{unit}" if unit != "B" else f"{size}{unit}"
            size /= 1024
        return f"{size:.1f}TB"

    def _update_size_cell(self, job_id: str, value: str = None) -> None:
        """Update the size cell for a job"""
        try:
            table = self.query_one("#orphan-table", DataTable)
            size_text = (
                value if value is not None else self._size_cache.get(job_id, "-")
            )
            table.update_cell(job_id, "size", size_text)
        except Exception:
            pass  # Table may have changed

    def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
        """Show job details when a row is selected"""
        self._update_job_info()

    def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None:
        """Show job details when cursor moves"""
        self._update_job_info()

    def _update_job_info(self) -> None:
        """Update the job info display"""
        table = self.query_one("#orphan-table", DataTable)
        info = self.query_one("#orphan-job-info", Static)

        if table.cursor_row is None:
            info.update("")
            return

        row_key = list(table.rows.keys())[table.cursor_row]
        if row_key:
            job_id = str(row_key.value)
            job = next((j for j in self.orphan_jobs if j.identifier == job_id), None)
            if job and job.path:
                size = self._size_cache.get(job.identifier, "calculating...")
                info.update(f"Path: {job.path}  |  Size: {size}")
            else:
                info.update("")

    def action_copy_path(self) -> None:
        """Copy the job folder path to clipboard"""
        import pyperclip

        table = self.query_one("#orphan-table", DataTable)
        if table.cursor_row is None:
            return

        row_key = list(table.rows.keys())[table.cursor_row]
        if row_key:
            job_id = str(row_key.value)
            job = next((j for j in self.orphan_jobs if j.identifier == job_id), None)
            if job and job.path:
                try:
                    pyperclip.copy(str(job.path))
                    self.notify("Path copied", severity="information")
                except Exception as e:
                    self.notify(f"Failed to copy: {e}", severity="error")

    def action_delete_selected(self) -> None:
        """Delete the selected orphan job"""
        table = self.query_one("#orphan-table", DataTable)
        if table.cursor_row is None:
            return

        row_key = list(table.rows.keys())[table.cursor_row]
        if row_key:
            job_id = str(row_key.value)
            job = next((j for j in self.orphan_jobs if j.identifier == job_id), None)
            if job:
                self._delete_job(job)

    def _delete_job(self, job) -> None:
        """Delete a single orphan job with confirmation"""

        def handle_delete(confirmed: bool) -> None:
            if confirmed:
                success, msg = self.state_provider.delete_job_safely(job)
                if success:
                    self.notify(msg, severity="information")
                    self.refresh_orphans()
                else:
                    self.notify(msg, severity="error")

        self.app.push_screen(
            DeleteConfirmScreen("orphan job", job.identifier),
            handle_delete,
        )

    def action_delete_all(self) -> None:
        """Delete all orphan jobs"""
        if not self.orphan_jobs:
            self.notify("No orphan jobs to delete", severity="warning")
            return

        # Filter out running jobs
        deletable_jobs = [j for j in self.orphan_jobs if not j.state.running()]

        if not deletable_jobs:
            self.notify("All orphan jobs are running", severity="warning")
            return

        def handle_delete_all(confirmed: bool) -> None:
            if confirmed:
                deleted = 0
                for job in deletable_jobs:
                    success, _ = self.state_provider.delete_job_safely(
                        job, cascade_orphans=False
                    )
                    if success:
                        deleted += 1

                # Clean up orphan partials once at the end
                self.state_provider.cleanup_orphan_partials(perform=True)

                self.notify(f"Deleted {deleted} orphan jobs", severity="information")
                self.refresh_orphans()

        self.app.push_screen(
            DeleteConfirmScreen(
                "all orphan jobs",
                f"{len(deletable_jobs)} jobs",
                "This action cannot be undone",
            ),
            handle_delete_all,
        )

    def action_refresh(self) -> None:
        """Refresh the orphan jobs list"""
        self.refresh_orphans()

    def action_go_back(self) -> None:
        """Go back to main screen"""
        self.dismiss()
