"""Embedded Python console — scripting inside the editor, run out-of-process.

User code runs in a SEPARATE process (:mod:`abax.console_worker`, via
:class:`~abax.gui.console_bridge.ConsoleBridge`) **on a background thread**, so a
crash, hang, or runaway there never freezes the GUI — and a runaway can be
**Interrupt**ed (which kills the worker; the next command respawns it). The live
workbook is shipped to the worker and back each command as a JSON envelope. It is
still **untrusted code running with your privileges** (gated by the consent
prompt) — the subprocess gives crash/memory isolation, not a security boundary.

Worker namespace: ``doc``, ``wb``, ``sheet()``, ``cell(ref)``, ``put(ref, val)``,
``refresh()``, ``rpn``, the engineering toolkit, and the data-science libraries
when installed.
"""

from __future__ import annotations

from .._qtcompat import (
    QCompleter,
    QDialog,
    QEvent,
    QFont,
    QHBoxLayout,
    QLineEdit,
    QPlainTextEdit,
    QPushButton,
    QStringListModel,
    Qt,
    QVBoxLayout,
)

# Identifiers bound in the console namespace (see abax.core.console_ns); offered
# for Tab-completion alongside Python keywords and builtins.
_NS_NAMES = (
    "doc", "wb", "sheet", "cell", "put", "refresh", "rpn", "compile_expr",
    "read_matrix", "write_matrix", "sheet_to_df", "df_to_sheet",
    "matrix", "eigen", "units", "numeric", "complexnum", "fft", "interp", "signal",
    "spectral", "filters", "ode", "ode_implicit", "resynth", "stats", "cluster",
    "ml", "trees", "bayes", "metrics", "gmm", "financial", "algebraic", "ti_engine",
    "rf", "rf_bands", "antenna", "antenna_impedance", "mom", "wire_mom", "nec",
    "sql", "sqlsheets", "profile", "describe", "chartsvg", "dxcc", "adif",
    "goalseek", "iq", "wbdiff", "html_report", "urlfetch",
    "np", "numpy", "pd", "pandas", "scipy", "sm", "statsmodels", "sklearn",
    "pingouin", "pg", "pymc", "pm", "sksurv",
)


def _console_words() -> list[str]:
    import builtins
    import keyword

    return sorted(set(_NS_NAMES) | set(keyword.kwlist)
                  | {n for n in dir(builtins) if not n.startswith("_")})


class _ConsoleInput(QLineEdit):
    """Console input line with explicit **Tab** identifier completion.

    Tab completes the identifier under the cursor against the namespace names,
    Python keywords, and builtins: a unique match is inserted, multiple matches
    extend to the common prefix and drop down a popup. (No pop-as-you-type — that
    is noisy while entering code.)
    """

    def __init__(self, parent, words: list[str]) -> None:
        super().__init__(parent)
        self._words = words
        self._comp = QCompleter([], self)
        self._comp.setCaseSensitivity(Qt.CaseSensitivity.CaseSensitive)
        self._comp.setCompletionMode(QCompleter.CompletionMode.PopupCompletion)
        self._comp.setWidget(self)
        self._comp.activated.connect(self._insert)

    def _token(self) -> tuple[str, int, int]:
        text, cur = self.text(), self.cursorPosition()
        start = cur
        while start > 0 and (text[start - 1].isalnum() or text[start - 1] == "_"):
            start -= 1
        return text[start:cur], start, cur

    def _insert(self, word: str) -> None:
        text, (_, start, cur) = self.text(), self._token()
        self.setText(text[:start] + word + text[cur:])
        self.setCursorPosition(start + len(word))

    def _tab_complete(self) -> None:
        from ...core.completion import common_prefix

        token, start, cur = self._token()
        if not token:
            return
        cands = [w for w in self._words if w.startswith(token)]
        if not cands:
            return
        if len(cands) == 1:
            self._insert(cands[0])
            return
        pre = common_prefix(cands)
        if len(pre) > len(token):
            self.setText(self.text()[:start] + pre + self.text()[cur:])
            self.setCursorPosition(start + len(pre))
        self._comp.setModel(QStringListModel(cands, self._comp))
        self._comp.setCompletionPrefix(self._token()[0])
        self._comp.complete()

    def event(self, e):  # noqa: N802 (Qt override) — catch Tab before focus change
        if e.type() == QEvent.Type.KeyPress and e.key() == Qt.Key.Key_Tab:
            self._tab_complete()
            return True
        return super().event(e)

