# request_tab_widget.py
#
# Copyright 2025 Pavel Baksy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later

import gi
gi.require_version('GtkSource', '5')
from gi.repository import Adw, Gtk, GLib, GtkSource, GObject
from typing import Optional, Set, Dict, List
import logging
from .models import HttpRequest, HttpResponse
from .widgets.header_row import HeaderRow
from .constants import (
    UI_PANE_REQUEST_RESPONSE_POSITION,
    UI_PANE_RESPONSE_DETAILS_POSITION,
    UI_PANE_RESULTS_PANEL_POSITION,
    UI_PANE_SCRIPTS_POSITION,
    UI_SCRIPT_RESULTS_PANEL_HEIGHT,
    DEBOUNCE_VARIABLE_INDICATORS_MS,
    DISPLAY_VARIABLE_MAX_LENGTH,
    DISPLAY_VARIABLE_TRUNCATE_SUFFIX,
)
import json
import xml.dom.minidom

logger = logging.getLogger(__name__)


class RequestTabWidget(Gtk.Box):
    """Widget representing a single request tab's UI."""

    def __init__(self, tab_id, request=None, response=None, project_id=None, selected_environment_id=None, scripts=None, **kwargs):
        super().__init__(orientation=Gtk.Orientation.VERTICAL, **kwargs)

        self.tab_id: str = tab_id
        self.request: HttpRequest = request or HttpRequest(method="GET", url="", headers=HttpRequest.default_headers(), body="", syntax="RAW")
        self.response: Optional[HttpResponse] = response
        self.modified: bool = False
        self.original_request: Optional[HttpRequest] = None
        self.original_scripts = None
        self.project_id: Optional[str] = project_id
        self.selected_environment_id: Optional[str] = selected_environment_id
        self.scripts = scripts
        self.project_manager = None  # Will be injected from window
        self.environment_dropdown: Optional[Gtk.DropDown] = None
        self.env_separator: Optional[Gtk.Separator] = None
        self.undefined_variables: Set[str] = set()
        self._update_indicators_timeout_id: Optional[int] = None
        self._is_programmatically_changing_environment: bool = False

        # Build the UI
        self._build_ui()

        # Load initial request
        if request:
            self._load_request(request)

        # Load initial scripts
        if scripts:
            self.load_scripts(scripts)

        # Setup change tracking
        self._setup_change_tracking()

    def _build_ui(self) -> None:
        """Build the complete UI for this tab."""

        # URL Input Section - outer container (store as instance var for dynamic updates)
        self.url_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0)
        self.url_container.set_margin_start(12)
        self.url_container.set_margin_end(12)
        self.url_container.set_margin_top(12)
        self.url_container.set_margin_bottom(12)

        # Environment Selector (left-aligned, outside clamp)
        if self.project_id:
            self._build_environment_selector_inline(self.url_container)
            # Add visual separator/spacer after environment
            self.env_separator = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL)
            self.env_separator.set_margin_start(12)
            self.env_separator.set_margin_end(12)
            self.url_container.append(self.env_separator)

        # URL bar (method, URL, send) - centered in clamp
        url_clamp = Adw.Clamp(maximum_size=1000)
        url_clamp.set_hexpand(True)
        self.url_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)

        # Method Dropdown
        self.method_dropdown = Gtk.DropDown()
        methods = Gtk.StringList()
        for method in ["GET", "POST", "PUT", "DELETE"]:
            methods.append(method)
        self.method_dropdown.set_model(methods)
        self.method_dropdown.set_selected(0)
        self.method_dropdown.set_enable_search(False)
        self.url_box.append(self.method_dropdown)

        # URL Entry
        self.url_entry = Gtk.Entry()
        self.url_entry.set_placeholder_text("Enter URL...")
        self.url_entry.set_hexpand(True)
        self.url_box.append(self.url_entry)

        # Send Button
        self.send_button = Gtk.Button(icon_name="media-playback-start-symbolic")
        self.send_button.set_tooltip_text("Send Request")
        self.send_button.add_css_class("suggested-action")
        self.url_box.append(self.send_button)

        url_clamp.set_child(self.url_box)
        self.url_container.append(url_clamp)
        self.append(self.url_container)

        # Horizontal Split: Request | Response
        split_pane = Gtk.Paned(orientation=Gtk.Orientation.HORIZONTAL)
        split_pane.set_vexpand(True)
        split_pane.set_position(UI_PANE_REQUEST_RESPONSE_POSITION)
        split_pane.set_shrink_start_child(False)
        split_pane.set_shrink_end_child(False)
        split_pane.set_resize_start_child(True)
        split_pane.set_resize_end_child(True)

        # Request Panel
        request_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        self.request_stack = Gtk.Stack()
        self.request_stack.set_vexpand(True)
        self.request_stack.set_transition_type(Gtk.StackTransitionType.CROSSFADE)
        self.request_stack.set_transition_duration(150)

        request_switcher = Gtk.StackSwitcher()
        request_switcher.set_stack(self.request_stack)
        request_switcher.set_halign(Gtk.Align.CENTER)
        request_switcher.set_margin_top(8)
        request_switcher.set_margin_bottom(8)

        request_box.append(request_switcher)
        request_box.append(self.request_stack)

        # Headers tab
        self._build_headers_tab()

        # Body tab
        self._build_body_tab()

        # Scripts tab
        self._build_scripts_tab()

        split_pane.set_start_child(request_box)

        # Response Panel
        response_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)

        # Stack switcher at the top
        response_switcher = Gtk.StackSwitcher()
        response_switcher.set_halign(Gtk.Align.CENTER)
        response_switcher.set_margin_top(8)
        response_switcher.set_margin_bottom(8)
        response_box.append(response_switcher)

        # Create a vertical paned for response stack and result panels
        self.response_main_paned = Gtk.Paned(orientation=Gtk.Orientation.VERTICAL)
        self.response_main_paned.set_vexpand(True)
        self.response_main_paned.set_position(UI_PANE_RESPONSE_DETAILS_POSITION)
        self.response_main_paned.set_shrink_start_child(False)
        # Don't allow results panels to shrink below their minimum size
        self.response_main_paned.set_shrink_end_child(False)
        self.response_main_paned.set_resize_start_child(True)
        self.response_main_paned.set_resize_end_child(True)

        # Response Stack
        self.response_stack = Gtk.Stack()
        self.response_stack.set_vexpand(True)
        self.response_stack.set_transition_type(Gtk.StackTransitionType.CROSSFADE)
        self.response_stack.set_transition_duration(150)

        response_switcher.set_stack(self.response_stack)
        self.response_main_paned.set_start_child(self.response_stack)

        # Response headers
        headers_scroll = Gtk.ScrolledWindow()
        headers_scroll.set_vexpand(True)
        self.response_headers_textview = Gtk.TextView()
        self.response_headers_textview.set_editable(False)
        self.response_headers_textview.set_monospace(True)
        self.response_headers_textview.set_left_margin(12)
        self.response_headers_textview.set_right_margin(12)
        self.response_headers_textview.set_top_margin(12)
        self.response_headers_textview.set_bottom_margin(12)
        headers_scroll.set_child(self.response_headers_textview)
        self.response_stack.add_titled(headers_scroll, "headers", "Headers")

        # Response body
        body_scroll = Gtk.ScrolledWindow()
        body_scroll.set_vexpand(True)
        self.response_body_sourceview = GtkSource.View()
        self.response_body_sourceview.set_editable(False)
        self.response_body_sourceview.set_show_line_numbers(True)
        self.response_body_sourceview.set_highlight_current_line(True)
        self.response_body_sourceview.set_left_margin(12)
        self.response_body_sourceview.set_right_margin(12)
        self.response_body_sourceview.set_top_margin(12)
        self.response_body_sourceview.set_bottom_margin(12)

        # Set up theme
        self._setup_sourceview_theme()

        # Set font
        css_provider = Gtk.CssProvider()
        css_provider.load_from_data(b"""
            textview {
                font-family: "Source Code Pro";
                font-size: 12pt;
                line-height: 1.2;
            }
        """)
        self.response_body_sourceview.get_style_context().add_provider(
            css_provider,
            Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
        )

        body_scroll.set_child(self.response_body_sourceview)
        self.response_stack.add_titled(body_scroll, "body", "Body")

        # Create a second paned for preprocessing and script results
        self.results_paned = Gtk.Paned(orientation=Gtk.Orientation.VERTICAL)
        self.results_paned.set_vexpand(True)
        self.results_paned.set_position(UI_PANE_RESULTS_PANEL_POSITION)
        self.results_paned.set_visible(False)  # Initially hidden until scripts produce output
        # Don't allow children to shrink below their minimum size
        self.results_paned.set_shrink_start_child(False)
        self.results_paned.set_shrink_end_child(False)
        self.results_paned.set_resize_start_child(True)
        self.results_paned.set_resize_end_child(True)

        # Preprocessing Results Panel (resizable)
        self.preprocessing_results_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
        self.preprocessing_results_container.set_visible(False)  # Initially hidden
        # Set minimum height to ensure header is always visible (header ~40px + content min 60px)
        self.preprocessing_results_container.set_size_request(-1, 100)

        # Header
        preprocessing_header = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
        preprocessing_header.set_margin_start(12)
        preprocessing_header.set_margin_end(12)
        preprocessing_header.set_margin_top(6)
        preprocessing_header.set_margin_bottom(6)

        preprocessing_label = Gtk.Label(label="Preprocessing Results")
        preprocessing_label.add_css_class("heading")
        preprocessing_label.set_halign(Gtk.Align.START)
        preprocessing_header.append(preprocessing_label)

        # Spacer
        preprocessing_spacer = Gtk.Box()
        preprocessing_spacer.set_hexpand(True)
        preprocessing_header.append(preprocessing_spacer)

        # Status icon
        self.preprocessing_status_icon = Gtk.Image()
        self.preprocessing_status_icon.set_from_icon_name("object-select-symbolic")
        preprocessing_header.append(self.preprocessing_status_icon)

        self.preprocessing_results_container.append(preprocessing_header)

        # Output text view (scrollable, resizable)
        self.preprocessing_output_scroll = Gtk.ScrolledWindow()
        self.preprocessing_output_scroll.set_vexpand(True)
        self.preprocessing_output_scroll.set_min_content_height(60)
        self.preprocessing_output_scroll.set_margin_start(12)
        self.preprocessing_output_scroll.set_margin_end(12)
        self.preprocessing_output_scroll.set_margin_bottom(6)

        self.preprocessing_output_textview = Gtk.TextView()
        self.preprocessing_output_textview.set_editable(False)
        self.preprocessing_output_textview.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
        self.preprocessing_output_textview.set_monospace(True)
        self.preprocessing_output_textview.set_left_margin(6)
        self.preprocessing_output_textview.set_right_margin(6)
        self.preprocessing_output_textview.set_top_margin(6)
        self.preprocessing_output_textview.set_bottom_margin(6)

        self.preprocessing_output_scroll.set_child(self.preprocessing_output_textview)
        self.preprocessing_results_container.append(self.preprocessing_output_scroll)

        self.results_paned.set_start_child(self.preprocessing_results_container)

        # Script Results Panel (resizable)
        self.script_results_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
        self.script_results_container.set_visible(False)  # Initially hidden
        # Set minimum height to ensure header is always visible (header ~40px + content min 60px)
        self.script_results_container.set_size_request(-1, 100)

        # Header
        results_header = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
        results_header.set_margin_start(12)
        results_header.set_margin_end(12)
        results_header.set_margin_top(6)
        results_header.set_margin_bottom(6)

        results_label = Gtk.Label(label="Script Output")
        results_label.add_css_class("heading")
        results_label.set_halign(Gtk.Align.START)
        results_header.append(results_label)

        # Spacer
        results_spacer = Gtk.Box()
        results_spacer.set_hexpand(True)
        results_header.append(results_spacer)

        # Status icon
        self.script_status_icon = Gtk.Image()
        self.script_status_icon.set_from_icon_name("object-select-symbolic")
        results_header.append(self.script_status_icon)

        self.script_results_container.append(results_header)

        # Output text view (scrollable, resizable)
        self.script_output_scroll = Gtk.ScrolledWindow()
        self.script_output_scroll.set_vexpand(True)
        self.script_output_scroll.set_min_content_height(60)
        self.script_output_scroll.set_margin_start(12)
        self.script_output_scroll.set_margin_end(12)
        self.script_output_scroll.set_margin_bottom(6)

        self.script_output_textview = Gtk.TextView()
        self.script_output_textview.set_editable(False)
        self.script_output_textview.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
        self.script_output_textview.set_monospace(True)
        self.script_output_textview.set_left_margin(6)
        self.script_output_textview.set_right_margin(6)
        self.script_output_textview.set_top_margin(6)
        self.script_output_textview.set_bottom_margin(6)

        self.script_output_scroll.set_child(self.script_output_textview)
        self.script_results_container.append(self.script_output_scroll)

        self.results_paned.set_end_child(self.script_results_container)

        # Set the results paned as the end child of the main response paned
        self.response_main_paned.set_end_child(self.results_paned)

        # Add the main paned to response_box
        response_box.append(self.response_main_paned)

        # Status Bar
        status_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
        status_box.set_margin_start(12)
        status_box.set_margin_end(12)
        status_box.set_margin_top(6)
        status_box.set_margin_bottom(6)

        self.status_label = Gtk.Label(label="Ready")
        self.status_label.add_css_class("heading")
        status_box.append(self.status_label)

        self.time_label = Gtk.Label(label="")
        status_box.append(self.time_label)

        self.size_label = Gtk.Label(label="")
        status_box.append(self.size_label)

        # Spacer
        spacer = Gtk.Box()
        spacer.set_hexpand(True)
        status_box.append(spacer)

        response_box.append(status_box)

        split_pane.set_end_child(response_box)

        self.append(split_pane)

    def _build_headers_tab(self) -> None:
        """Build the headers tab."""
        headers_scroll = Gtk.ScrolledWindow()
        headers_scroll.set_vexpand(True)
        headers_scroll.set_min_content_height(150)

        self.headers_listbox = Gtk.ListBox()
        self.headers_listbox.add_css_class("boxed-list")
        headers_scroll.set_child(self.headers_listbox)

        headers_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
        headers_box.set_margin_start(12)
        headers_box.set_margin_end(12)
        headers_box.set_margin_top(12)
        headers_box.set_margin_bottom(12)
        headers_box.append(headers_scroll)

        add_header_button = Gtk.Button(label="Add Header")
        add_header_button.connect("clicked", self._on_add_header_clicked)
        headers_box.append(add_header_button)

        self.request_stack.add_titled(headers_box, "headers", "Headers")

        # Add initial empty header row
        self._add_header_row()

    def _build_body_tab(self) -> None:
        """Build the body tab."""
        body_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)

        body_scroll = Gtk.ScrolledWindow()
        body_scroll.set_vexpand(True)

        self.body_sourceview = GtkSource.View()
        self.body_sourceview.set_editable(True)
        self.body_sourceview.set_show_line_numbers(True)
        self.body_sourceview.set_highlight_current_line(True)
        self.body_sourceview.set_left_margin(12)
        self.body_sourceview.set_right_margin(12)
        self.body_sourceview.set_top_margin(12)
        self.body_sourceview.set_bottom_margin(12)

        # Set font
        css_provider = Gtk.CssProvider()
        css_provider.load_from_data(b"""
            textview {
                font-family: "Source Code Pro";
                font-size: 12pt;
                line-height: 1.2;
            }
        """)
        self.body_sourceview.get_style_context().add_provider(
            css_provider,
            Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
        )

        # Set up theme
        self._setup_request_body_theme()

        # Set up text tag for undefined variable highlighting
        buffer = self.body_sourceview.get_buffer()
        self.undefined_var_tag = buffer.create_tag(
            "undefined-variable",
            background="#ffc27d30"  # Semi-transparent warning color
        )

        body_scroll.set_child(self.body_sourceview)
        body_box.append(body_scroll)

        # Bottom toolbar
        toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
        toolbar.set_margin_start(12)
        toolbar.set_margin_end(12)
        toolbar.set_margin_top(6)
        toolbar.set_margin_bottom(6)

        # Spacer
        spacer = Gtk.Box()
        spacer.set_hexpand(True)
        toolbar.append(spacer)

        # Language selector
        lang_label = Gtk.Label(label="Syntax:")
        toolbar.append(lang_label)

        lang_list = Gtk.StringList()
        for lang in ["RAW", "JSON", "XML"]:
            lang_list.append(lang)

        self.body_language_dropdown = Gtk.DropDown(model=lang_list)
        self.body_language_dropdown.set_selected(0)
        self.body_language_dropdown.connect("notify::selected", self._on_request_body_language_changed)
        toolbar.append(self.body_language_dropdown)

        body_box.append(toolbar)

        self.request_stack.add_titled(body_box, "body", "Body")

    def _build_scripts_tab(self):
        """Build the scripts tab with preprocessing and postprocessing sections."""
        scripts_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)

        # Vertical paned to separate preprocessing and postprocessing
        paned = Gtk.Paned(orientation=Gtk.Orientation.VERTICAL)
        paned.set_position(UI_PANE_SCRIPTS_POSITION)
        paned.set_vexpand(True)

        # Preprocessing Section
        preprocessing_section = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
        preprocessing_section.set_margin_start(12)
        preprocessing_section.set_margin_end(12)
        preprocessing_section.set_margin_top(12)
        preprocessing_section.set_margin_bottom(12)

        preprocessing_label = Gtk.Label(label="Preprocessing")
        preprocessing_label.set_halign(Gtk.Align.START)
        preprocessing_label.add_css_class("heading")
        preprocessing_section.append(preprocessing_label)

        preprocessing_scroll = Gtk.ScrolledWindow()
        preprocessing_scroll.set_vexpand(True)

        self.preprocessing_sourceview = GtkSource.View()
        self.preprocessing_sourceview.set_editable(True)
        self.preprocessing_sourceview.set_sensitive(True)
        self.preprocessing_sourceview.set_show_line_numbers(True)
        self.preprocessing_sourceview.set_highlight_current_line(True)
        self.preprocessing_sourceview.set_left_margin(12)
        self.preprocessing_sourceview.set_right_margin(12)
        self.preprocessing_sourceview.set_top_margin(12)
        self.preprocessing_sourceview.set_bottom_margin(12)

        # Set JavaScript syntax highlighting
        lang_manager = GtkSource.LanguageManager.get_default()
        js_lang = lang_manager.get_language('js')
        if js_lang:
            self.preprocessing_sourceview.get_buffer().set_language(js_lang)

        # Set font
        css_provider = Gtk.CssProvider()
        css_provider.load_from_data(b"""
            textview {
                font-family: "Source Code Pro";
                font-size: 12pt;
                line-height: 1.2;
            }
        """)
        self.preprocessing_sourceview.get_style_context().add_provider(
            css_provider,
            Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
        )

        # Add placeholder text
        preprocessing_buffer = self.preprocessing_sourceview.get_buffer()
        placeholder_text = """// Available: request object (modifiable)
// - request.method: "GET", "POST", "PUT", "DELETE"
// - request.url: Full URL string
// - request.headers: Object with header key-value pairs
// - request.body: Request body string
//
// Available: roster API
// - roster.getVariable(name): Get variable from selected environment
// - roster.setVariable(name, value): Set/update variable
// - roster.setVariables({key: value}): Batch set
// - roster.project.name: Current project name
// - roster.project.environments: Array of environment names
//
// Modify the request before sending:
// request.headers['Authorization'] = 'Bearer ' + roster.getVariable('token');
"""
        preprocessing_buffer.set_text(placeholder_text)

        preprocessing_scroll.set_child(self.preprocessing_sourceview)
        preprocessing_section.append(preprocessing_scroll)

        paned.set_start_child(preprocessing_section)

        # Postprocessing Section (functional)
        postprocessing_section = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
        postprocessing_section.set_margin_start(12)
        postprocessing_section.set_margin_end(12)
        postprocessing_section.set_margin_top(12)
        postprocessing_section.set_margin_bottom(12)

        postprocessing_label = Gtk.Label(label="Postprocessing")
        postprocessing_label.set_halign(Gtk.Align.START)
        postprocessing_label.add_css_class("heading")
        postprocessing_section.append(postprocessing_label)

        postprocessing_scroll = Gtk.ScrolledWindow()
        postprocessing_scroll.set_vexpand(True)

        self.postprocessing_sourceview = GtkSource.View()
        self.postprocessing_sourceview.set_editable(True)
        self.postprocessing_sourceview.set_show_line_numbers(True)
        self.postprocessing_sourceview.set_highlight_current_line(True)
        self.postprocessing_sourceview.set_left_margin(12)
        self.postprocessing_sourceview.set_right_margin(12)
        self.postprocessing_sourceview.set_top_margin(12)
        self.postprocessing_sourceview.set_bottom_margin(12)

        # Set JavaScript syntax highlighting
        if js_lang:
            self.postprocessing_sourceview.get_buffer().set_language(js_lang)

        # Set font
        self.postprocessing_sourceview.get_style_context().add_provider(
            css_provider,
            Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
        )

        # Set up theme
        self._setup_scripts_theme()

        # Set placeholder text
        buffer = self.postprocessing_sourceview.get_buffer()
        placeholder_text = """// Available: response object
// - response.body (string)
// - response.headers (object)
// - response.statusCode (number)
// - response.statusText (string)
// - response.responseTime (number, in ms)
//
// Use console.log() to output results
// Example:
// console.log('Status:', response.statusCode);
"""
        buffer.set_text(placeholder_text)

        # Connect change signal for tracking modifications
        buffer.connect('changed', self._on_script_changed)

        postprocessing_scroll.set_child(self.postprocessing_sourceview)
        postprocessing_section.append(postprocessing_scroll)

        # Toolbar
        toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)

        # Spacer
        spacer = Gtk.Box()
        spacer.set_hexpand(True)
        toolbar.append(spacer)

        # Language label
        lang_label = Gtk.Label(label="JavaScript")
        lang_label.add_css_class("dim-label")
        toolbar.append(lang_label)

        postprocessing_section.append(toolbar)

        paned.set_end_child(postprocessing_section)

        scripts_box.append(paned)

        self.request_stack.add_titled(scripts_box, "scripts", "Scripts")

    def _setup_scripts_theme(self):
        """Set up GtkSourceView theme for scripts based on system color scheme."""
        style_manager = Adw.StyleManager.get_default()
        is_dark = style_manager.get_dark()

        scheme_manager = GtkSource.StyleSchemeManager.get_default()
        scheme = scheme_manager.get_scheme('Adwaita-dark' if is_dark else 'Adwaita')

        if scheme:
            self.preprocessing_sourceview.get_buffer().set_style_scheme(scheme)
            self.postprocessing_sourceview.get_buffer().set_style_scheme(scheme)

    def _on_script_changed(self, buffer):
        """Called when script content changes."""
        # This will be used for change tracking
        pass

    def _setup_sourceview_theme(self):
        """Set up GtkSourceView theme based on system color scheme."""
        style_manager = Adw.StyleManager.get_default()
        is_dark = style_manager.get_dark()

        style_scheme_manager = GtkSource.StyleSchemeManager.get_default()
        scheme_name = 'Adwaita-dark' if is_dark else 'Adwaita'
        scheme = style_scheme_manager.get_scheme(scheme_name)

        if scheme:
            self.response_body_sourceview.get_buffer().set_style_scheme(scheme)

    def _setup_request_body_theme(self):
        """Set up GtkSourceView theme for request body."""
        style_manager = Adw.StyleManager.get_default()
        is_dark = style_manager.get_dark()

        style_scheme_manager = GtkSource.StyleSchemeManager.get_default()
        scheme_name = 'Adwaita-dark' if is_dark else 'Adwaita'
        scheme = style_scheme_manager.get_scheme(scheme_name)

        if scheme:
            self.body_sourceview.get_buffer().set_style_scheme(scheme)

    def _on_request_body_language_changed(self, dropdown, param):
        """Handle request body language selection change."""
        selected = dropdown.get_selected()
        language_manager = GtkSource.LanguageManager.get_default()

        buffer = self.body_sourceview.get_buffer()

        if selected == 0:  # RAW
            buffer.set_language(None)
        elif selected == 1:  # JSON
            language = language_manager.get_language('json')
            buffer.set_language(language)
        elif selected == 2:  # XML
            language = language_manager.get_language('xml')
            buffer.set_language(language)

    def _on_add_header_clicked(self, button):
        """Add new header row."""
        self._add_header_row()

    def _add_header_row(self, key='', value=''):
        """Add a header row to the list."""
        row = HeaderRow()
        row.set_header(key, value)
        row.connect('remove-requested', self._on_header_remove)
        row.connect('changed', self._on_request_changed)

        # Connect to variable indicator updates (debounced)
        if self.project_id:
            row.connect('changed', lambda r: self._schedule_indicator_update())

        self.headers_listbox.append(row)

        if key or value:
            self._on_request_changed(None)

    def _on_header_remove(self, header_row):
        """Handle header row removal."""
        parent = header_row.get_parent()
        if parent:
            self.headers_listbox.remove(parent)
            self._on_request_changed(None)

    def _setup_change_tracking(self) -> None:
        """Setup change tracking for this tab."""
        self.url_entry.connect("changed", self._on_request_changed)
        self.method_dropdown.connect("notify::selected", self._on_request_changed)
        self.body_language_dropdown.connect("notify::selected", self._on_request_changed)

        body_buffer = self.body_sourceview.get_buffer()
        body_buffer.connect("changed", self._on_request_changed)

        # Setup script change tracking
        preprocessing_buffer = self.preprocessing_sourceview.get_buffer()
        preprocessing_buffer.connect("changed", self._on_request_changed)

        postprocessing_buffer = self.postprocessing_sourceview.get_buffer()
        postprocessing_buffer.connect("changed", self._on_request_changed)

        # Setup variable indicator updates (debounced)
        if self.project_id:
            self.url_entry.connect("changed", lambda w: self._schedule_indicator_update())
            body_buffer.connect("changed", lambda b: self._schedule_indicator_update())

    def _on_request_changed(self, widget, *args):
        """Mark this tab as modified."""
        if not self.original_request:
            return

        current = self.get_request()
        was_modified = self.modified
        self.modified = self._is_different_from_original(current)

        # Notify if modified state changed
        if was_modified != self.modified:
            self.emit('modified-changed', self.modified)

    # Signal for modified state changes
    __gsignals__ = {
        'modified-changed': (GObject.SignalFlags.RUN_FIRST, None, (bool,))
    }

    def _is_different_from_original(self, request: HttpRequest) -> bool:
        """Check if current request differs from original."""
        if not self.original_request:
            return False

        # Check if request changed
        request_changed = (
            request.method != self.original_request.method or
            request.url != self.original_request.url or
            request.body != self.original_request.body or
            request.headers != self.original_request.headers or
            request.syntax != self.original_request.syntax
        )

        # Check if scripts changed
        current_scripts = self.get_scripts()
        scripts_changed = False

        if self.original_scripts is None and current_scripts:
            # Had no scripts, now has scripts
            scripts_changed = (current_scripts.preprocessing.strip() != "" or
                             current_scripts.postprocessing.strip() != "")
        elif self.original_scripts and current_scripts:
            # Compare scripts
            scripts_changed = (
                current_scripts.preprocessing != self.original_scripts.preprocessing or
                current_scripts.postprocessing != self.original_scripts.postprocessing
            )

        return request_changed or scripts_changed

    def _load_request(self, request: HttpRequest) -> None:
        """Load a request into this tab's UI."""
        # Set method
        methods = ["GET", "POST", "PUT", "DELETE"]
        if request.method in methods:
            self.method_dropdown.set_selected(methods.index(request.method))

        # Set URL
        self.url_entry.set_text(request.url)

        # Clear and set headers
        while child := self.headers_listbox.get_first_child():
            self.headers_listbox.remove(child)

        for key, value in request.headers.items():
            self._add_header_row(key, value)

        if not request.headers:
            self._add_header_row()

        # Set body
        buffer = self.body_sourceview.get_buffer()
        buffer.set_text(request.body)

        # Set syntax
        syntax_options = ["RAW", "JSON", "XML"]
        if request.syntax in syntax_options:
            self.body_language_dropdown.set_selected(syntax_options.index(request.syntax))

        # Update variable indicators after loading the request (if project is set)
        if self.project_id:
            self._update_variable_indicators()

    def get_request(self) -> HttpRequest:
        """Build and return HttpRequest from current UI state."""
        method = self.method_dropdown.get_selected_item().get_string()
        url = self.url_entry.get_text().strip()

        # Collect headers
        headers = {}
        child = self.headers_listbox.get_first_child()
        while child is not None:
            if isinstance(child, Gtk.ListBoxRow):
                header_row = child.get_child()
                if isinstance(header_row, HeaderRow):
                    key, value = header_row.get_header()
                    if key and value:
                        headers[key] = value
            child = child.get_next_sibling()

        # Get body
        buffer = self.body_sourceview.get_buffer()
        body = buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), False)

        # Get syntax
        syntax_index = self.body_language_dropdown.get_selected()
        syntax_options = ["RAW", "JSON", "XML"]
        syntax = syntax_options[syntax_index]

        return HttpRequest(method=method, url=url, headers=headers, body=body, syntax=syntax)

    def get_scripts(self):
        """Get current scripts from UI."""
        from .models import Scripts

        # Get preprocessing script
        preprocessing_buffer = self.preprocessing_sourceview.get_buffer()
        preprocessing = preprocessing_buffer.get_text(
            preprocessing_buffer.get_start_iter(),
            preprocessing_buffer.get_end_iter(),
            False
        )

        # Get postprocessing script
        postprocessing_buffer = self.postprocessing_sourceview.get_buffer()
        postprocessing = postprocessing_buffer.get_text(
            postprocessing_buffer.get_start_iter(),
            postprocessing_buffer.get_end_iter(),
            False
        )

        return Scripts(preprocessing=preprocessing, postprocessing=postprocessing)

    def load_scripts(self, scripts):
        """Load scripts into UI."""
        if not scripts:
            return

        # Load preprocessing script
        preprocessing_buffer = self.preprocessing_sourceview.get_buffer()
        preprocessing_buffer.set_text(scripts.preprocessing)

        # Load postprocessing script
        postprocessing_buffer = self.postprocessing_sourceview.get_buffer()
        postprocessing_buffer.set_text(scripts.postprocessing)

    def display_response(self, response: HttpResponse) -> None:
        """Display response in this tab's UI."""
        self.response = response

        # Update status
        status_text = f"{response.status_code} {response.status_text}"
        self.status_label.set_text(status_text)

        # Update time
        time_text = f"{response.response_time_ms:.0f} ms"
        self.time_label.set_text(time_text)

        # Update size
        size_text = self._format_size(response.response_size_bytes)
        self.size_label.set_text(size_text)

        # Update response headers
        buffer = self.response_headers_textview.get_buffer()
        buffer.set_text(response.headers)

        # Extract content-type and format body
        content_type = self._extract_content_type(response.headers)
        formatted_body = self._format_response_body(response.body, content_type)

        # Update response body
        source_buffer = self.response_body_sourceview.get_buffer()
        source_buffer.set_text(formatted_body)

        # Set language for syntax highlighting
        language = self._get_language_from_content_type(content_type)
        source_buffer.set_language(language)

        # Switch to body tab
        self.response_stack.set_visible_child_name("body")

    def display_error(self, error: str) -> None:
        """Display error in this tab's UI."""
        self.status_label.set_text("Error")
        self.time_label.set_text("")
        self.size_label.set_text("")

        # Clear response headers
        buffer = self.response_headers_textview.get_buffer()
        buffer.set_text("")

        # Display error in body
        source_buffer = self.response_body_sourceview.get_buffer()
        source_buffer.set_text(error)
        source_buffer.set_language(None)

    def display_script_results(self, script_result):
        """Display script execution results."""
        from .script_executor import ScriptResult

        if not isinstance(script_result, ScriptResult):
            return

        # Determine if there's meaningful content to display (before building output_text)
        has_content = (
            (script_result.success and script_result.output.strip()) or  # Has console output
            script_result.variable_updates or  # Set variables
            script_result.warnings or  # Has warnings
            not script_result.success  # Has errors
        )

        # If no content, hide the panel and return early
        if not has_content:
            self._clear_script_results()
            return

        # Build output text for display
        if script_result.success:
            self.script_status_icon.set_from_icon_name("object-select-symbolic")
            output_text = script_result.output
        else:
            self.script_status_icon.set_from_icon_name("dialog-error-symbolic")
            output_text = script_result.error if script_result.error else "Unknown error"

        # Append variable updates to output
        if script_result.variable_updates:
            if output_text:
                output_text += "\n"
            output_text += "\n--- Variables Set ---"
            for var_name, var_value in script_result.variable_updates.items():
                # Truncate long values for display
                display_value = var_value if len(var_value) <= DISPLAY_VARIABLE_MAX_LENGTH else var_value[:DISPLAY_VARIABLE_MAX_LENGTH - 3] + DISPLAY_VARIABLE_TRUNCATE_SUFFIX
                output_text += f"\n>>> {var_name} = '{display_value}'"

        # Append warnings to output
        if script_result.warnings:
            if output_text:
                output_text += "\n"
            output_text += "\n--- Warnings ---"
            for warning in script_result.warnings:
                output_text += f"\n⚠ {warning}"

        # Show container and parent paned
        self.results_paned.set_visible(True)
        self.script_results_container.set_visible(True)

        # Adjust paned positions to make the panel visible (use idle_add to wait for proper sizing)
        GLib.idle_add(self._adjust_paned_for_script_results)

        # Set output text
        buffer = self.script_output_textview.get_buffer()
        buffer.set_text(output_text)

    def _clear_script_results(self):
        """Clear and hide script results panel."""
        self.script_results_container.set_visible(False)
        buffer = self.script_output_textview.get_buffer()
        buffer.set_text("")

        # Update results_paned visibility and minimum size based on remaining visible panels
        if self.preprocessing_results_container.get_visible():
            self.results_paned.set_size_request(-1, 100)  # Only preprocessing visible
        else:
            self.results_paned.set_visible(False)  # Hide parent paned when both panels are hidden
            self.results_paned.set_size_request(-1, -1)  # No panels visible, no minimum

    def _adjust_paned_for_script_results(self):
        """Adjust paned positions to show script results panel."""
        # Ensure results_paned has proper minimum size based on visible panels
        if self.preprocessing_results_container.get_visible():
            # Both panels visible: minimum 200px (100px each)
            self.results_paned.set_size_request(-1, 200)
        else:
            # Only script results visible: minimum 100px
            self.results_paned.set_size_request(-1, 100)

        # Reserve space in the main paned for results
        main_height = self.response_main_paned.get_height()
        if main_height > 200:
            # Set position to show ~150px of results at the bottom
            self.response_main_paned.set_position(main_height - UI_SCRIPT_RESULTS_PANEL_HEIGHT)

        # Adjust results paned to show script output (bottom panel)
        results_height = self.results_paned.get_height()
        if results_height > 150:
            # If preprocessing is visible, share space
            if self.preprocessing_results_container.get_visible():
                self.results_paned.set_position(UI_PANE_RESULTS_PANEL_POSITION)  # Give preprocessing minimal space
            # If only script results, the paned will show it automatically

        return False  # Don't repeat

    def display_preprocessing_results(self, preprocessing_result):
        """Display preprocessing execution results."""
        from .script_executor import ScriptResult

        if not isinstance(preprocessing_result, ScriptResult):
            return

        # Determine if there's meaningful content to display (before building output_text)
        has_content = (
            (preprocessing_result.success and preprocessing_result.output.strip()) or  # Has console output
            preprocessing_result.variable_updates or  # Set variables
            preprocessing_result.warnings or  # Has warnings
            not preprocessing_result.success  # Has errors
        )

        # If no content, hide the panel and return early
        if not has_content:
            self._clear_preprocessing_results()
            return

        # Build output text for display
        if preprocessing_result.success:
            self.preprocessing_status_icon.set_from_icon_name("object-select-symbolic")
            output_text = preprocessing_result.output
        else:
            self.preprocessing_status_icon.set_from_icon_name("dialog-error-symbolic")
            output_text = preprocessing_result.error if preprocessing_result.error else "Unknown error"

        # Append variable updates to output
        if preprocessing_result.variable_updates:
            if output_text:
                output_text += "\n"
            output_text += "\n--- Variables Set ---"
            for var_name, var_value in preprocessing_result.variable_updates.items():
                # Truncate long values for display
                display_value = var_value if len(var_value) <= DISPLAY_VARIABLE_MAX_LENGTH else var_value[:DISPLAY_VARIABLE_MAX_LENGTH - 3] + DISPLAY_VARIABLE_TRUNCATE_SUFFIX
                output_text += f"\n>>> {var_name} = '{display_value}'"

        # Append warnings to output
        if preprocessing_result.warnings:
            if output_text:
                output_text += "\n"
            output_text += "\n--- Warnings ---"
            for warning in preprocessing_result.warnings:
                output_text += f"\n⚠ {warning}"

        # Add request modification summary if successful and there's other content to show
        if preprocessing_result.success and preprocessing_result.modified_request:
            if output_text:
                output_text += "\n"
            output_text += "\n--- Request Modified ---"
            output_text += "\n✓ Request was modified by preprocessing script"

        # Show container and parent paned
        self.results_paned.set_visible(True)
        self.preprocessing_results_container.set_visible(True)

        # Adjust paned positions to make the panel visible (use idle_add to wait for proper sizing)
        GLib.idle_add(self._adjust_paned_for_preprocessing_results)

        # Set output text
        buffer = self.preprocessing_output_textview.get_buffer()
        buffer.set_text(output_text)

    def _clear_preprocessing_results(self):
        """Clear and hide preprocessing results panel."""
        self.preprocessing_results_container.set_visible(False)
        buffer = self.preprocessing_output_textview.get_buffer()
        buffer.set_text("")

        # Update results_paned visibility and minimum size based on remaining visible panels
        if self.script_results_container.get_visible():
            self.results_paned.set_size_request(-1, 100)  # Only script results visible
        else:
            self.results_paned.set_visible(False)  # Hide parent paned when both panels are hidden
            self.results_paned.set_size_request(-1, -1)  # No panels visible, no minimum

    def _adjust_paned_for_preprocessing_results(self):
        """Adjust paned positions to show preprocessing results panel."""
        # Ensure results_paned has proper minimum size based on visible panels
        if self.script_results_container.get_visible():
            # Both panels visible: minimum 200px (100px each)
            self.results_paned.set_size_request(-1, 200)
        else:
            # Only preprocessing visible: minimum 100px
            self.results_paned.set_size_request(-1, 100)

        # Reserve space in the main paned for results
        main_height = self.response_main_paned.get_height()
        if main_height > 200:
            # Set position to show ~150px of results at the bottom
            self.response_main_paned.set_position(main_height - UI_SCRIPT_RESULTS_PANEL_HEIGHT)

        # Adjust results paned to show preprocessing output (top panel)
        results_height = self.results_paned.get_height()
        if results_height > 150:
            # If script results is also visible, share space equally
            if self.script_results_container.get_visible():
                self.results_paned.set_position(results_height // 2)  # Split space
            # If only preprocessing, the paned will show it automatically

        return False  # Don't repeat

    def _extract_content_type(self, headers_text):
        """Extract content-type from response headers."""
        if not headers_text:
            return ""

        for line in headers_text.split('\n'):
            line = line.strip()
            if line.lower().startswith('content-type:'):
                return line.split(':', 1)[1].strip().lower()

        return ""

    def _is_json_content_type(self, content_type):
        """Check if the content type is JSON or a JSON-based format."""
        return ('application/json' in content_type
                or 'text/json' in content_type
                or '+json' in content_type)

    def _is_xml_content_type(self, content_type):
        """Check if the content type is XML or an XML-based format."""
        return ('application/xml' in content_type
                or 'text/xml' in content_type
                or '+xml' in content_type)

    def _get_language_from_content_type(self, content_type):
        """Get GtkSourceView language ID from content type."""
        if not content_type:
            return None

        language_manager = GtkSource.LanguageManager.get_default()

        if self._is_json_content_type(content_type):
            return language_manager.get_language('json')
        elif self._is_xml_content_type(content_type):
            return language_manager.get_language('xml')
        elif 'text/html' in content_type:
            return language_manager.get_language('html')
        elif 'application/javascript' in content_type or 'text/javascript' in content_type:
            return language_manager.get_language('js')
        elif 'text/css' in content_type:
            return language_manager.get_language('css')

        return None

    def _format_response_body(self, body, content_type):
        """Format response body based on content type."""
        if not body or not body.strip():
            return body

        try:
            if self._is_json_content_type(content_type):
                parsed = json.loads(body)
                return json.dumps(parsed, indent=2, ensure_ascii=False)
            elif self._is_xml_content_type(content_type):
                dom = xml.dom.minidom.parseString(body)
                return dom.toprettyxml(indent="  ")
        except Exception as e:
            logger.debug(f"Failed to format response body: {e}")

        return body

    def _format_size(self, size_bytes):
        """Format size in bytes to human-readable format (B, KB, MB, GB)."""
        if size_bytes < 1024:
            return f"{size_bytes} B"
        elif size_bytes < 1024 * 1024:
            size_kb = size_bytes / 1024
            return f"{size_kb:.1f} KB"
        elif size_bytes < 1024 * 1024 * 1024:
            size_mb = size_bytes / (1024 * 1024)
            return f"{size_mb:.1f} MB"
        else:
            size_gb = size_bytes / (1024 * 1024 * 1024)
            return f"{size_gb:.1f} GB"

    def _build_environment_selector_inline(self, container):
        """Build environment selector and add to container."""
        if not self.project_id:
            return

        # Create dropdown (compact, no label)
        self.environment_dropdown = Gtk.DropDown()
        self.environment_dropdown.set_enable_search(False)
        self.environment_dropdown.set_tooltip_text("Select environment for variable substitution")

        # Connect signal handler
        self._env_change_handler_id = self.environment_dropdown.connect("notify::selected", self._on_environment_changed)

        # Add to container at the beginning (left side)
        container.prepend(self.environment_dropdown)

        # Populate if project_manager is already available
        if self.project_manager:
            self._populate_environment_dropdown()

    def _populate_environment_dropdown(self):
        """Populate environment dropdown with project's environments."""
        if not self.environment_dropdown or not self.project_manager or not self.project_id:
            return

        # Get project
        projects = self.project_manager.load_projects()
        project = None
        for p in projects:
            if p.id == self.project_id:
                project = p
                break

        if not project:
            return

        # Set flag to indicate we're programmatically changing the dropdown
        # This prevents the signal handler from triggering indicator updates during initialization
        self._is_programmatically_changing_environment = True
        old_env_id = self.selected_environment_id

        try:
            # Build string list with "None" + environment names
            string_list = Gtk.StringList()
            string_list.append("None")

            # Track environment IDs (index 0 is None)
            self.environment_ids = [None]

            for env in project.environments:
                string_list.append(env.name)
                self.environment_ids.append(env.id)

            self.environment_dropdown.set_model(string_list)

            # Select current environment
            if self.selected_environment_id:
                try:
                    index = self.environment_ids.index(self.selected_environment_id)
                    self.environment_dropdown.set_selected(index)
                except ValueError:
                    # Previously selected environment no longer exists
                    self.selected_environment_id = None
                    self.environment_dropdown.set_selected(0)  # Default to "None"
            else:
                self.environment_dropdown.set_selected(0)  # Default to "None"

        finally:
            # Always clear the flag
            self._is_programmatically_changing_environment = False

        # If the effective environment changed (e.g. was deleted), update indicators
        if self.selected_environment_id != old_env_id:
            self._update_variable_indicators()

    def _show_environment_selector(self):
        """Show environment selector (create if it doesn't exist)."""
        # If selector already exists, nothing to do
        if self.environment_dropdown:
            return

        # Create and add the selector to URL container
        if hasattr(self, 'url_container'):
            self._build_environment_selector_inline(self.url_container)

            # Add separator after environment dropdown (insert before the url_clamp)
            self.env_separator = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL)
            self.env_separator.set_margin_start(12)
            self.env_separator.set_margin_end(12)
            # Insert separator as second child (after environment dropdown)
            first_child = self.url_container.get_first_child()
            if first_child:
                self.url_container.insert_child_after(self.env_separator, first_child)

            # Populate if project_manager is available
            if self.project_manager:
                self._populate_environment_dropdown()
                # Update indicators after environment selector is shown
                self._update_variable_indicators()

    def _on_environment_changed(self, dropdown, _param):
        """Handle environment selection change."""
        # If we're programmatically changing the environment during population, skip this
        if self._is_programmatically_changing_environment:
            return

        if not hasattr(self, 'environment_ids'):
            logger.warning("Environment changed but environment_ids not initialized")
            return

        selected_index = dropdown.get_selected()

        if selected_index < len(self.environment_ids):
            self.selected_environment_id = self.environment_ids[selected_index]
        else:
            logger.warning(f"Environment index {selected_index} out of range (max {len(self.environment_ids)-1})")
            self.selected_environment_id = None

        # Always update visual indicators when environment changes
        self._update_variable_indicators()

    def get_selected_environment(self, callback):
        """
        Get the currently selected environment object with all values (async).

        This includes sensitive variable values from the keyring.

        Args:
            callback: Function called with the Environment object (or None)
        """
        if not self.selected_environment_id or not self.project_manager or not self.project_id:
            callback(None)
            return

        # Get environment with secrets (includes values from keyring) - async
        self.project_manager.get_environment_with_secrets(
            self.project_id,
            self.selected_environment_id,
            callback
        )

    def _detect_undefined_variables(self, callback):
        """Detect undefined variables in the current request (async).

        Args:
            callback: Function called with set of undefined variable names
        """
        from .variable_substitution import VariableSubstitution

        # Get current request from UI
        request = self.get_request()

        def on_environment_loaded(env):
            if not env:
                # No environment selected - ALL variables are undefined
                # Find all variables in the request
                all_vars = set()
                all_vars.update(VariableSubstitution.find_variables(request.url))
                all_vars.update(VariableSubstitution.find_variables(request.body))
                for key, value in request.headers.items():
                    all_vars.update(VariableSubstitution.find_variables(key))
                    all_vars.update(VariableSubstitution.find_variables(value))
                callback(all_vars)
            else:
                # Environment selected - find which variables are undefined
                _, undefined = VariableSubstitution.substitute_request(request, env)
                callback(undefined)

        # Get selected environment asynchronously
        self.get_selected_environment(on_environment_loaded)

    def _schedule_indicator_update(self):
        """Schedule an indicator update with debouncing."""
        # Cancel existing timeout
        if self._update_indicators_timeout_id:
            GLib.source_remove(self._update_indicators_timeout_id)

        # Schedule new update after 500ms
        self._update_indicators_timeout_id = GLib.timeout_add(DEBOUNCE_VARIABLE_INDICATORS_MS, self._update_variable_indicators_timeout)

    def _update_variable_indicators_timeout(self):
        """Timeout callback for updating indicators."""
        self._update_variable_indicators()
        self._update_indicators_timeout_id = None
        return False  # Don't repeat

    def _update_variable_indicators(self):
        """Update visual indicators for undefined variables (async)."""
        # Only update if we have a project (variables only make sense in project context)
        if not self.project_id:
            return

        def on_undefined_detected(undefined_vars):
            # Store detected undefined variables
            self.undefined_variables = undefined_vars

            # Update URL entry
            self._update_url_indicator()

            # Update headers
            self._update_header_indicators()

            # Update body
            self._update_body_indicators()

        # Detect undefined variables asynchronously
        self._detect_undefined_variables(on_undefined_detected)

    def _update_url_indicator(self):
        """Update warning indicator on URL entry."""
        from .variable_substitution import VariableSubstitution

        url_text = self.url_entry.get_text()
        url_vars = set(VariableSubstitution.find_variables(url_text))
        undefined_in_url = url_vars & self.undefined_variables

        if undefined_in_url:
            self.url_entry.add_css_class("warning")
            tooltip = "Undefined variables: " + ", ".join(sorted(undefined_in_url))
            self.url_entry.set_tooltip_text(tooltip)
        else:
            self.url_entry.remove_css_class("warning")
            self.url_entry.set_tooltip_text("")

    def _update_header_indicators(self):
        """Update warning indicators on header rows."""
        from .variable_substitution import VariableSubstitution
        from .widgets.header_row import HeaderRow

        # Iterate through all header rows
        child = self.headers_listbox.get_first_child()
        while child:
            # HeaderRow might be a child of the ListBox row
            header_row = None
            if isinstance(child, HeaderRow):
                header_row = child
            elif hasattr(child, 'get_child'):
                # GTK4 ListBox wraps widgets in GtkListBoxRow
                actual_child = child.get_child()
                if isinstance(actual_child, HeaderRow):
                    header_row = actual_child

            if header_row:
                # Check key
                key_text = header_row.key_entry.get_text()
                key_vars = set(VariableSubstitution.find_variables(key_text))
                undefined_in_key = key_vars & self.undefined_variables

                if undefined_in_key:
                    header_row.key_entry.add_css_class("warning")
                    tooltip = "Undefined: " + ", ".join(sorted(undefined_in_key))
                    header_row.key_entry.set_tooltip_text(tooltip)
                else:
                    header_row.key_entry.remove_css_class("warning")
                    header_row.key_entry.set_tooltip_text("")

                # Check value
                value_text = header_row.value_entry.get_text()
                value_vars = set(VariableSubstitution.find_variables(value_text))
                undefined_in_value = value_vars & self.undefined_variables

                if undefined_in_value:
                    header_row.value_entry.add_css_class("warning")
                    tooltip = "Undefined: " + ", ".join(sorted(undefined_in_value))
                    header_row.value_entry.set_tooltip_text(tooltip)
                else:
                    header_row.value_entry.remove_css_class("warning")
                    header_row.value_entry.set_tooltip_text("")

            child = child.get_next_sibling()

    def _update_body_indicators(self):
        """Update warning indicators in body text with colored background."""
        import re
        from .variable_substitution import VariableSubstitution

        buffer = self.body_sourceview.get_buffer()

        # Get body text
        start_iter = buffer.get_start_iter()
        end_iter = buffer.get_end_iter()
        body_text = buffer.get_text(start_iter, end_iter, False)

        # Remove all existing undefined-variable tags
        buffer.remove_tag(self.undefined_var_tag, start_iter, end_iter)

        # Find all {{variable}} patterns and highlight undefined ones
        pattern = VariableSubstitution.VARIABLE_PATTERN
        for match in pattern.finditer(body_text):
            var_name = match.group(1)

            # Check if this variable is undefined
            if var_name in self.undefined_variables:
                # Get start and end positions
                match_start = match.start()
                match_end = match.end()

                # Create text iters at these positions
                start_tag_iter = buffer.get_iter_at_offset(match_start)
                end_tag_iter = buffer.get_iter_at_offset(match_end)

                # Apply the tag
                buffer.apply_tag(self.undefined_var_tag, start_tag_iter, end_tag_iter)

        # Also set tooltip for overall info
        body_vars = set(VariableSubstitution.find_variables(body_text))
        undefined_in_body = body_vars & self.undefined_variables

        if undefined_in_body:
            tooltip = "Undefined variables: " + ", ".join(sorted(undefined_in_body))
            self.body_sourceview.set_tooltip_text(tooltip)
        else:
            self.body_sourceview.set_tooltip_text("")
