"""Rebrand the brand word in opik's evaluation *console report* output.

opik's evaluation reporters print brand lines such as "Uploading results to
Opik ..." and "View results in Opik dashboard" through a ``rich.console.Console``
they instantiate internally — NOT through the ``logging`` system, so
``_log_rebrand`` cannot reach them. This module is the console-output analogue
of ``_log_rebrand`` for the ``evaluate()`` and evaluation-suite flows.

The brand reaches output in two different shapes AND via two different render
paths:

  - shapes: plain strings passed to ``Console.print`` (``opik.evaluation.report``)
    and ``rich.Text`` / ``Table`` / ``Panel`` renderables (the evaluation-suite
    displayer builds its link via ``Text.from_markup``).
  - paths: a terminal / plain ``.py`` process writes rendered text to
    ``Console.file``; a Jupyter / Colab kernel (``Console.is_jupyter`` is True)
    instead renders via ``rich.jupyter.display``, which builds the cell's
    text/html + text/plain from the SEGMENT buffer and never touches
    ``Console.file``. A file-only scrub therefore leaks the brand in notebooks
    while working in ``.py`` — the exact ``.py``-clean / ``.ipynb``-leaks bug.

The choke point shared by BOTH paths is the segment buffer rich assembles just
before flushing, so we scrub there (``_RebrandConsole._write_buffer``):
rewriting the brand word in each buffered ``Segment`` covers the terminal write,
the notebook HTML link, and its plain-text fallback alike — while keeping rich's
rendering (styled panels, clickable links) intact. As defense-in-depth for the
common terminal path we ALSO wrap ``Console.file``. We match the capitalised
brand case-sensitively, so a lowercase "opik" inside a ``[link=URL]`` target is
never touched.

The rebranding console is installed onto each reporter module's ``console``
reference, scoped to those modules (global rich output is untouched).

Idempotent — safe to call repeatedly.
"""

from __future__ import annotations

import re
from typing import IO, Any

from rich import console as _rich_console

# Mirror _log_rebrand's inline brand word ("AIO"). Case-sensitive on the
# capitalised brand so a lowercase "opik" inside a [link=URL] is left alone.
_BRAND_RE = re.compile(r"\bOpik\b")
_BRAND = "AIO"

_applied = False


class _ScrubbingFile:
    """Wrap a text stream, rewriting the brand word in everything written.
    Everything else (flush, isatty, fileno, ...) delegates to the wrapped
    stream so rich's terminal/width detection is unaffected."""

    def __init__(self, wrapped: IO[str]) -> None:
        self._wrapped = wrapped

    def write(self, s: Any) -> int:
        if isinstance(s, str):
            s = _BRAND_RE.sub(_BRAND, s)
        return self._wrapped.write(s)

    def __getattr__(self, name: str) -> Any:
        return getattr(self._wrapped, name)


class _RebrandConsole(_rich_console.Console):
    """A rich Console that rewrites the brand word in its output regardless of
    how the renderable was built (string, Text, Table, Panel, ...) OR which
    render path rich takes (terminal ``file.write`` vs Jupyter ``display``).

    Primary scrub — ``_write_buffer``: rewrite the segment buffer before rich
    flushes it; that buffer is the one choke point BOTH paths consume. Fallback
    scrub — the ``file`` property wraps the stream, still covering the common
    terminal path if a future rich reshapes ``_write_buffer``.
    """

    def _write_buffer(self) -> None:
        # Scrub the brand out of the buffered segments before rich flushes them.
        # Both render paths read this same buffer:
        #   terminal -> _render_buffer(self._buffer) -> self.file.write(...)
        #   Jupyter  -> rich.jupyter.display(self._buffer, ...) builds the cell
        #               HTML + plain-text from these segments, bypassing file.
        # Rewriting the segment text here covers the terminal write, the
        # notebook HTML link, and its text fallback alike — while keeping rich's
        # styling/links intact (we only touch each segment's text, not style).
        buf = self._buffer
        for i, segment in enumerate(buf):
            original = segment.text
            if original and _BRAND_RE.search(original):
                buf[i] = segment._replace(text=_BRAND_RE.sub(_BRAND, original))
        super()._write_buffer()

    @property
    def file(self) -> IO[str]:
        return _ScrubbingFile(_rich_console.Console.file.fget(self))  # type: ignore[union-attr]

    @file.setter
    def file(self, value: IO[str]) -> None:
        # rich's Console.__init__ stores the stream on self._file; mirror that.
        self._file = value


class _ConsoleModuleProxy:
    """Stand-in for the ``rich.console`` module as referenced inside a reporter
    module: hands back the rebranding Console and delegates every other
    attribute to the real ``rich.console`` module."""

    Console = _RebrandConsole

    def __getattr__(self, name: str) -> Any:
        return getattr(_rich_console, name)


def apply() -> None:
    """Rebrand opik's evaluation console-report output — the ``evaluate()``
    reporter and the evaluation-suite displayer. Idempotent."""
    global _applied
    if _applied:
        return

    proxy = _ConsoleModuleProxy()

    # Primary: the evaluate() reporter (the lines users see most).
    import opik.evaluation.report as _report

    _report.console = proxy

    # Best-effort: the evaluation-suite displayer lives deeper in opik and is a
    # niche path; if a future opik relocates it, don't break SDK import — the
    # tripwire test catches the relocation on a version bump.
    try:
        from opik.api_objects.dataset.evaluation_suite.report_processors import (
            displayer as _suite_displayer,
        )

        _suite_displayer.console = proxy
    except Exception:  # pragma: no cover - opik path moved
        pass

    _applied = True
