import customtkinter as ctk
from core.process_runner import ProcessRunner
from core import reporting
import threading
import os 
import ipaddress # Required for IP address validation
from tkinter import messagebox # For user feedback on validation errors

class NetworkFrame(ctk.CTkFrame):
    def __init__(self, master, set_session_callback=None, heading_font=None, text_font=None):
        super().__init__(master)
        self.set_session_callback = set_session_callback
        self.heading_font = heading_font if heading_font else ctk.CTkFont(size=18, weight="bold")
        self.text_font = text_font if text_font else ctk.CTkFont(size=12)
        self.monospace_font = ctk.CTkFont(family="Fira Code", size=12) # For output textboxes
        self.grid_columnconfigure(0, weight=1)
        self.grid_rowconfigure(1, weight=1)

        title = ctk.CTkLabel(self, text="Sección de Herramientas de Red", font=self.heading_font)
        title.grid(row=0, column=0, padx=20, pady=(20, 10), sticky="ew")

        self.tab_view = ctk.CTkTabview(self)
        self.tab_view.grid(row=1, column=0, padx=20, pady=10, sticky="nsew")

        self.tab_view.add("Nmap")
        self.tab_view.add("Hping3")
        self.tab_view.add("Tshark")

        self.nmap_process_runner = None
        self.hping3_process_runner = None
        self.tshark_process_runner = None # New attribute for Tshark
        self.current_session_name = None
        self._tshark_current_log_format = "txt" # Default format for tshark

        self.setup_nmap_tab()
        self.setup_hping3_tab()
        self.setup_tshark_tab()

    def setup_nmap_tab(self):
        tab = self.tab_view.tab("Nmap")
        tab.grid_columnconfigure(0, weight=1)
        tab.grid_rowconfigure(4, weight=1) # Adjusted for output textbox vertical expansion

        # ---- Input Controls Frame ----
        input_controls_frame = ctk.CTkFrame(tab)
        input_controls_frame.grid(row=0, column=0, padx=10, pady=10, sticky="ew")
        input_controls_frame.grid_columnconfigure(0, weight=1)
        input_controls_frame.grid_columnconfigure(1, weight=2) # Entry wider than label

        # Target Host/IP
        self.nmap_target_label = ctk.CTkLabel(input_controls_frame, text="Objetivo (Host/IP):")
        self.nmap_target_label.grid(row=0, column=0, padx=10, pady=8, sticky="w")
        self.nmap_target_entry = ctk.CTkEntry(input_controls_frame, placeholder_text="ej: scanme.nmap.org o 192.168.1.1")
        self.nmap_target_entry.grid(row=0, column=1, padx=10, pady=8, sticky="ew")

        # Scan Type Options (e.g., SYN scan, OS detection, Service detection)
        self.nmap_options_label = ctk.CTkLabel(input_controls_frame, text="Opciones de Escaneo:")
        self.nmap_options_label.grid(row=1, column=0, padx=10, pady=8, sticky="w")
        self.nmap_options_entry = ctk.CTkEntry(input_controls_frame, placeholder_text="ej: -sS -sV -O -p 1-1000")
        self.nmap_options_entry.grid(row=1, column=1, padx=10, pady=8, sticky="ew")

        # ---- Action Buttons Frame ----
        action_buttons_frame = ctk.CTkFrame(tab)
        action_buttons_frame.grid(row=1, column=0, padx=10, pady=10, sticky="ew")
        action_buttons_frame.grid_columnconfigure(0, weight=1)
        action_buttons_frame.grid_columnconfigure(1, weight=1)

        self.nmap_start_button = ctk.CTkButton(action_buttons_frame, text="Iniciar Escaneo Nmap", command=self.start_nmap_scan)
        self.nmap_start_button.grid(row=0, column=0, padx=10, pady=12, sticky="ew")

        self.nmap_stop_button = ctk.CTkButton(action_buttons_frame, text="Detener Escaneo", command=self.stop_nmap_scan, fg_color="red", state="disabled")
        self.nmap_stop_button.grid(row=0, column=1, padx=10, pady=12, sticky="ew")

        # ---- Status and Progress Indicators ----
        self.nmap_status_label = ctk.CTkLabel(tab, text="Listo", font=self.text_font)
        self.nmap_status_label.grid(row=2, column=0, padx=10, pady=(5, 0), sticky="w")
        
        self.nmap_progress_bar = ctk.CTkProgressBar(tab, mode="indeterminate")
        self.nmap_progress_bar.grid(row=3, column=0, padx=10, pady=(0, 5), sticky="ew")
        self.nmap_progress_bar.set(0) # Hide it initially

        # ---- Output ----
        self.nmap_output_textbox = ctk.CTkTextbox(tab, state="disabled", font=self.monospace_font)
        self.nmap_output_textbox.grid(row=4, column=0, padx=10, pady=10, sticky="nsew")

    def setup_hping3_tab(self):
        tab = self.tab_view.tab("Hping3")
        tab.grid_columnconfigure(0, weight=1)
        tab.grid_rowconfigure(4, weight=1) # Adjusted for output textbox vertical expansion

        # ---- Input Controls Frame ----
        input_controls_frame = ctk.CTkFrame(tab)
        input_controls_frame.grid(row=0, column=0, padx=10, pady=10, sticky="ew")
        input_controls_frame.grid_columnconfigure(0, weight=1)
        input_controls_frame.grid_columnconfigure(1, weight=2) # Entry wider than label

        # Target Host/IP
        self.hping3_target_label = ctk.CTkLabel(input_controls_frame, text="Objetivo (Host/IP):")
        self.hping3_target_label.grid(row=0, column=0, padx=10, pady=8, sticky="w")
        self.hping3_target_entry = ctk.CTkEntry(input_controls_frame, placeholder_text="ej: example.com o 192.168.1.1")
        self.hping3_target_entry.grid(row=0, column=1, padx=10, pady=8, sticky="ew")

        # Destination Port
        self.hping3_port_label = ctk.CTkLabel(input_controls_frame, text="Puerto Destino (-p):")
        self.hping3_port_label.grid(row=1, column=0, padx=10, pady=8, sticky="w")
        self.hping3_port_entry = ctk.CTkEntry(input_controls_frame, placeholder_text="ej: 80 (para HTTP)")
        self.hping3_port_entry.grid(row=1, column=1, padx=10, pady=8, sticky="ew")

        # Count
        self.hping3_count_label = ctk.CTkLabel(input_controls_frame, text="Cantidad de Paquetes (-c):")
        self.hping3_count_label.grid(row=2, column=0, padx=10, pady=8, sticky="w")
        self.hping3_count_entry = ctk.CTkEntry(input_controls_frame, placeholder_text="ej: 5 (por defecto infinito)")
        self.hping3_count_entry.grid(row=2, column=1, padx=10, pady=8, sticky="ew")

        # Interval
        self.hping3_interval_label = ctk.CTkLabel(input_controls_frame, text="Intervalo (segundos) (-i):")
        self.hping3_interval_label.grid(row=3, column=0, padx=10, pady=8, sticky="w")
        self.hping3_interval_entry = ctk.CTkEntry(input_controls_frame, placeholder_text="ej: 1 (segundo)")
        self.hping3_interval_entry.grid(row=3, column=1, padx=10, pady=8, sticky="ew")

        # Flags (SYN, ACK, FIN, UDP, ICMP)
        flags_frame = ctk.CTkFrame(input_controls_frame, fg_color="transparent")
        flags_frame.grid(row=4, column=0, columnspan=2, padx=5, pady=5, sticky="ew")
        flags_frame.grid_columnconfigure(0, weight=1)
        flags_frame.grid_columnconfigure(1, weight=1)
        flags_frame.grid_columnconfigure(2, weight=1)

        self.hping3_syn_var = ctk.BooleanVar(value=True) # Default to SYN for common use
        self.hping3_syn_checkbox = ctk.CTkCheckBox(flags_frame, text="SYN (-S)", variable=self.hping3_syn_var)
        self.hping3_syn_checkbox.grid(row=0, column=0, padx=10, pady=5, sticky="w")

        self.hping3_ack_var = ctk.BooleanVar(value=False)
        self.hping3_ack_checkbox = ctk.CTkCheckBox(flags_frame, text="ACK (-A)", variable=self.hping3_ack_var)
        self.hping3_ack_checkbox.grid(row=0, column=1, padx=10, pady=5, sticky="w")
        
        self.hping3_fin_var = ctk.BooleanVar(value=False)
        self.hping3_fin_checkbox = ctk.CTkCheckBox(flags_frame, text="FIN (-F)", variable=self.hping3_fin_var)
        self.hping3_fin_checkbox.grid(row=0, column=2, padx=10, pady=5, sticky="w")

        self.hping3_udp_var = ctk.BooleanVar(value=False)
        self.hping3_udp_checkbox = ctk.CTkCheckBox(flags_frame, text="UDP (--udp)", variable=self.hping3_udp_var)
        self.hping3_udp_checkbox.grid(row=1, column=0, padx=10, pady=5, sticky="w")

        self.hping3_icmp_var = ctk.BooleanVar(value=False)
        self.hping3_icmp_checkbox = ctk.CTkCheckBox(flags_frame, text="ICMP (--icmp)", variable=self.hping3_icmp_var)
        self.hping3_icmp_checkbox.grid(row=1, column=1, padx=10, pady=5, sticky="w")

        # Data Size
        self.hping3_data_label = ctk.CTkLabel(input_controls_frame, text="Tamaño de Datos (-d):")
        self.hping3_data_label.grid(row=5, column=0, padx=10, pady=8, sticky="w")
        self.hping3_data_entry = ctk.CTkEntry(input_controls_frame, placeholder_text="ej: 100 (bytes)")
        self.hping3_data_entry.grid(row=5, column=1, padx=10, pady=8, sticky="ew")

        # ---- Action Buttons Frame ----
        action_buttons_frame = ctk.CTkFrame(tab)
        action_buttons_frame.grid(row=1, column=0, padx=10, pady=10, sticky="ew")
        action_buttons_frame.grid_columnconfigure(0, weight=1)
        action_buttons_frame.grid_columnconfigure(1, weight=1)

        self.hping3_start_button = ctk.CTkButton(action_buttons_frame, text="Iniciar Hping3", command=self.start_hping3_scan)
        self.hping3_start_button.grid(row=0, column=0, padx=10, pady=12, sticky="ew")

        self.hping3_stop_button = ctk.CTkButton(action_buttons_frame, text="Detener Hping3", command=self.stop_hping3_scan, fg_color="red", state="disabled")
        self.hping3_stop_button.grid(row=0, column=1, padx=10, pady=12, sticky="ew")

        # ---- Status and Progress Indicators ----
        self.hping3_status_label = ctk.CTkLabel(tab, text="Listo", font=self.text_font)
        self.hping3_status_label.grid(row=2, column=0, padx=10, pady=(5, 0), sticky="w")
        
        self.hping3_progress_bar = ctk.CTkProgressBar(tab, mode="indeterminate")
        self.hping3_progress_bar.grid(row=3, column=0, padx=10, pady=(0, 5), sticky="ew")
        self.hping3_progress_bar.set(0) # Hide it initially

        # ---- Output ----
        self.hping3_output_textbox = ctk.CTkTextbox(tab, state="disabled", font=self.monospace_font)
        self.hping3_output_textbox.grid(row=4, column=0, padx=10, pady=10, sticky="nsew")

    def _update_status_label(self, label_widget, text, color):
        label_widget.configure(text=text, text_color=color)

    def _is_valid_ip_or_hostname(self, target):
        try:
            ipaddress.ip_address(target)
            return True
        except ValueError:
            # Not a valid IP, check if it's a simple hostname
            if target and all(c.isalnum() or c in '.-' for c in target) and '.' in target:
                return True
            return False

    def _is_valid_port(self, port_str):
        try:
            port = int(port_str)
            return 1 <= port <= 65535
        except ValueError:
            return False

    def _is_valid_int_range(self, num_str, min_val=0, max_val=999999): # Sensible defaults
        try:
            num = int(num_str)
            return min_val <= num <= max_val
        except ValueError:
            return False

    def _sanitize_cli_input(self, input_string):
        """
        Basic sanitization for CLI inputs to prevent simple command injection.
        Removes common shell metacharacters.
        """
        if not isinstance(input_string, str):
            return ""
        # Remove characters that could break out of a command or inject new ones
        sanitized = input_string.replace(';', '').replace('&', '').replace('|', '')
        sanitized = sanitized.replace('`', '').replace('$', '').replace('(', '').replace(')', '')
        sanitized = sanitized.replace('<', '').replace('>', '').replace('!', '').replace('*', '')
        # Allow spaces for options, but be careful with quotes if rejoining
        return sanitized.strip()

    def start_nmap_scan(self):
        target = self.nmap_target_entry.get()
        options = self.nmap_options_entry.get()

        if not target:
            self._update_status_label(self.nmap_status_label, "Error: Objetivo no especificado.", "red")
            return
        if not self._is_valid_ip_or_hostname(target):
            self._update_status_label(self.nmap_status_label, "Error: Formato de Host/IP objetivo inválido.", "red")
            return
        
        sanitized_options = self._sanitize_cli_input(options)

        self.nmap_start_button.configure(state="disabled")
        self.nmap_stop_button.configure(state="normal")
        
        self._update_status_label(self.nmap_status_label, "Iniciando Nmap...", "orange")
        self.nmap_progress_bar.start()

        self.current_session_name = reporting.create_session(target)
        if self.set_session_callback:
            self.set_session_callback(self.current_session_name)
        
        nmap_path = "/usr/bin/nmap" # Assuming nmap is in /usr/bin or user's PATH
        command = [nmap_path, target]
        if sanitized_options:
            command.extend(sanitized_options.split()) # Split options by space

        # Generate a temporary XML file for Nmap output
        self.nmap_xml_output_file = os.path.join("/tmp", f"nmap_scan_{self.current_session_name}.xml")
        command.extend(["-oX", self.nmap_xml_output_file])
        self.nmap_output_textbox.configure(state="normal")
        self.nmap_output_textbox.delete("1.0", "end")
        self.nmap_output_textbox.insert("1.0", f"--- Iniciando escaneo de Nmap en {target} con opciones '{options}' ---\nComando: {' '.join(command)}\n\n")
        self.nmap_output_textbox.configure(state="disabled")

        self.nmap_process_runner = ProcessRunner(
            command,
            self.nmap_output_textbox,
            status_label=self.nmap_status_label,
            progress_bar=self.nmap_progress_bar
        )
        self.nmap_process_runner.start()
        
        threading.Thread(target=self.wait_and_save_nmap, daemon=True).start()

    def stop_nmap_scan(self):
        if self.nmap_process_runner:
            self.nmap_process_runner.stop()
            self.nmap_start_button.configure(state="normal")
            self.nmap_stop_button.configure(state="disabled")
            self._update_status_label(self.nmap_status_label, "Detenido.", "red")
            self.nmap_progress_bar.stop()

    def wait_and_save_nmap(self):
        if self.nmap_process_runner:
            self.nmap_process_runner.thread.join()

        full_output = self.nmap_output_textbox.get("1.0", "end")
        reporting.log_tool_output(self.current_session_name, "Nmap Scan (Text)", full_output, output_format="txt")

        # Read and log the XML output
        if os.path.exists(self.nmap_xml_output_file):
            with open(self.nmap_xml_output_file, 'r') as f:
                xml_output = f.read()
            reporting.log_tool_output(self.current_session_name, "Nmap Scan (XML)", xml_output, output_format="xml")
            os.remove(self.nmap_xml_output_file) # Clean up the temporary XML file
        
        self.master.after(0, self._reset_nmap_buttons)

    def _reset_nmap_buttons(self):
        self.nmap_start_button.configure(state="normal")
        self.nmap_stop_button.configure(state="disabled")
        self._update_status_label(self.nmap_status_label, "Completado.", "green")
        self.nmap_progress_bar.stop()

    def start_hping3_scan(self):
        target = self.hping3_target_entry.get()
        port = self.hping3_port_entry.get()
        count = self.hping3_count_entry.get()
        interval = self.hping3_interval_entry.get()
        data_size = self.hping3_data_entry.get()

        if not target:
            self._update_status_label(self.hping3_status_label, "Error: Objetivo no especificado.", "red")
            return
        if not self._is_valid_ip_or_hostname(target):
            self._update_status_label(self.hping3_status_label, "Error: Formato de Host/IP objetivo inválido.", "red")
            return

        if port and not self._is_valid_port(port):
            self._update_status_label(self.hping3_status_label, "Error: Puerto destino inválido (1-65535).", "red")
            return
        if count and not self._is_valid_int_range(count, min_val=1): # Count should be at least 1
            self._update_status_label(self.hping3_status_label, "Error: Cantidad de paquetes inválida.", "red")
            return
        if interval and not self._is_valid_int_range(interval, min_val=0): # Interval can be 0 (fast)
            self._update_status_label(self.hping3_status_label, "Error: Intervalo inválido.", "red")
            return
        if data_size and not self._is_valid_int_range(data_size, min_val=0): # Data size can be 0
            self._update_status_label(self.hping3_status_label, "Error: Tamaño de datos inválido.", "red")
            return

        self.hping3_start_button.configure(state="disabled")
        self.hping3_stop_button.configure(state="normal")
        self._update_status_label(self.hping3_status_label, "Iniciando Hping3...", "orange")
        self.hping3_progress_bar.start()

        self.current_session_name = reporting.create_session(target)
        if self.set_session_callback:
            self.set_session_callback(self.current_session_name)
        
        hping3_path = "/usr/sbin/hping3" # hping3 is often in /usr/sbin, may require sudo

        command = [hping3_path, target]

        if self.hping3_syn_var.get():
            command.append("-S")
        if self.hping3_ack_var.get():
            command.append("-A")
        if self.hping3_fin_var.get():
            command.append("-F")
        if self.hping3_udp_var.get():
            command.append("--udp")
        if self.hping3_icmp_var.get():
            command.append("--icmp")

        if port:
            command.extend(["-p", port])
        if count:
            command.extend(["-c", count])
        if interval:
            command.extend(["-i", interval])
        if data_size:
            command.extend(["-d", data_size])

        self.hping3_output_textbox.configure(state="normal")
        self.hping3_output_textbox.delete("1.0", "end")
        self.hping3_output_textbox.insert("1.0", f"--- Iniciando Hping3 en {target} ---\nComando: {' '.join(command)}\n\n")
        self.hping3_output_textbox.configure(state="disabled")

        self.hping3_process_runner = ProcessRunner(
            command,
            self.hping3_output_textbox,
            status_label=self.hping3_status_label,
            progress_bar=self.hping3_progress_bar,
            requires_sudo=True # hping3 commands often require sudo
        )
        self.hping3_process_runner.start()
        
        threading.Thread(target=self.wait_and_save_hping3, daemon=True).start()

    def stop_hping3_scan(self):
        if self.hping3_process_runner:
            self.hping3_process_runner.stop()
            self.hping3_start_button.configure(state="normal")
            self.hping3_stop_button.configure(state="disabled")
            self._update_status_label(self.hping3_status_label, "Detenido.", "red")
            self.hping3_progress_bar.stop()

    def wait_and_save_hping3(self):
        if self.hping3_process_runner:
            self.hping3_process_runner.thread.join()

        full_output = self.hping3_output_textbox.get("1.0", "end")
        reporting.log_tool_output(self.current_session_name, "Hping3 Scan", full_output, output_format="txt")
        
        self.master.after(0, self._reset_hping3_buttons)

    def _reset_hping3_buttons(self):
        self.hping3_start_button.configure(state="normal")
        self.hping3_stop_button.configure(state="disabled")
        self._update_status_label(self.hping3_status_label, "Completado.", "green")
        self.hping3_progress_bar.stop()

    def _build_tshark_command(self, interface, capture_filter, output_file, duration_str, selected_output_format):
        """Helper method to build the tshark command and determine the log file format."""
        command = ["sudo", "tshark", "-i", interface]
        log_file_format = "txt"

        if capture_filter:
            command.extend(["-f", capture_filter])
        
        if output_file:
            command.extend(["-w", output_file])
        
        if duration_str:
            try:
                duration = int(duration_str)
                if duration > 0:
                    command.extend(["-a", f"duration:{duration}"])
            except ValueError:
                # Handle error upstream where inputs are collected
                return None, None # Indicate error
        
        if selected_output_format == "JSON":
            command.extend(["-T", "json",
                            "-e", "frame.number",
                            "-e", "frame.time",
                            "-e", "_ws.col.Protocol",
                            "-e", "_ws.col.Source",
                            "-e", "_ws.col.Destination",
                            "-e", "_ws.col.Length",
                            "-e", "_ws.col.Info"])
            if output_file:
                command.extend(["-l"])
            else: 
                command.extend(["-P"])
            log_file_format = "json"
        else: # Default to Text output
            if not output_file:
                command.append("-V")
            log_file_format = "txt"
        
        return command, log_file_format

    def setup_tshark_tab(self):
        tab = self.tab_view.tab("Tshark")
        tab.grid_columnconfigure(0, weight=1)
        tab.grid_rowconfigure(4, weight=1)

        # ---- Input Controls Frame ----
        input_controls_frame = ctk.CTkFrame(tab)
        input_controls_frame.grid(row=0, column=0, padx=10, pady=10, sticky="ew")
        input_controls_frame.grid_columnconfigure(0, weight=1)
        input_controls_frame.grid_columnconfigure(1, weight=2)

        # Interface
        self.tshark_interface_label = ctk.CTkLabel(input_controls_frame, text="Interfaz (ej: eth0, wlan0):")
        self.tshark_interface_label.grid(row=0, column=0, padx=10, pady=8, sticky="w")
        self.tshark_interface_entry = ctk.CTkEntry(input_controls_frame, placeholder_text="ej: eth0")
        self.tshark_interface_entry.grid(row=0, column=1, padx=10, pady=8, sticky="ew")

        # Capture Filter
        self.tshark_filter_label = ctk.CTkLabel(input_controls_frame, text="Filtro de Captura (ej: 'port 80', 'host 192.168.1.1'):")
        self.tshark_filter_label.grid(row=1, column=0, padx=10, pady=8, sticky="w")
        self.tshark_filter_entry = ctk.CTkEntry(input_controls_frame, placeholder_text="ej: tcp port 80")
        self.tshark_filter_entry.grid(row=1, column=1, padx=10, pady=8, sticky="ew")

        # Output File
        cap_file_frame = ctk.CTkFrame(input_controls_frame, fg_color="transparent")
        cap_file_frame.grid(row=2, column=0, columnspan=2, padx=5, pady=5, sticky="ew")
        cap_file_frame.grid_columnconfigure(0, weight=1)

        self.tshark_output_file_entry = ctk.CTkEntry(cap_file_frame, placeholder_text="Archivo de salida .pcap (opcional, ej: capture.pcap)")
        self.tshark_output_file_entry.grid(row=0, column=0, padx=(5, 5), sticky="ew")
        self.tshark_output_file_button = ctk.CTkButton(cap_file_frame, text="Guardar como...", command=self.select_tshark_output_file, width=120)
        self.tshark_output_file_button.grid(row=0, column=1, padx=(10, 0))

        # Capture Duration
        self.tshark_duration_label = ctk.CTkLabel(input_controls_frame, text="Duración (segundos, 0 para continuo):")
        self.tshark_duration_label.grid(row=3, column=0, padx=10, pady=8, sticky="w")
        self.tshark_duration_entry = ctk.CTkEntry(input_controls_frame, placeholder_text="ej: 30")
        self.tshark_duration_entry.grid(row=3, column=1, padx=10, pady=8, sticky="ew")

        # Output Format
        self.tshark_output_format_label = ctk.CTkLabel(input_controls_frame, text="Formato de Salida:")
        self.tshark_output_format_label.grid(row=4, column=0, padx=10, pady=8, sticky="w")
        self.tshark_output_format_option = ctk.CTkOptionMenu(input_controls_frame, values=["Texto", "JSON"])
        self.tshark_output_format_option.set("Texto")
        self.tshark_output_format_option.grid(row=4, column=1, padx=10, pady=8, sticky="ew")

        # ---- Action Buttons Frame ----
        action_buttons_frame = ctk.CTkFrame(tab)
        action_buttons_frame.grid(row=1, column=0, padx=10, pady=10, sticky="ew")
        action_buttons_frame.grid_columnconfigure(0, weight=1)
        action_buttons_frame.grid_columnconfigure(1, weight=1)

        self.tshark_start_button = ctk.CTkButton(action_buttons_frame, text="Iniciar Captura Tshark", command=self.start_tshark_capture)
        self.tshark_start_button.grid(row=0, column=0, padx=10, pady=12, sticky="ew")

        self.tshark_stop_button = ctk.CTkButton(action_buttons_frame, text="Detener Captura", command=self.stop_tshark_capture, fg_color="red", state="disabled")
        self.tshark_stop_button.grid(row=0, column=1, padx=10, pady=12, sticky="ew")

        # ---- Status and Progress Indicators ----
        self.tshark_status_label = ctk.CTkLabel(tab, text="Listo", font=self.text_font)
        self.tshark_status_label.grid(row=2, column=0, padx=10, pady=(5, 0), sticky="w")
        
        self.tshark_progress_bar = ctk.CTkProgressBar(tab, mode="indeterminate")
        self.tshark_progress_bar.grid(row=3, column=0, padx=10, pady=(0, 5), sticky="ew")
        self.tshark_progress_bar.set(0) # Hide it initially

        # ---- Output ----
        self.tshark_output_textbox = ctk.CTkTextbox(tab, state="disabled", font=self.monospace_font)
        self.tshark_output_textbox.grid(row=4, column=0, padx=10, pady=10, sticky="nsew")

    def select_tshark_output_file(self):
        filepath = filedialog.asksaveasfilename(title="Guardar captura como...",defaultextension=".pcap",
                                                filetypes=[("PCAP files", "*.pcap"), ("All files", "*.*")])
        if filepath:
            self.tshark_output_file_entry.delete(0, "end")
            self.tshark_output_file_entry.insert(0, filepath)

    def start_tshark_capture(self):
        interface = self.tshark_interface_entry.get()
        capture_filter = self.tshark_filter_entry.get()
        output_file = self.tshark_output_file_entry.get()
        duration_str = self.tshark_duration_entry.get()
        selected_output_format = self.tshark_output_format_option.get()

        if not interface:
            self._update_status_label(self.tshark_status_label, "Error: Interfaz no especificada.", "red")
            return
        # Basic interface name validation (alphanumeric, -, .)
        if not all(c.isalnum() or c in '.-' for c in interface):
            self._update_status_label(self.tshark_status_label, "Error: Nombre de interfaz inválido.", "red")
            return
        
        sanitized_capture_filter = self._sanitize_cli_input(capture_filter)
        sanitized_output_file = self._sanitize_cli_input(output_file) # Sanitize output file path too

        if duration_str and not self._is_valid_int_range(duration_str, min_val=0):
            self._update_status_label(self.tshark_status_label, "Error: Duración inválida.", "red")
            return

        command, log_file_format = self._build_tshark_command(interface, sanitized_capture_filter, sanitized_output_file, duration_str, selected_output_format)
        if command is None: # Error during command building (e.g., invalid duration)
            # _build_tshark_command already sets status label for invalid duration, but we double check here
            self._update_status_label(self.tshark_status_label, "Error en la construcción del comando Tshark.", "red")
            self.stop_tshark_capture() # This will reset buttons and stop progress bar
            return

        self._tshark_current_log_format = log_file_format # Store for wait_and_save_tshark

        self.tshark_start_button.configure(state="disabled")
        self.tshark_stop_button.configure(state="normal")
        
        self._update_status_label(self.tshark_status_label, "Iniciando captura Tshark...", "orange")
        self.tshark_progress_bar.start()

        self.current_session_name = reporting.create_session(f"tshark_capture_{interface}")
        if self.set_session_callback:
            self.set_session_callback(self.current_session_name)
        
        self.tshark_output_textbox.configure(state="normal")
        self.tshark_output_textbox.delete("1.0", "end")
        self.tshark_output_textbox.insert("1.0", f"--- Iniciando captura Tshark en {interface} ---\nComando: {' '.join(command)}\n\n")
        self.tshark_output_textbox.configure(state="disabled")

        self.tshark_process_runner = ProcessRunner(
            command,
            self.tshark_output_textbox,
            status_label=self.tshark_status_label,
            progress_bar=self.tshark_progress_bar,
            requires_sudo=True # Tshark commands require sudo
        )
        self.tshark_process_runner.start()
        
        threading.Thread(target=self.wait_and_save_tshark, daemon=True).start()

    def stop_tshark_capture(self):
        if self.tshark_process_runner:
            self.tshark_process_runner.stop()
            self.tshark_start_button.configure(state="normal")
            self.tshark_stop_button.configure(state="disabled")
            self._update_status_label(self.tshark_status_label, "Detenido.", "red")
            self.tshark_progress_bar.stop()

    def wait_and_save_tshark(self):
        if self.tshark_process_runner:
            self.tshark_process_runner.thread.join()

        full_output = self.tshark_output_textbox.get("1.0", "end")
        reporting.log_tool_output(self.current_session_name, "Tshark Capture", full_output, output_format=self._tshark_current_log_format)
        
        self.master.after(0, self._reset_tshark_buttons)

    def _reset_tshark_buttons(self):
        self.tshark_start_button.configure(state="normal")
        self.tshark_stop_button.configure(state="disabled")
        self._update_status_label(self.tshark_status_label, "Completado.", "green")
        self.tshark_progress_bar.stop()