_BANNER = (
    "abax Python console (sandboxed — runs in a separate process, off the UI thread).\n"
    "Namespace: doc, wb, sheet(), cell(ref), put(ref, val), refresh(), rpn; "
    "engineering: matrix, eigen, units, numeric, complexnum, fft, interp, signal, "
    "spectral, filters, ode, ode_implicit, resynth, stats, cluster, ml, trees, "
    "bayes, metrics, gmm, compile_expr, read_matrix(rng), write_matrix(cell, mat), "
    "sheet_to_df(rng), df_to_sheet(df, cell); data science (if installed): "
    "np/numpy, pd/pandas, scipy, sm/statsmodels, sklearn, pg/pingouin, pm/pymc, "
    "sksurv.\n(The first command starts the sandbox; later commands reuse it. "
    "Use Interrupt to stop a runaway command.)\n>>> "
)


class PyConsole(QDialog):
    def __init__(self, window) -> None:
        super().__init__(window)
        self._win = window
        self.setWindowTitle("Python console (sandboxed)")
        self.resize(620, 420)
        self.setModal(False)
        from .console_bridge import ConsoleBridge

        self._bridge = ConsoleBridge()
        self._thread = None
        self._worker = None
        self._closing = False
        self._build()

    def _build(self) -> None:
        layout = QVBoxLayout(self)
        mono = QFont("monospace")
        mono.setStyleHint(QFont.StyleHint.TypeWriter)
        self._out = QPlainTextEdit(self)
        self._out.setReadOnly(True)
        self._out.setFont(mono)
        self._out.setPlainText(_BANNER)
        layout.addWidget(self._out)
        row = QHBoxLayout()
        self._in = _ConsoleInput(self, _console_words())
        self._in.setFont(mono)
        self._in.returnPressed.connect(self._run)
        row.addWidget(self._in, 1)
        self._interrupt_btn = QPushButton("Interrupt", self)
        self._interrupt_btn.setEnabled(False)
        self._interrupt_btn.clicked.connect(self._interrupt)
        row.addWidget(self._interrupt_btn)
        layout.addLayout(row)

    # --- async run -------------------------------------------------------

    def _run(self) -> None:
        if self._thread is not None:          # a command is already running
            return
        src = self._in.text()
        self._in.clear()
        self._out.appendPlainText(src)
        envelope = self._win._doc.workbook.to_envelope()
        self._set_running(True)

        from .._qtcompat import QThread
        from ...workers import FuncWorker

        self._thread = QThread(self)
        self._worker = FuncWorker(lambda s=src, e=envelope: self._bridge.execute(s, e))
        self._worker.moveToThread(self._thread)
        self._thread.started.connect(self._worker.run)
        self._worker.result.connect(self._on_result)
        self._worker.error.connect(self._on_error)
        self._worker.finished.connect(self._thread.quit)
        # Drop refs on the THREAD's finished (not the worker's) — the canonical
        # wiring that avoids "QThread destroyed while still running".
        self._thread.finished.connect(self._teardown_thread)
        self._thread.start()

    def _on_result(self, resp) -> None:
        if self._closing:
            return
        out = resp.get("output", "")
        if out:
            self._out.appendPlainText(out.rstrip("\n"))
        if resp.get("crashed"):
            reason = resp.get("stderr") or ""
            self._out.appendPlainText(
                "[console process exited — it will restart on the next command]"
                + (f"\n{reason}" if reason else ""))
        else:
            try:
                self._win._doc.workbook.load_envelope(resp["envelope"])
                self._win._doc.mark_dirty()
                self._win.refresh_table()
            except Exception as exc:  # pragma: no cover - defensive
                self._out.appendPlainText(f"[could not apply workbook changes: {exc}]")

    def _on_error(self, msg) -> None:
        if not self._closing:
            self._out.appendPlainText(f"[console error: {msg}]")

    def _teardown_thread(self) -> None:
        if self._worker is not None:
            self._worker.deleteLater()
            self._worker = None
        if self._thread is not None:
            self._thread.deleteLater()
            self._thread = None
        if not self._closing:
            self._set_running(False)
            self._out.appendPlainText(">>> ")

    def _set_running(self, running: bool) -> None:
        self._in.setEnabled(not running)
        self._interrupt_btn.setEnabled(running)
        if not running:
            self._in.setFocus()

    def _interrupt(self) -> None:
        self._out.appendPlainText("[interrupting...]")
        self._bridge.interrupt()              # kills the worker -> execute returns crashed

    # --- teardown --------------------------------------------------------

    def _shutdown(self) -> None:
        """Stop any running command and tear down the worker thread + subprocess."""
        self._closing = True
        self._bridge.interrupt()
        th = self._thread
        if th is not None:
            th.quit()
            th.wait(3000)
        self._bridge.close()

    def closeEvent(self, event) -> None:  # noqa: N802 (Qt override)
        self._shutdown()
        super().closeEvent(event)
