"""UI-facing orchestration for file browser operations."""

from __future__ import annotations

from collections.abc import Callable, Mapping, Sequence
from concurrent.futures import CancelledError as FutureCancelledError
from contextlib import nullcontext, suppress
from dataclasses import dataclass
from pathlib import Path
from typing import Any

from ...core.errors import CancelledError
from ...core.sheet_traits import resolve_sheet_traits
from ...core.viewer import Viewer
from ..presenters import StatusPresenter
from .file_browser import (
    FileBrowserController,
    FileConvertPlan,
    FileConvertResult,
    FileTransferPlan,
)


@dataclass(slots=True)
class _PathEntry:
    path: Path


class _ConvertJobHandle:
    def __init__(
        self,
        future: Any,
        *,
        skipped: int,
        on_done: Callable[[], None] | None = None,
        on_error: Callable[[tuple[Path, str]], None] | None = None,
        status_source: str | None = None,
    ) -> None:
        self._future = future
        self._skipped = skipped
        self._on_done = on_done
        self._on_done_called = False
        self._on_error = on_error
        self._status_source = status_source

    def cancel(self) -> None:
        if self._future is None:
            return
        if hasattr(self._future, "cancel"):
            with suppress(Exception):
                self._future.cancel()

    def consume_update(self, viewer: Viewer) -> bool:
        future = self._future
        if future is None:
            self._trigger_on_done()
            return True
        if not future.done():
            return False
        try:
            result = future.result()
        except (CancelledError, FutureCancelledError):
            with self._status_source_context(viewer, self._status_source):
                viewer.status_message = "convert canceled"
            self._trigger_on_done()
            return True
        except Exception as exc:
            with self._status_source_context(viewer, self._status_source):
                viewer.status_message = f"convert error: {exc}"[:120]
            self._trigger_on_done()
            return True

        status_source = getattr(result, "status_source", None) or self._status_source
        payload: FileConvertResult | None = getattr(result, "value", None)
        completed = payload.completed if payload is not None else 0
        errors = payload.errors if payload is not None else []

        if errors and self._on_error is not None:
            self._on_error(errors[0])
        message = None
        if completed > 0:
            message = self._format_status(completed, len(errors), self._skipped)
        if message:
            with self._status_source_context(viewer, status_source):
                viewer.status_message = message
        self._trigger_on_done()
        return True

    def _format_status(self, completed: int, errors: int, skipped: int) -> str:
        if completed <= 0 and errors:
            return "convert failed"
        if completed <= 0:
            return "convert finished"
        suffix = "" if completed == 1 else "s"
        extra = []
        if errors:
            extra.append(f"{errors} error{'s' if errors != 1 else ''}")
        if skipped:
            extra.append(f"{skipped} skipped")
        extra_text = f" ({', '.join(extra)})" if extra else ""
        return f"Converted {completed} item{suffix}{extra_text}"

    def _trigger_on_done(self) -> None:
        if self._on_done_called:
            return
        self._on_done_called = True
        if self._on_done is None:
            return
        try:
            self._on_done()
        except Exception:
            return

    @staticmethod
    def _status_source_context(viewer: Viewer, source: str | None):
        binder = getattr(viewer, "bind_status_source", None)
        if callable(binder):
            return binder(source)
        return nullcontext()


