from __future__ import annotations

import math
from collections.abc import Callable
from pathlib import Path

import msgspec

from grim.assets import RuntimeResources, TextureId, runtime_resources_for
from grim.config import CrimsonConfig
from grim.fonts.small import SmallFontData, draw_small_text, measure_small_text_width
from grim.geom import Rect, Vec2
from grim.rand import Crand, CrandLike
from grim.raylib_api import rl
from grim.sfx_map import SfxId

from ...game_modes import GameMode
from ...persistence.highscores import (
    NAME_MAX_EDIT,
    TABLE_MAX,
    HighScoreRecord,
    rank_index,
    read_highscore_table,
    scores_path_for_mode,
    upsert_highscore_record,
)
from ...quests.level import QuestLevel
from ...quests.results import QuestFinalTime, QuestResultsBreakdownAnim, tick_quest_results_breakdown_anim
from ...ui.cursor import draw_menu_cursor
from ...ui.formatting import format_ordinal, format_time_mm_ss
from ...ui.layout import menu_widescreen_y_shift, ui_scale
from ...ui.menu_panel import draw_classic_menu_panel
from ...ui.perk_menu import (
    UiButtonState,
    button_draw,
    button_update,
    button_width,
    draw_ui_text,
)
from ...ui.text_input import flush_text_input_events, gameplay_controls_held, update_name_entry_text
from ...weapons import WEAPON_BY_ID, WeaponId, weapon_display_name

# `quest_results_screen_update` base layout (Crimsonland classic UI panel).
# Values are derived from `ui_menu_assets_init` + `ui_menu_layout_init` and how
# the quest results screen composes `ui_menuPanel` geometry:
#   panel_left = geom_x0 + pos_x + slide_x
#   panel_top  = geom_y0 + pos_y
#
# Where:
# - pos_x/pos_y are `ui_element_t` position fields set to (-45, 110)
# - geom_x0/geom_y0 are the first vertex coordinates of the `ui_menuPanel` geo,
#   after `ui_menu_assets_init` transforms it into an 8-vertex 3-slice panel.
QUEST_RESULTS_PANEL_POS_X = -45.0
QUEST_RESULTS_PANEL_POS_Y = 110.0
QUEST_RESULTS_PANEL_GEOM_X0 = -63.0
QUEST_RESULTS_PANEL_GEOM_Y0 = -81.0

QUEST_RESULTS_PANEL_W = 510.0
QUEST_RESULTS_PANEL_H = 378.0

TEXTURE_TOP_BANNER_W = 256.0
TEXTURE_TOP_BANNER_H = 64.0

# `quest_results_screen_update` uses the classic UI element sums for positioning:
#   content_x = (pos_x + offset_x + slide_x) + 180.0 + 40.0
#   banner_x  = content_x - 18.0
#   score_x   = content_x + 30.0
QUEST_RESULTS_CONTENT_X = 220.0
QUEST_RESULTS_BANNER_X_FROM_CONTENT = -18.0
QUEST_RESULTS_SCORE_CARD_X_FROM_CONTENT = 30.0

INPUT_BOX_W = 166.0
INPUT_BOX_H = 18.0

# Capture (1024x768) shows the quest results panel uses the same ui_element
# timeline pattern as other screens: fully hidden until 100ms, then slides in
# over 300ms (end=100, start=400).
PANEL_SLIDE_START_MS = 400.0
PANEL_SLIDE_END_MS = 100.0

COLOR_TEXT = rl.Color(255, 255, 255, 255)
COLOR_TEXT_MUTED = rl.Color(255, 255, 255, int(255 * 0.8))
COLOR_TEXT_SUBTLE = rl.Color(255, 255, 255, int(255 * 0.7))
COLOR_GREEN = rl.Color(25, 200, 25, 255)
# `sub_41e070` initializes DAT_004965f8..600 to this blue tint (149,175,198),
# reused by quest/game-over captions and score-card separator outlines.
COLOR_UI_ACCENT = rl.Color(149, 175, 198, 255)


class _QuestResultsPanelLayout(msgspec.Struct, frozen=True):
    panel: Rect
    top_left: Vec2


def _weapon_icon_src(texture: rl.Texture, weapon_id_native: int) -> rl.Rectangle | None:
    weapon_id = int(weapon_id_native)
    icon_index = WEAPON_BY_ID[WeaponId(weapon_id)].icon_index
    if icon_index < 0 or icon_index > 31:
        return None
    grid = 8
    cell_w = float(texture.width) / grid
    cell_h = float(texture.height) / grid
    frame = int(icon_index) * 2
    col = frame % grid
    row = frame // grid
    return rl.Rectangle(float(col * cell_w), float(row * cell_h), float(cell_w * 2), float(cell_h))


