# SPDX-License-Identifier: MIT OR Apache-2.0
# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the MIT License.  See the LICENSE file in the root of this
# repository for complete details.

from __future__ import annotations

import sys
import traceback

from io import StringIO
from types import FrameType
from typing import Callable

from .contextvars import _ASYNC_CALLING_STACK
from .typing import ExcInfo


def _format_exception(exc_info: ExcInfo) -> str:
    """
    Prettyprint an `exc_info` tuple.

    Shamelessly stolen from stdlib's logging module.
    """
    if exc_info == (None, None, None):  # type: ignore[comparison-overlap]
        return "MISSING"

    sio = StringIO()

    traceback.print_exception(exc_info[0], exc_info[1], exc_info[2], None, sio)
    s = sio.getvalue()
    sio.close()
    if s[-1:] == "\n":
        s = s[:-1]

    return s


def _find_first_app_frame_and_name(
    additional_ignores: list[str] | None = None,
    *,
    _getframe: Callable[[], FrameType] = sys._getframe,
) -> tuple[FrameType, str]:
    """
    Remove all intra-structlog calls and return the relevant app frame.

    Args:
        additional_ignores:
            Additional names with which the first frame must not start.

        _getframe:
            Callable to find current frame. Only for testing to avoid
            monkeypatching of sys._getframe.

    Returns:
        tuple of (frame, name)
    """
    ignores = tuple(["structlog"] + (additional_ignores or []))
    f = _ASYNC_CALLING_STACK.get(_getframe())
    name = f.f_globals.get("__name__") or "?"
    while name.startswith(ignores):
        if f.f_back is None:
            name = "?"
            break
        f = f.f_back
        name = f.f_globals.get("__name__") or "?"
    return f, name


def _format_stack(frame: FrameType) -> str:
    """
    Pretty-print the stack of *frame* like logging would.
    """
    sio = StringIO()

    sio.write("Stack (most recent call last):\n")
    traceback.print_stack(frame, file=sio)
    sinfo = sio.getvalue()
    if sinfo[-1] == "\n":
        sinfo = sinfo[:-1]
    sio.close()

    return sinfo
