# -*- coding: utf-8 -*-
"""
Background worker for blocking git operations.

H5 (audit 2026-04-27): clone / pull / commit / push and ls-remote can each
take up to ``GitClient.timeout_seconds`` (30s by default), so chaining
them on the Qt main thread freezes QGIS for ~2 min in the worst case.

The worker is intentionally tiny — it just runs an arbitrary zero-arg
callable on a ``QThread`` and re-emits the outcome on the main thread via
queued signals. Callers (publish dialog, repo manager dialog) build a
closure capturing exactly the operation they want offloaded.
"""

from __future__ import annotations

import logging
from typing import Any, Callable, Iterable, Optional

try:
    from qgis.PyQt.QtCore import QThread, pyqtSignal
    HAS_QT = True
except ImportError:  # pragma: no cover — headless / standalone tests
    HAS_QT = False

    # Provide a minimal stand-in so this module is importable without Qt.
    class QThread:  # type: ignore[no-redef]
        def __init__(self, parent=None) -> None:
            self._parent = parent

        def start(self) -> None:
            self.run()

        def quit(self) -> None:
            pass

        def wait(self, *_a, **_kw) -> bool:
            return True

        def isRunning(self) -> bool:
            return False

        def run(self) -> None:
            pass

    def pyqtSignal(*_a, **_kw):  # type: ignore[no-redef]
        class _Signal:
            def connect(self, *_a, **_kw):
                pass

            def emit(self, *_a, **_kw):
                pass

        return _Signal()


logger = logging.getLogger('FilterMate.FavoritesSharing.UI.GitWorker')


class GitOpsWorker(QThread):
    """Run ``op_callable`` on a worker thread and emit the result.

    Signals:
        finished(object): emitted with the callable's return value on
            successful completion.
        error(str): emitted with a scrubbed message when the callable
            raises. The full exception is logged at exception level on
            the worker thread for diagnostics.

    The worker owns no Qt parents that touch the UI — connections from
    callers are auto-resolved to ``Qt.QueuedConnection`` because the
    sender lives on the worker thread, so signal handlers fire safely on
    the main thread.
    """

    finished = pyqtSignal(object)
    error = pyqtSignal(str)

    def __init__(
        self,
        op_callable: Callable[[], Any],
        parent: Optional[Any] = None,
    ) -> None:
        super().__init__(parent)
        self._op = op_callable

    def run(self) -> None:  # type: ignore[override]
        try:
            result = self._op()
        except Exception as exc:
            # Full traceback to logs; only the message goes back to UI.
            logger.exception("Git worker raised")
            try:
                self.error.emit(str(exc))
            except Exception:  # nosec B110 - Qt teardown: dialog already gone, nothing left to do
                # If signal emission itself fails (Qt teardown), there's
                # nothing left for us to do — the dialog is gone.
                pass
            return
        try:
            self.finished.emit(result)
        except Exception:
            logger.debug("Result signal emit failed (dialog torn down?)")


def start_git_worker(
    op_callable: Callable[[], Any],
    on_finished: Callable[[Any], None],
    on_error: Callable[[str], None],
) -> "GitOpsWorker":
    """Build, wire and start a one-shot :class:`GitOpsWorker`.

    Centralises the H5 lifecycle ritual the audit (2026-04-27) locked
    in: ``parent=None`` so dialog close cannot Qt-destroy a still-running
    thread, ``deleteLater`` self-cleanup on terminal signals, then
    ``start``. The caller is expected to keep a Python reference to the
    returned worker (e.g. ``self._publish_worker = start_git_worker(...)``)
    so the GC does not collect it before ``run`` finishes.
    """
    worker = GitOpsWorker(op_callable, parent=None)
    worker.finished.connect(worker.deleteLater)
    worker.error.connect(worker.deleteLater)
    worker.finished.connect(on_finished)
    worker.error.connect(on_error)
    worker.start()
    return worker


def gracefully_close_worker(
    worker: Optional[Any],
    *,
    timeout_ms: int,
    signal_names: Iterable[str] = ("finished", "error"),
    cancel: bool = False,
) -> None:
    """Detach + wait for an in-flight QThread worker before dialog teardown.

    EXT-7 (audit 2026-04-29): the publish/repo-manager/git-binary
    dialogs all rolled their own variant of "if worker is running:
    disconnect signals, then wait()". Centralised here so the H5
    invariant (terminate() corrupts in-flight git subprocesses) stays
    enforced in one place.

    - ``timeout_ms`` caps the wait so QGIS never hangs on a stuck call;
      tune to slightly more than the longest underlying op timeout.
    - ``signal_names`` lists which Qt signals to disconnect first, so a
      queued finished/error cannot fire on a half-destroyed dialog
      after wait() returns. Defaults match :func:`start_git_worker`'s
      surface.
    - ``cancel=True`` calls ``worker.request_cancel()`` first, for
      workers (e.g. PortableGitDownloadWorker) that cooperate with
      cancellation rather than running to completion.
    """
    if worker is None or not worker.isRunning():
        return
    if cancel:
        try:
            worker.request_cancel()
        except AttributeError:
            logger.debug("Worker has no request_cancel(); skipping cancel hook.")
    for sig_name in signal_names:
        try:
            getattr(worker, sig_name).disconnect()
        except (TypeError, RuntimeError, AttributeError):
            pass
    worker.wait(timeout_ms)