class QuestResultsUi(msgspec.Struct):
    assets_root: Path
    base_dir: Path
    config: CrimsonConfig
    preserve_bugs: bool = False

    phase: int = -1  # -1 init, 0 breakdown, 1 name entry (if qualifies), 2 results/buttons
    rank: int = TABLE_MAX
    highlight_rank: int | None = None

    quest_level: QuestLevel | None = None
    quest_title: str = ""
    unlock_weapon_name: str = ""
    unlock_perk_name: str = ""

    record: HighScoreRecord | None = None
    breakdown: QuestFinalTime | None = None
    _breakdown_anim: QuestResultsBreakdownAnim | None = None
    _scores_path: Path | None = None

    input_text: str = ""
    input_caret: int = 0
    _saved: bool = False

    _intro_ms: float = 0.0
    _cursor_pulse_time: float = 0.0
    _panel_open_sfx_played: bool = False
    _closing: bool = False
    _close_action: str | None = None
    _consume_enter: bool = False
    _defer_name_input_until_controls_released: bool = False

    _ok_button: UiButtonState = msgspec.field(default_factory=lambda: UiButtonState("OK", force_wide=False))
    _play_next_button: UiButtonState = msgspec.field(
        default_factory=lambda: UiButtonState("Play Next", force_wide=True),
    )
    _play_again_button: UiButtonState = msgspec.field(
        default_factory=lambda: UiButtonState("Play Again", force_wide=True),
    )
    _high_scores_button: UiButtonState = msgspec.field(
        default_factory=lambda: UiButtonState("High scores", force_wide=True),
    )
    _main_menu_button: UiButtonState = msgspec.field(
        default_factory=lambda: UiButtonState("Main Menu", force_wide=True),
    )

    def open(
        self,
        *,
        record: HighScoreRecord,
        breakdown: QuestFinalTime,
        quest_level: QuestLevel,
        quest_title: str,
        unlock_weapon_name: str,
        unlock_perk_name: str,
        player_name_default: str,
    ) -> None:
        self.close()
        self.phase = -1
        self.rank = TABLE_MAX
        self.highlight_rank = None
        self.quest_level = quest_level
        self.quest_title = str(quest_title or "")
        self.unlock_weapon_name = str(unlock_weapon_name or "")
        self.unlock_perk_name = str(unlock_perk_name or "")
        self.record = record.copy()
        self.breakdown = breakdown
        self._breakdown_anim = QuestResultsBreakdownAnim.start()
        self._saved = False

        # Native behavior: the final quest replaces "Play Next" with "Show End Note".
        if self.quest_level == QuestLevel(5, 10):
            self._play_next_button.label = "Show End Note"
        else:
            self._play_next_button.label = "Play Next"

        assert self.quest_level is not None, "quest results require quest level"
        hardcore = self.config.gameplay.hardcore
        self._scores_path = scores_path_for_mode(
            self.base_dir,
            GameMode.QUESTS,
            hardcore=hardcore,
            quest_stage_major=int(self.quest_level.major),
            quest_stage_minor=int(self.quest_level.minor),
            player_count=self.config.gameplay.player_count,
        )

        try:
            records = read_highscore_table(self._scores_path, game_mode_id=GameMode.QUESTS)
            self.rank = int(rank_index(records, self.record))
        except (OSError, ValueError):
            self.rank = TABLE_MAX

        self.input_text = str(player_name_default or "")[:NAME_MAX_EDIT]
        self.input_caret = len(self.input_text)

        self._intro_ms = 0.0
        self._cursor_pulse_time = 0.0
        self._panel_open_sfx_played = False
        self._closing = False
        self._close_action = None
        self._consume_enter = True
        self._defer_name_input_until_controls_released = False
        self.phase = 0

    def close(self) -> None:
        return None

    def _begin_close_transition(self, action: str) -> None:
        if self._closing:
            return
        self._closing = True
        self._close_action = action

    def _arm_name_input_after_control_release(self) -> None:
        self._defer_name_input_until_controls_released = True
        flush_text_input_events()
        rl.is_key_pressed(rl.KeyboardKey.KEY_ENTER)
        rl.is_key_pressed(rl.KeyboardKey.KEY_KP_ENTER)

    def world_entity_alpha(self) -> float:
        if not self._closing:
            return 1.0
        t_ms = float(self._intro_ms)
        if t_ms <= PANEL_SLIDE_END_MS:
            return 0.0
        if t_ms >= PANEL_SLIDE_START_MS:
            return 1.0
        span = float(PANEL_SLIDE_START_MS - PANEL_SLIDE_END_MS)
        if span <= 1e-6:
            return 1.0
        alpha = (t_ms - PANEL_SLIDE_END_MS) / span
        if alpha < 0.0:
            return 0.0
        if alpha > 1.0:
            return 1.0
        return alpha

    def _text_width(self, font: SmallFontData, text: str, scale: float) -> float:
        del scale
        return float(measure_small_text_width(font, text))

    def _draw_small(self, font: SmallFontData, text: str, pos: Vec2, scale: float, color: rl.Color) -> None:
        del scale
        draw_small_text(font, text, pos, color)

    def _draw_name_entry_stats(
        self,
        *,
        pos: Vec2,
        scale: float,
        alpha: float,
        show_weapon_row: bool,
        resources: RuntimeResources,
        font: SmallFontData,
    ) -> None:
        if self.record is None:
            return
        record = self.record
        qualifies = int(self.rank) < TABLE_MAX
        rank_text = format_ordinal(int(self.rank) + 1) if qualifies else "--"
        x = pos.x
        y = pos.y

        seconds = float(int(record.survival_elapsed_ms)) * 0.001
        score_value = f"{seconds:.2f} secs"
        xp_value = f"{int(record.score_xp)}"

        alpha_f = max(0.0, min(1.0, float(alpha)))
        col_label = rl.Color(230, 230, 230, int(255 * alpha_f * 0.8))
        col_score_value = rl.Color(230, 230, 255, int(255 * alpha_f))
        col_row = rl.Color(230, 230, 230, int(255 * alpha_f * 0.7))
        col_line = rl.Color(COLOR_UI_ACCENT.r, COLOR_UI_ACCENT.g, COLOR_UI_ACCENT.b, int(255 * alpha_f * 0.7))
        icon_tint = rl.Color(255, 255, 255, int(255 * alpha_f))

        left_center_x = x + 36.0 * scale
        right_label_x = x + 100.0 * scale
        right_center_x = right_label_x + 32.0 * scale

        score_w = self._text_width(font, "Score", 1.0 * scale)
        self._draw_small(font, "Score", Vec2(left_center_x - score_w * 0.5, y), 1.0 * scale, col_label)
        score_value_w = self._text_width(font, score_value, 1.0 * scale)
        self._draw_small(
            font,
            score_value,
            Vec2(left_center_x - score_value_w * 0.5, y + 15.0 * scale),
            1.0 * scale,
            col_score_value,
        )
        rank_label = f"Rank: {rank_text}"
        rank_w = self._text_width(font, rank_label, 1.0 * scale)
        self._draw_small(
            font,
            rank_label,
            Vec2(left_center_x - rank_w * 0.5, y + 30.0 * scale),
            1.0 * scale,
            col_label,
        )

        # Native path: FUN_00441220 sets current color from DAT_004ccca8 just before
        # drawing "Experience", so it uses the accent-blue tint (alpha*0.7).
        self._draw_small(font, "Experience", Vec2(right_label_x, y), 1.0 * scale, col_line)
        xp_value_w = self._text_width(font, xp_value, 1.0 * scale)
        self._draw_small(
            font,
            xp_value,
            Vec2(right_center_x - xp_value_w * 0.5, y + 15.0 * scale),
            1.0 * scale,
            col_label,
        )

        # Native vertical separator drawn via FUN_00441220 from x+84, height 48.
        sep_x = x + 84.0 * scale
        rl.draw_line(int(sep_x), int(y), int(sep_x), int(y + 48.0 * scale), col_line)

        row_top = y + 52.0 * scale
        rl.draw_line(int(x - 12.0 * scale), int(row_top), int(x + 180.0 * scale), int(row_top), col_line)
        if not show_weapon_row:
            return

        row_y = row_top
        wicons = resources.texture(TextureId.UI_WICONS)
        src = _weapon_icon_src(wicons, record.most_used_weapon_id)
        if src is not None:
            dst = rl.Rectangle(x + 4.0 * scale, row_y, 64.0 * scale, 32.0 * scale)
            rl.draw_texture_pro(wicons, src, dst, rl.Vector2(0.0, 0.0), 0.0, icon_tint)

        weapon_id = record.most_used_weapon_id
        weapon_name = weapon_display_name(weapon_id, preserve_bugs=bool(self.preserve_bugs))
        name_w = self._text_width(font, weapon_name, 1.0 * scale)
        name_x = max(x + 4.0 * scale, left_center_x - name_w * 0.5)
        self._draw_small(font, weapon_name, Vec2(name_x, row_y + 32.0 * scale), 1.0 * scale, col_row)

        frags_text = f"Frags: {int(record.creature_kill_count)}"
        self._draw_small(font, frags_text, Vec2(x + 114.0 * scale, row_y + 1.0 * scale), 1.0 * scale, col_row)

        fired = max(0, int(record.shots_fired))
        hit = max(0, min(int(record.shots_hit), fired))
        ratio = int((hit * 100) / fired) if fired > 0 else 0
        hit_text = f"Hit %: {ratio}%"
        self._draw_small(font, hit_text, Vec2(x + 114.0 * scale, row_y + 15.0 * scale), 1.0 * scale, col_row)

        rl.draw_line(
            int(x - 12.0 * scale),
            int(row_y + 48.0 * scale),
            int(x + 180.0 * scale),
            int(row_y + 48.0 * scale),
            col_line,
        )

    def _panel_layout(self, *, screen_w: float, scale: float) -> _QuestResultsPanelLayout:
        # Match MenuView._ui_element_anim offset math (linear, with a 100ms hold hidden).
        t_ms = float(self._intro_ms)
        if t_ms < PANEL_SLIDE_END_MS:
            panel_slide_x = -QUEST_RESULTS_PANEL_W
        elif t_ms < PANEL_SLIDE_START_MS:
            span = float(PANEL_SLIDE_START_MS - PANEL_SLIDE_END_MS)
            p = (t_ms - PANEL_SLIDE_END_MS) / span if span > 1e-6 else 1.0
            panel_slide_x = -((1.0 - p) * QUEST_RESULTS_PANEL_W)
        else:
            panel_slide_x = 0.0

        panel_pos = Vec2((QUEST_RESULTS_PANEL_GEOM_X0 + QUEST_RESULTS_PANEL_POS_X + panel_slide_x) * scale, 0.0)
        layout_w = screen_w / scale if scale else screen_w
        widescreen_shift_y = menu_widescreen_y_shift(layout_w)
        panel_pos = Vec2(
            panel_pos.x,
            (QUEST_RESULTS_PANEL_GEOM_Y0 + QUEST_RESULTS_PANEL_POS_Y + widescreen_shift_y) * scale,
        )
        panel = Rect.from_top_left(panel_pos, QUEST_RESULTS_PANEL_W * scale, QUEST_RESULTS_PANEL_H * scale)
        return _QuestResultsPanelLayout(panel=panel, top_left=panel_pos)

    def update(
        self,
        dt: float,
        *,
        play_sfx: Callable[[SfxId], None] | None = None,
        rng: CrandLike | None = None,
        mouse: rl.Vector2 | None = None,
    ) -> str | None:
        dt_s = float(min(dt, 0.1))
        dt_ms = dt_s * 1000.0
        self._cursor_pulse_time += dt_s * 1.1
        if mouse is None:
            mouse = rl.get_mouse_position()
        if rng is None:
            rng = Crand(0)

        if self.record is None or self.breakdown is None:
            return None

        if self._closing:
            self._intro_ms = max(0.0, float(self._intro_ms) - dt_ms)
            if self._intro_ms <= 1e-3 and self._close_action is not None:
                action = self._close_action
                self._close_action = None
                self._closing = False
                return action
            return None

        self._intro_ms = min(PANEL_SLIDE_START_MS, self._intro_ms + dt_ms)
        if (not self._panel_open_sfx_played) and play_sfx is not None and self._intro_ms >= PANEL_SLIDE_START_MS - 1e-3:
            play_sfx(SfxId.UI_PANELCLICK)
            self._panel_open_sfx_played = True
        if self._consume_enter:
            self._consume_enter = False
            rl.is_key_pressed(rl.KeyboardKey.KEY_ENTER)

        if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
            if play_sfx is not None:
                play_sfx(SfxId.UI_BUTTONCLICK)
            self._begin_close_transition("main_menu")
            return None

        qualifies = int(self.rank) < TABLE_MAX

        if self.phase == 0:
            anim = self._breakdown_anim
            if anim is None:
                self._breakdown_anim = QuestResultsBreakdownAnim.start()
                anim = self._breakdown_anim

            click = rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT)
            if rl.is_key_pressed(rl.KeyboardKey.KEY_SPACE) or click:
                anim.set_final(self.breakdown)
                if qualifies:
                    self.phase = 1
                    self._arm_name_input_after_control_release()
                else:
                    self.phase = 2
                return None

            clinks = tick_quest_results_breakdown_anim(
                anim,
                frame_dt_ms=int(dt_s * 1000.0),
                target=self.breakdown,
            )
            if clinks > 0 and play_sfx is not None:
                play_sfx(SfxId.UI_CLINK_01)
            if anim.done:
                if qualifies:
                    self.phase = 1
                    self._arm_name_input_after_control_release()
                else:
                    self.phase = 2
            return None

        if self.phase == 1:
            if self._defer_name_input_until_controls_released:
                flush_text_input_events()
                rl.is_key_pressed(rl.KeyboardKey.KEY_ENTER)
                rl.is_key_pressed(rl.KeyboardKey.KEY_KP_ENTER)
                if not gameplay_controls_held(self.config):
                    self._defer_name_input_until_controls_released = False
                return None
            click = rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT)
            self.input_text, self.input_caret = update_name_entry_text(
                self.input_text,
                self.input_caret,
                max_len=NAME_MAX_EDIT,
                rng=rng,
                play_sfx=play_sfx,
            )

            screen_w = float(rl.get_screen_width())
            screen_h = float(rl.get_screen_height())
            scale = ui_scale(screen_w, screen_h)
            panel_layout = self._panel_layout(screen_w=screen_w, scale=scale)
            content_pos = panel_layout.top_left.offset(dx=QUEST_RESULTS_CONTENT_X * scale)
            input_pos = content_pos.offset(dy=150.0 * scale)
            ok_pos = input_pos + Vec2(170.0 * scale, -8.0 * scale)
            resources = runtime_resources_for(self.assets_root)
            ok_w = button_width(resources, self._ok_button.label, scale=scale, force_wide=self._ok_button.force_wide)
            ok_clicked = button_update(self._ok_button, pos=ok_pos, width=ok_w, dt_ms=dt_ms, mouse=mouse, click=click)

            if ok_clicked or rl.is_key_pressed(rl.KeyboardKey.KEY_ENTER):
                if self.input_text.strip():
                    if play_sfx is not None:
                        play_sfx(SfxId.UI_TYPEENTER)
                    if (not self._saved) and self._scores_path is not None:
                        candidate = self.record.copy()
                        candidate.set_name(self.input_text)
                        try:
                            _table, idx = upsert_highscore_record(self._scores_path, candidate)
                            self.highlight_rank = int(idx) if int(idx) < TABLE_MAX else None
                            if int(idx) < TABLE_MAX:
                                self.rank = int(idx)
                        except (OSError, ValueError):
                            self.highlight_rank = None
                        self._saved = True
                    self.config.profile.set_player_name_input(self.input_text)
                    self.config.save()
                    self.phase = 2
                    return None
                if play_sfx is not None:
                    play_sfx(SfxId.SHOCK_HIT_01)
            return None

        if self.phase == 2:
            click = rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT)
            if rl.is_key_pressed(rl.KeyboardKey.KEY_ENTER):
                if play_sfx is not None:
                    play_sfx(SfxId.UI_BUTTONCLICK)
                self._begin_close_transition("play_again")
                return None
            if rl.is_key_pressed(rl.KeyboardKey.KEY_N):
                if play_sfx is not None:
                    play_sfx(SfxId.UI_BUTTONCLICK)
                self._begin_close_transition("play_next")
                return None
            if rl.is_key_pressed(rl.KeyboardKey.KEY_H):
                if play_sfx is not None:
                    play_sfx(SfxId.UI_BUTTONCLICK)
                self._begin_close_transition("high_scores")
                return None

            screen_w = float(rl.get_screen_width())
            screen_h = float(rl.get_screen_height())
            scale = ui_scale(screen_w, screen_h)
            panel_layout = self._panel_layout(screen_w=screen_w, scale=scale)
            qualifies = int(self.rank) < TABLE_MAX
            content_pos = panel_layout.top_left.offset(dx=QUEST_RESULTS_CONTENT_X * scale)
            score_card_pos = content_pos.offset(dx=QUEST_RESULTS_SCORE_CARD_X_FROM_CONTENT * scale)

            var_c_12 = panel_layout.top_left.y + (96.0 if qualifies else 108.0) * scale
            var_c_14 = var_c_12 + 84.0 * scale
            if self.unlock_weapon_name:
                var_c_14 += 30.0 * scale
            if self.unlock_perk_name:
                var_c_14 += 30.0 * scale

            button_pos = Vec2(score_card_pos.x + 20.0 * scale, var_c_14 + 6.0 * scale)
            resources = runtime_resources_for(self.assets_root)

            play_next_w = button_width(
                resources,
                self._play_next_button.label,
                scale=scale,
                force_wide=self._play_next_button.force_wide,
            )
            if button_update(
                self._play_next_button,
                pos=button_pos,
                width=play_next_w,
                dt_ms=dt_ms,
                mouse=mouse,
                click=click,
            ):
                if play_sfx is not None:
                    play_sfx(SfxId.UI_BUTTONCLICK)
                self._begin_close_transition("play_next")
                return None
            button_pos = button_pos.offset(dy=32.0 * scale)

            play_again_w = button_width(
                resources,
                self._play_again_button.label,
                scale=scale,
                force_wide=self._play_again_button.force_wide,
            )
            if button_update(
                self._play_again_button,
                pos=button_pos,
                width=play_again_w,
                dt_ms=dt_ms,
                mouse=mouse,
                click=click,
            ):
                if play_sfx is not None:
                    play_sfx(SfxId.UI_BUTTONCLICK)
                self._begin_close_transition("play_again")
                return None
            button_pos = button_pos.offset(dy=32.0 * scale)

            high_scores_w = button_width(
                resources,
                self._high_scores_button.label,
                scale=scale,
                force_wide=self._high_scores_button.force_wide,
            )
            if button_update(
                self._high_scores_button,
                pos=button_pos,
                width=high_scores_w,
                dt_ms=dt_ms,
                mouse=mouse,
                click=click,
            ):
                if play_sfx is not None:
                    play_sfx(SfxId.UI_BUTTONCLICK)
                self._begin_close_transition("high_scores")
                return None
            button_pos = button_pos.offset(dy=32.0 * scale)

            main_menu_w = button_width(
                resources,
                self._main_menu_button.label,
                scale=scale,
                force_wide=self._main_menu_button.force_wide,
            )
            if button_update(
                self._main_menu_button,
                pos=button_pos,
                width=main_menu_w,
                dt_ms=dt_ms,
                mouse=mouse,
                click=click,
            ):
                if play_sfx is not None:
                    play_sfx(SfxId.UI_BUTTONCLICK)
                self._begin_close_transition("main_menu")
                return None
            return None

        return None

    def draw(self, *, mouse: rl.Vector2 | None = None) -> None:
        if self.record is None or self.breakdown is None:
            return
        if mouse is None:
            mouse = rl.get_mouse_position()

        screen_w = float(rl.get_screen_width())
        screen_h = float(rl.get_screen_height())
        scale = ui_scale(screen_w, screen_h)

        resources = runtime_resources_for(self.assets_root)
        font = resources.small_font
        panel_layout = self._panel_layout(screen_w=screen_w, scale=scale)
        panel = panel_layout.panel

        fx_detail = self.config.display.fx_detail_enabled(level=0, default=False)
        draw_classic_menu_panel(
            resources.texture(TextureId.UI_MENU_PANEL),
            dst=panel.to_rl(),
            tint=rl.WHITE,
            shadow=fx_detail,
        )

        content_pos = panel_layout.top_left.offset(dx=QUEST_RESULTS_CONTENT_X * scale)
        banner_pos = content_pos + Vec2(QUEST_RESULTS_BANNER_X_FROM_CONTENT * scale, 36.0 * scale)
        text_well_done = resources.texture(TextureId.UI_TEXT_WELL_DONE)
        src = rl.Rectangle(0.0, 0.0, float(text_well_done.width), float(text_well_done.height))
        dst = rl.Rectangle(banner_pos.x, banner_pos.y, TEXTURE_TOP_BANNER_W * scale, TEXTURE_TOP_BANNER_H * scale)
        rl.draw_texture_pro(text_well_done, src, dst, rl.Vector2(0.0, 0.0), 0.0, rl.WHITE)

        qualifies = int(self.rank) < TABLE_MAX

        if self.phase == 0:
            label_x = content_pos.x + 32.0 * scale
            value_x = label_x + 132.0 * scale

            anim = self._breakdown_anim
            step = 4
            highlight_alpha = 1.0
            base_time_ms = int(self.breakdown.base_time_ms)
            life_bonus_ms = int(self.breakdown.life_bonus_ms)
            perk_bonus_ms = int(self.breakdown.unpicked_perk_bonus_ms)
            final_time_ms = int(self.breakdown.final_time_ms)
            if anim is not None and not anim.done:
                step = int(anim.step)
                highlight_alpha = float(anim.highlight_alpha())
                base_time_ms = int(anim.base_time_ms)
                life_bonus_ms = int(anim.life_bonus_ms)
                perk_bonus_ms = int(anim.unpicked_perk_bonus_s) * 1000
                final_time_ms = int(anim.final_time_ms)

            def _row_color(idx: int, *, final: bool = False) -> rl.Color:
                if anim is None or anim.done:
                    return COLOR_TEXT
                alpha = 0.2
                if idx < step:
                    alpha = 0.4
                elif idx == step:
                    alpha = 1.0
                    if final:
                        alpha *= highlight_alpha
                rgb = (255, 255, 255)
                if idx == step:
                    rgb = (COLOR_GREEN.r, COLOR_GREEN.g, COLOR_GREEN.b)
                return rl.Color(rgb[0], rgb[1], rgb[2], int(255 * max(0.0, min(1.0, alpha))))

            y = panel_layout.top_left.y + 156.0 * scale
            base_value = format_time_mm_ss(base_time_ms)
            life_value = format_time_mm_ss(life_bonus_ms)
            perk_value = format_time_mm_ss(perk_bonus_ms)
            final_value = format_time_mm_ss(final_time_ms)

            self._draw_small(font, "Base Time:", Vec2(label_x, y), 1.0 * scale, _row_color(0))
            self._draw_small(font, base_value, Vec2(value_x, y), 1.0 * scale, _row_color(0))
            y += 20.0 * scale

            self._draw_small(font, "Life Bonus:", Vec2(label_x, y), 1.0 * scale, _row_color(1))
            self._draw_small(font, life_value, Vec2(value_x, y), 1.0 * scale, _row_color(1))
            y += 20.0 * scale

            self._draw_small(font, "Unpicked Perk Bonus:", Vec2(label_x, y), 1.0 * scale, _row_color(2))
            self._draw_small(font, perk_value, Vec2(value_x, y), 1.0 * scale, _row_color(2))
            y += 20.0 * scale

            # Final time underline + row (matches the extra quad draw in native).
            line_y = y + 1.0 * scale
            line_color = rl.Color(255, 255, 255, _row_color(3, final=True).a)
            rl.draw_rectangle(int(label_x - 4.0 * scale), int(line_y), int(168.0 * scale), int(1.0 * scale), line_color)

            y += 8.0 * scale
            self._draw_small(font, "Final Time:", Vec2(label_x, y), 1.0 * scale, _row_color(3, final=True))
            self._draw_small(font, final_value, Vec2(value_x, y), 1.0 * scale, _row_color(3, final=True))

        elif self.phase == 1:
            text_y = panel_layout.top_left.y + 118.0 * scale
            name_prompt = "State your name trooper!" if bool(self.preserve_bugs) else "State your name, trooper!"
            self._draw_small(
                font,
                name_prompt,
                Vec2(content_pos.x + 42.0 * scale, text_y),
                1.0 * scale,
                COLOR_UI_ACCENT,
            )

            input_pos = content_pos.offset(dy=150.0 * scale)
            rl.draw_rectangle_lines(
                int(input_pos.x),
                int(input_pos.y),
                int(INPUT_BOX_W * scale),
                int(INPUT_BOX_H * scale),
                rl.WHITE,
            )
            rl.draw_rectangle(
                int(input_pos.x + 1.0 * scale),
                int(input_pos.y + 1.0 * scale),
                int((INPUT_BOX_W - 2.0) * scale),
                int((INPUT_BOX_H - 2.0) * scale),
                rl.Color(0, 0, 0, 255),
            )
            draw_ui_text(
                resources,
                self.input_text,
                input_pos + Vec2(4.0 * scale, 2.0 * scale),
                scale=1.0 * scale,
                color=COLOR_TEXT_MUTED,
            )
            caret_alpha = 1.0
            if math.sin(float(rl.get_time()) * 4.0) > 0.0:
                caret_alpha = 0.4
            caret_color = rl.Color(255, 255, 255, int(255 * caret_alpha))
            caret_x = (
                input_pos.x + 4.0 * scale + self._text_width(font, self.input_text[: self.input_caret], 1.0 * scale)
            )
            rl.draw_rectangle(
                int(caret_x),
                int(input_pos.y + 2.0 * scale),
                int(1.0 * scale),
                int(14.0 * scale),
                caret_color,
            )

            ok_pos = input_pos + Vec2(170.0 * scale, -8.0 * scale)
            ok_w = button_width(resources, self._ok_button.label, scale=scale, force_wide=self._ok_button.force_wide)
            button_draw(resources, self._ok_button, pos=ok_pos, width=ok_w, scale=scale)

            # Native phase 1 still renders the quest score card while entering the name.
            score_card_pos = input_pos + Vec2(26.0 * scale, 46.0 * scale)
            self._draw_name_entry_stats(
                pos=score_card_pos,
                scale=scale,
                alpha=1.0,
                show_weapon_row=True,
                resources=resources,
                font=font,
            )

        else:
            score_card_pos = content_pos.offset(dx=QUEST_RESULTS_SCORE_CARD_X_FROM_CONTENT * scale)
            var_c_12 = panel_layout.top_left.y + (96.0 if qualifies else 108.0) * scale
            if not qualifies:
                self._draw_small(
                    font,
                    "Score too low for top100.",
                    Vec2(score_card_pos.x + 8.0 * scale, panel_layout.top_left.y + 102.0 * scale),
                    1.0 * scale,
                    rl.Color(200, 200, 200, 255),
                )

            card_y = var_c_12 + 16.0 * scale
            self._draw_name_entry_stats(
                pos=Vec2(score_card_pos.x, card_y),
                scale=scale,
                alpha=1.0,
                show_weapon_row=False,
                resources=resources,
                font=font,
            )

            # Unlock lines (their presence shifts the buttons down in native).
            var_c_14 = var_c_12 + 84.0 * scale
            if self.unlock_weapon_name:
                self._draw_small(
                    font,
                    "Weapon unlocked:",
                    Vec2(score_card_pos.x, var_c_14 + 1.0 * scale),
                    1.0 * scale,
                    COLOR_TEXT_SUBTLE,
                )
                self._draw_small(
                    font,
                    self.unlock_weapon_name,
                    Vec2(score_card_pos.x, var_c_14 + 14.0 * scale),
                    1.0 * scale,
                    COLOR_TEXT,
                )
                var_c_14 += 30.0 * scale
            if self.unlock_perk_name:
                self._draw_small(
                    font,
                    "Perk unlocked:",
                    Vec2(score_card_pos.x, var_c_14 + 1.0 * scale),
                    1.0 * scale,
                    COLOR_TEXT_SUBTLE,
                )
                self._draw_small(
                    font,
                    self.unlock_perk_name,
                    Vec2(score_card_pos.x, var_c_14 + 14.0 * scale),
                    1.0 * scale,
                    COLOR_TEXT,
                )
                var_c_14 += 30.0 * scale

            # Buttons
            button_pos = Vec2(score_card_pos.x + 20.0 * scale, var_c_14 + 6.0 * scale)
            play_next_w = button_width(
                resources,
                self._play_next_button.label,
                scale=scale,
                force_wide=self._play_next_button.force_wide,
            )
            button_draw(
                resources,
                self._play_next_button,
                pos=button_pos,
                width=play_next_w,
                scale=scale,
            )
            button_pos = button_pos.offset(dy=32.0 * scale)
            play_again_w = button_width(
                resources,
                self._play_again_button.label,
                scale=scale,
                force_wide=self._play_again_button.force_wide,
            )
            button_draw(
                resources,
                self._play_again_button,
                pos=button_pos,
                width=play_again_w,
                scale=scale,
            )
            button_pos = button_pos.offset(dy=32.0 * scale)
            high_scores_w = button_width(
                resources,
                self._high_scores_button.label,
                scale=scale,
                force_wide=self._high_scores_button.force_wide,
            )
            button_draw(
                resources,
                self._high_scores_button,
                pos=button_pos,
                width=high_scores_w,
                scale=scale,
            )
            button_pos = button_pos.offset(dy=32.0 * scale)
            main_menu_w = button_width(
                resources,
                self._main_menu_button.label,
                scale=scale,
                force_wide=self._main_menu_button.force_wide,
            )
            button_draw(
                resources,
                self._main_menu_button,
                pos=button_pos,
                width=main_menu_w,
                scale=scale,
            )

        draw_menu_cursor(
            resources.texture(TextureId.PARTICLES),
            resources.texture(TextureId.UI_CURSOR),
            pos=Vec2.from_xy(mouse),
            pulse_time=float(self._cursor_pulse_time),
        )