class FileOpsController:
    """Coordinates file browser operations with screen presentation hooks."""

    def __init__(
        self,
        *,
        file_browser: FileBrowserController,
        presenter: StatusPresenter,
        get_viewer: Callable[[], Viewer | None],
        refresh: Callable[[], None],
        handle_file_browser_refresh: Callable[[Any], None],
        invalidate: Callable[[], None],
        register_job: Callable[[Viewer, object | None], None],
    ) -> None:
        self._file_browser = file_browser
        self._presenter = presenter
        self._get_viewer = get_viewer
        self._refresh = refresh
        self._handle_file_browser_refresh = handle_file_browser_refresh
        self._invalidate = invalidate
        self._register_job = register_job

    def request_transfer(
        self, operation: str, dest: str, *, source_paths: Sequence[Path] | None = None
    ) -> None:
        viewer = self._get_viewer()
        sheet = getattr(viewer, "sheet", None) if viewer is not None else None
        if viewer is None or sheet is None or not resolve_sheet_traits(sheet).is_file_browser:
            return

        if source_paths is None:
            entries, status = self._file_browser.resolve_entries(viewer=viewer)
            if status:
                viewer.status_message = status
            if not entries:
                self._refresh()
                return
        else:
            entries = [_PathEntry(path=path) for path in source_paths]
            if not entries:
                viewer.status_message = "select at least one item"
                self._refresh()
                return

        plan: FileTransferPlan = self._file_browser.plan_transfer(
            operation, dest, entries=entries, sheet=sheet
        )
        if plan.error:
            viewer.status_message = plan.error
            self._refresh()
            return

        if plan.missing_directories:
            count = len(plan.missing_directories)
            noun = "directory" if count == 1 else "directories"
            verb = "does" if count == 1 else "do"
            display_path = plan.missing_directories[0]
            prompt = "Create it?" if count == 1 else "Create them?"
            message_lines = [
                f"{count} destination {noun} {verb} not exist.",
                f"First: {display_path}",
                prompt,
            ]

            def _create_and_execute() -> None:
                self.perform_transfer(operation, plan.targets, allow_overwrite=bool(plan.conflicts))

            self._presenter.open_confirmation_modal(
                title="Create destination directories",
                message_lines=message_lines,
                on_confirm=_create_and_execute,
                context_type="file_transfer_mkdir",
                payload={"count": count, "op": operation},
            )
            self._refresh()
            return

        def _execute() -> None:
            self.perform_transfer(operation, plan.targets, allow_overwrite=bool(plan.conflicts))

        if plan.conflicts:
            title = "Overwrite existing items"
            count = len(plan.conflicts)
            suffix = "" if count == 1 else "s"
            display_path = plan.conflicts[0]
            message_lines = [
                f"{count} destination item{suffix} already exist.",
                f"First: {display_path}",
                "Overwrite them?",
            ]
            self._presenter.open_confirmation_modal(
                title=title,
                message_lines=message_lines,
                on_confirm=_execute,
                context_type="file_transfer_overwrite",
                payload={"count": count, "op": operation},
            )
            self._refresh()
            return

        _execute()

    def request_rename(self, new_name: str) -> None:
        viewer = self._get_viewer()
        sheet = getattr(viewer, "sheet", None) if viewer is not None else None
        if viewer is None or sheet is None or not resolve_sheet_traits(sheet).is_file_browser:
            return

        entries, status = self._file_browser.resolve_entries(viewer=viewer)
        if status:
            viewer.status_message = status
        if not entries:
            self._refresh()
            return
        if len(entries) != 1:
            viewer.status_message = "select exactly one item to rename"
            self._refresh()
            return

        message, error = self._file_browser.rename_entry(
            sheet=sheet, entry=entries[0], new_name=new_name
        )
        if error:
            viewer.status_message = error
            self._refresh()
            return

        self._handle_file_browser_refresh(sheet)
        self._refresh()
        if message:
            viewer.status_message = message

    def request_mkdir(self, path: str) -> None:
        viewer = self._get_viewer()
        sheet = getattr(viewer, "sheet", None) if viewer is not None else None
        if viewer is None or sheet is None or not resolve_sheet_traits(sheet).is_file_browser:
            return

        message, error = self._file_browser.make_directory(sheet=sheet, dest=path)
        if error:
            viewer.status_message = error
            self._refresh()
            return

        self._handle_file_browser_refresh(sheet)
        self._refresh()
        if message:
            viewer.status_message = message

    def request_convert(
        self,
        dest: str,
        *,
        format_hint: str | None,
        options: Mapping[str, Any] | None,
    ) -> None:
        viewer = self._get_viewer()
        sheet = getattr(viewer, "sheet", None) if viewer is not None else None
        if viewer is None or sheet is None or not resolve_sheet_traits(sheet).is_file_browser:
            return

        entries, status = self._file_browser.resolve_entries(viewer=viewer)
        if status:
            viewer.status_message = status
        if not entries:
            self._refresh()
            return

        plan = self._file_browser.plan_convert(
            dest,
            format_hint=format_hint,
            entries=entries,
            sheet=sheet,
        )
        if plan.error:
            viewer.status_message = plan.error
            self._refresh()
            return

        if plan.missing_directories:
            count = len(plan.missing_directories)
            noun = "directory" if count == 1 else "directories"
            verb = "does" if count == 1 else "do"
            display_path = plan.missing_directories[0]
            prompt = "Create it?" if count == 1 else "Create them?"
            message_lines = [
                f"{count} destination {noun} {verb} not exist.",
                f"First: {display_path}",
                prompt,
            ]

            def _create_and_execute() -> None:
                self._perform_convert(plan, allow_overwrite=bool(plan.conflicts), options=options)

            self._presenter.open_confirmation_modal(
                title="Create destination directories",
                message_lines=message_lines,
                on_confirm=_create_and_execute,
                context_type="file_convert_mkdir",
                payload={"count": count},
            )
            self._refresh()
            return

        def _execute() -> None:
            self._perform_convert(plan, allow_overwrite=bool(plan.conflicts), options=options)

        if plan.conflicts:
            title = "Overwrite existing items"
            count = len(plan.conflicts)
            suffix = "" if count == 1 else "s"
            display_path = plan.conflicts[0]
            message_lines = [
                f"{count} destination item{suffix} already exist.",
                f"First: {display_path}",
                "Overwrite them?",
            ]
            self._presenter.open_confirmation_modal(
                title=title,
                message_lines=message_lines,
                on_confirm=_execute,
                context_type="file_convert_overwrite",
                payload={"count": count},
            )
            self._refresh()
            return

        _execute()

    def _perform_convert(
        self,
        plan: FileConvertPlan,
        *,
        allow_overwrite: bool,
        options: Mapping[str, Any] | None,
    ) -> None:
        viewer = self._get_viewer()
        sheet = getattr(viewer, "sheet", None) if viewer is not None else None
        if viewer is None or sheet is None:
            return

        future = self._file_browser.submit_convert(
            plan,
            allow_overwrite=allow_overwrite,
            options=options,
            sheet=sheet,
        )
        if future is None:
            viewer.status_message = "convert failed to start"
            self._refresh()
            return

        viewer.status_message = "Conversion in progress..."

        def _on_error(error: tuple[Path, str]) -> None:
            path, err = error
            viewer.status_message = f"convert error: {err}"[:120]
            self._presenter.open_status_modal(
                title="File conversion error",
                lines=[f"{path}: {err}"],
            )

        def _on_done() -> None:
            if resolve_sheet_traits(sheet).is_file_browser:
                self._handle_file_browser_refresh(sheet)
            with suppress(Exception):
                self._invalidate()

        handle = _ConvertJobHandle(
            future,
            skipped=len(plan.skipped),
            on_done=_on_done,
            on_error=_on_error,
            status_source=getattr(viewer, "status_source", None),
        )
        self._register_job(viewer, handle)
        self._refresh()

    def perform_transfer(
        self,
        operation: str,
        targets: list[tuple[Path, Path]],
        *,
        allow_overwrite: bool = False,
    ) -> None:
        viewer = self._get_viewer()
        if viewer is None:
            return

        message, errors, completed = self._file_browser.perform_transfer(
            operation, targets, allow_overwrite=allow_overwrite
        )
        sheet = getattr(viewer, "sheet", None)
        if (
            sheet is not None
            and resolve_sheet_traits(sheet).is_file_browser
            and (completed or errors)
        ):
            self._handle_file_browser_refresh(sheet)
        if errors:
            path, err = errors[0]
            self._presenter.open_status_modal(
                title="File operation error", lines=[f"{path}: {err}"]
            )
            if not completed:
                message = "operation failed"

        if operation == "move" and completed:
            with suppress(Exception):
                viewer._clear_selection_state()

        self._refresh()
        if message:
            viewer.status_message = message
            with suppress(Exception):
                self._invalidate()
