"""AST-based expression introspection for soft_assert.

Extracts the source expression from a ``soft_assert(...)`` call,
substitutes runtime values, and returns a colorized string with
a diff (for strings and containers) similar to pytest assertion output.
"""

import ast
import difflib
import inspect
import os
import pprint
from pathlib import Path
from typing import Any


_SENTINEL = object()

_COMPARE_OPS = {
    ast.Eq: "==",
    ast.NotEq: "!=",
    ast.Lt: "<",
    ast.LtE: "<=",
    ast.Gt: ">",
    ast.GtE: ">=",
    ast.Is: "is",
    ast.IsNot: "is not",
    ast.In: "in",
    ast.NotIn: "not in",
}

_NO_COLOR = bool(
    os.environ.get("NO_COLOR")
    or os.environ.get("PROMIUM_ASSERTION_PANEL_MONO_COLOR")
)
_RED = "" if _NO_COLOR else "\033[31m"
_GREEN = "" if _NO_COLOR else "\033[32m"
_YELLOW = "" if _NO_COLOR else "\033[1;33m"
_BOLD = "" if _NO_COLOR else "\033[1m"
_WHITE = "" if _NO_COLOR else "\033[1;37m"
_RESET = "" if _NO_COLOR else "\033[0m"


def _escape_control(s: str) -> str:
    """Replace control characters with \\xNN escapes, keep Unicode as-is."""
    return "".join(
        f"\\x{ord(c):02x}" if ord(c) < 32 or ord(c) == 127 else c for c in s
    )


def _colorize_ndiff(left_lines: list[str], right_lines: list[str]) -> str:
    """Return colorized ndiff output for two sequences of lines."""
    colored = []
    for line in difflib.ndiff(left_lines, right_lines):
        stripped = line.rstrip("\n")
        if line.startswith("- "):
            colored.append(f"{_RED}{stripped}{_RESET}\n")
        elif line.startswith("+ "):
            colored.append(f"{_GREEN}{stripped}{_RESET}\n")
        elif line.startswith("? "):
            colored.append(f"{_YELLOW}{stripped}{_RESET}\n")
        else:
            colored.append(line)
    return "".join(colored)


def extract_call_expression(method_name: str) -> str | None:
    """Extract the first argument source from a call to *method_name*
    and rebuild it with runtime values substituted.

    Example: ``self.soft_assert(code in var)``
    where code='23', var=[1,2,3] → ``"'23' in [1, 2, 3]"``
    """
    try:
        caller = inspect.currentframe().f_back.f_back
        source = Path(caller.f_code.co_filename).read_text(encoding="utf-8")

        for node in ast.walk(ast.parse(source)):
            if not (
                isinstance(node, ast.Call)
                and isinstance(node.func, ast.Attribute)
                and node.func.attr == method_name
                and node.args
                and node.lineno
                <= caller.f_lineno
                <= (node.end_lineno or node.lineno)
            ):
                continue

            expr_node = node.args[0]
            expr_source = ast.get_source_segment(source, expr_node)
            if not expr_source:
                return None

            def _may_have_side_effect(node: ast.expr) -> bool:
                if isinstance(node, ast.Call):
                    return True
                if isinstance(node, ast.Attribute):
                    if _may_have_side_effect(node.value):
                        return True
                    val_src = ast.get_source_segment(source, node.value)
                    if val_src:
                        try:
                            obj = eval(
                                val_src, caller.f_globals, caller.f_locals
                            )  # noqa: S307
                            if isinstance(
                                getattr(type(obj), node.attr, None),
                                property,
                            ):
                                return True
                        except Exception:
                            pass
                    return False
                return any(
                    _may_have_side_effect(child)
                    for child in ast.iter_child_nodes(node)
                    if isinstance(child, ast.expr)
                )

            def _try_eval(operand: ast.expr) -> Any:
                """Return the runtime value or _SENTINEL if eval fails."""
                if _may_have_side_effect(operand):
                    return _SENTINEL
                src = ast.get_source_segment(source, operand)
                if not src:
                    return _SENTINEL
                try:
                    return eval(src, caller.f_globals, caller.f_locals)  # noqa: S307
                except Exception:
                    return _SENTINEL

            def _eval_operand(operand: ast.expr) -> str:
                """Return repr(value) if eval succeeds, otherwise source text."""
                val = _try_eval(operand)
                if val is _SENTINEL:
                    return ast.get_source_segment(source, operand) or "?"
                r = repr(val)
                return r if len(r) <= 200 else r[:200] + "..."

            if isinstance(expr_node, ast.Compare):
                left_repr = _eval_operand(expr_node.left)
                parts = [f"{_RED}{left_repr}{_RESET}"]
                for cmp_op, comp in zip(expr_node.ops, expr_node.comparators):
                    op_str = _COMPARE_OPS.get(type(cmp_op), "??")
                    parts.append(f"{_BOLD}{op_str}{_RESET}")
                    parts.append(f"{_GREEN}{_eval_operand(comp)}{_RESET}")
                result = " ".join(parts)

                if len(expr_node.ops) == 1 and isinstance(
                    expr_node.ops[0], (ast.Eq, ast.NotEq)
                ):
                    left_val = _try_eval(expr_node.left)
                    right_val = _try_eval(expr_node.comparators[0])
                    if isinstance(left_val, str) and isinstance(right_val, str):
                        esc = _escape_control
                        result += "\n" + _colorize_ndiff(
                            [esc(left_val) + "\n"],
                            [esc(right_val) + "\n"],
                        )
                    elif (
                        left_val is not _SENTINEL
                        and right_val is not _SENTINEL
                        and isinstance(
                            left_val,
                            (dict, list, tuple, set, frozenset),
                        )
                        and isinstance(
                            right_val,
                            (dict, list, tuple, set, frozenset),
                        )
                    ):
                        fmt = pprint.PrettyPrinter(
                            width=1, sort_dicts=False
                        ).pformat
                        left_lines = fmt(left_val).splitlines(keepends=True)
                        right_lines = fmt(right_val).splitlines(keepends=True)
                        if left_lines and not left_lines[-1].endswith("\n"):
                            left_lines[-1] += "\n"
                        if right_lines and not right_lines[-1].endswith("\n"):
                            right_lines[-1] += "\n"
                        result += "\n" + _colorize_ndiff(left_lines, right_lines)
                return result

            if isinstance(expr_node, ast.BoolOp):
                op_str = " and " if isinstance(expr_node.op, ast.And) else " or "
                colored_parts = []
                for v in expr_node.values:
                    val = _try_eval(v)
                    operand_str = _eval_operand(v)
                    if val is _SENTINEL:
                        colored_parts.append(operand_str)
                    elif val:
                        colored_parts.append(f"{_GREEN}{operand_str}{_RESET}")
                    else:
                        colored_parts.append(f"{_RED}{operand_str}{_RESET}")
                return (f"{_BOLD}{op_str}{_RESET}").join(colored_parts)

            val = _try_eval(expr_node)
            operand_str = _eval_operand(expr_node)
            if val is not _SENTINEL and not val:
                return f"{_RED}{operand_str}{_RESET}"
            return operand_str
    except Exception:
        pass
    return None
