#!/usr/bin/env python3
"""
Session Module - vzug-e-hinge
==============================
Orchestrates session execution with queued pause/stop behavior.

This module coordinates the entire automated test session flow:
- Loading interface and session profiles from database
- Opening UART/I2C ports based on configuration
- Executing command sequences via run.py
- Managing pause/stop requests (queued after current run)
- Providing real-time status updates via Qt signals
- Handling session lifecycle (create, pause, resume, abort)

Key Features:
- Queued pause/stop (executes AFTER current run completes)
- Countdown during delays with second-by-second updates
- Real-time timestamp correlation (UART/I2C)
- Automatic port management
- Comprehensive error handling
- Database integration with status tracking

Author: Kynsight
Version: 2.0.0
Date: 2025-11-09
"""

# =============================================================================
# IMPORTS
# =============================================================================

from PyQt6.QtCore import QObject, pyqtSignal
from typing import Optional, Dict, List, Any
import sqlite3
import json
import time
from datetime import datetime

# UART core
from uart.uart_kit.uart_core import (
    UARTPort,
    UARTConfig,
    PacketConfig,
    uart_create,
    uart_open,
    uart_close,
    uart_start_reader,
    uart_stop_reader,
    Status as UARTStatus
)

# I2C core
from i2c.i2c_kit.i2c_core import (
    I2CHandle,
    I2CConfig,
    i2c_create,
    i2c_open,
    i2c_close,
    Status as I2CStatus
)

# Run executor
from run import execute_run, execute_i2c_command

# Database manager
from database.init_database import DatabaseManager


# =============================================================================
# SESSION CLASS
# =============================================================================

class Session(QObject):
    """
    Session orchestration class.
    
    Manages the complete lifecycle of an automated test session:
    1. Load profiles (interface + session) from database
    2. Open hardware ports (UART + I2C)
    3. Execute command sequence
    4. Handle pause/stop requests (queued)
    5. Provide real-time status updates
    6. Close ports and finalize session
    
    Signals:
        session_started: Emitted when session starts (session_id)
        command_started: Emitted when command starts (command_no, command_name)
        run_completed: Emitted when run completes (run_no, packet_count)
        delay_countdown: Emitted during delay countdown (seconds_remaining)
        session_paused: Emitted when session pauses
        session_finished: Emitted when session completes successfully
        error_occurred: Emitted on error (error_message)
        status_changed: Emitted on status change (status_text)
    """
    
    # =========================================================================
    # SIGNALS (for GUI updates)
    # =========================================================================
    
    session_started = pyqtSignal(str)              # session_id
    command_started = pyqtSignal(int, str)         # command_no, command_name
    run_completed = pyqtSignal(int, int)           # run_no, packet_count
    delay_countdown = pyqtSignal(int)              # seconds_remaining
    session_paused = pyqtSignal()
    session_finished = pyqtSignal()
    error_occurred = pyqtSignal(str)               # error_message
    status_changed = pyqtSignal(str)               # status_text
    raw_data_received = pyqtSignal(str, str)       # direction (TX/RX), hex_string
    
    # =========================================================================
    # CONSTRUCTOR
    # =========================================================================
    
    def __init__(self, db_manager: DatabaseManager):
        """
        Initialize session manager.
        
        Args:
            db_manager: Database manager instance
        """
        super().__init__()
        
        # Database connection
        self.db_manager = db_manager
        self.db_conn = db_manager.get_connection()
        
        # Session state
        self.current_session_id: Optional[str] = None
        self.session_name: Optional[str] = None
        self.interface_profile_id: Optional[int] = None
        self.session_profile_id: Optional[int] = None
        
        # Hardware ports (managed by this class)
        self.uart_command_port: Optional[UARTPort] = None  # For sending commands (TX/RX)
        self.uart_logger_port: Optional[UARTPort] = None   # For telemetry packets (RX only, optional)
        self.i2c_handle: Optional[I2CHandle] = None
        self.packet_config: Optional[PacketConfig] = None
        
        # Phase execution (multi-phase support: Init, Execute, De-init)
        self.phases: List[Dict[str, Any]] = []  # List of phase configs
        self.total_commands: int = 0  # Total commands across all phases
        self.total_uart_runs: int = 0  # Total UART commands (actual runs)
        self.total_i2c_commands: int = 0  # Total I2C commands (not runs)
        self.current_command_index: int = 0  # Global command counter
        self.current_phase_name: str = ""  # Current executing phase name

        # I2C zero reference (persists across all runs in session)
        self.i2c_zero_reference: int = 0  # Absolute angle used as zero (0 = not zeroed)

        # Execution control flags
        self.is_running: bool = False
        self.is_paused: bool = False
        self.pause_queued: bool = False
        self.stop_queued: bool = False
    
    # =========================================================================
    # PROFILE LOADING
    # =========================================================================
    
    def load_session(
        self,
        interface_profile_id: int,
        init_session_id: Optional[int] = None,
        execute_session_id: Optional[int] = None,
        deinit_session_id: Optional[int] = None,
        session_name: Optional[str] = None
    ) -> bool:
        """
        Load session and interface profiles from database with 3 phases.

        This method:
        1. Reads interface_profile (UART/I2C config, packet detection)
        2. Reads up to 3 session_profiles (Init, Execute, De-init)
        3. Parses command sequences for each phase
        4. Validates all commands exist in uart_commands table
        5. Stores configuration for execution

        Args:
            interface_profile_id: ID from interface_profiles table
            init_session_id: ID for Init phase (optional, None to skip)
            execute_session_id: ID for Execute phase (optional, None to skip)
            deinit_session_id: ID for De-init phase (optional, None to skip)
            session_name: Custom session name (auto-generated if None)

        Returns:
            True if successful, False on error
        """
        try:
            # ===================================================================
            # 1. Load interface profile (UART/I2C configuration)
            # ===================================================================
            
            cursor = self.db_conn.execute("""
                SELECT
                    profile_name,
                    uart_command_port, uart_command_baud, uart_command_data_bits,
                    uart_command_stop_bits, uart_command_parity, uart_command_timeout_ms,
                    uart_logger_enable, uart_logger_port, uart_logger_baud, uart_logger_data_bits,
                    uart_logger_stop_bits, uart_logger_parity, uart_logger_timeout_ms,
                    uart_logger_grace_ms,
                    uart_logger_packet_detect_enable, uart_logger_packet_detect_start,
                    uart_logger_packet_detect_length, uart_logger_packet_detect_end,
                    i2c_port, i2c_slave_address, i2c_slave_read_register, i2c_slave_read_length
                FROM interface_profiles
                WHERE profile_id = ?
            """, (interface_profile_id,))
            
            row = cursor.fetchone()
            if not row:
                self.error_occurred.emit(f"Interface profile {interface_profile_id} not found")
                return False
            
            # Store interface configuration
            self.interface_profile_id = interface_profile_id
            self.interface_config = {
                'profile_name': row[0],
                # UART Command interface
                'uart_command_port': row[1],
                'uart_command_baud': row[2],
                'uart_command_data_bits': row[3],
                'uart_command_stop_bits': row[4],
                'uart_command_parity': row[5],
                'uart_command_timeout_ms': row[6],
                # UART Logger interface
                'uart_logger_enable': bool(row[7]) if row[7] is not None else True,
                'uart_logger_port': row[8],
                'uart_logger_baud': row[9],
                'uart_logger_data_bits': row[10],
                'uart_logger_stop_bits': row[11],
                'uart_logger_parity': row[12],
                'uart_logger_timeout_ms': row[13],
                'uart_logger_grace_ms': row[14],
                # Packet detection
                'packet_detect_enable': row[15],
                'packet_detect_start': row[16],
                'packet_detect_length': row[17],
                'packet_detect_end': row[18],
                # I2C configuration
                'i2c_port': row[19],
                'i2c_slave_address': row[20],
                'i2c_slave_read_register': row[21],
                'i2c_slave_read_length': row[22]
            }
            
            # ===================================================================
            # 2. Load session profiles (up to 3 phases: Init, Execute, De-init)
            # ===================================================================

            # Storage for phase configurations
            self.phases = []  # List of dicts: {'name': str, 'commands': list, 'profile_id': int}
            self.total_commands = 0
            self.total_uart_runs = 0
            self.total_i2c_commands = 0

            # Load Init phase
            if init_session_id is not None:
                success, phase_config = self._load_phase_profile(init_session_id, "Init")
                if not success:
                    return False
                if phase_config:
                    self.phases.append(phase_config)
                    self.total_commands += len(phase_config['commands'])
                    # Count by type
                    for cmd in phase_config['commands']:
                        if cmd.get('command_type', 'uart') == 'i2c':
                            self.total_i2c_commands += 1
                        else:
                            self.total_uart_runs += 1

            # Load Execute phase
            if execute_session_id is not None:
                success, phase_config = self._load_phase_profile(execute_session_id, "Execute")
                if not success:
                    return False
                if phase_config:
                    self.phases.append(phase_config)
                    self.total_commands += len(phase_config['commands'])
                    # Count by type
                    for cmd in phase_config['commands']:
                        if cmd.get('command_type', 'uart') == 'i2c':
                            self.total_i2c_commands += 1
                        else:
                            self.total_uart_runs += 1

            # Load De-init phase
            if deinit_session_id is not None:
                success, phase_config = self._load_phase_profile(deinit_session_id, "De-init")
                if not success:
                    return False
                if phase_config:
                    self.phases.append(phase_config)
                    self.total_commands += len(phase_config['commands'])
                    # Count by type
                    for cmd in phase_config['commands']:
                        if cmd.get('command_type', 'uart') == 'i2c':
                            self.total_i2c_commands += 1
                        else:
                            self.total_uart_runs += 1

            # Check at least one phase loaded
            if len(self.phases) == 0:
                self.error_occurred.emit("No session phases selected")
                return False

            # ===================================================================
            # 3. Generate session name if not provided
            # ===================================================================

            if session_name is None or session_name.strip() == "":
                timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
                phase_names = "_".join([p['name'] for p in self.phases])
                self.session_name = f"{phase_names}_{timestamp}"
            else:
                self.session_name = session_name

            # ===================================================================
            # 4. Emit success status
            # ===================================================================

            phase_summary = ", ".join([f"{p['name']}({len(p['commands'])} cmds)" for p in self.phases])
            self.status_changed.emit(f"Multi-phase session loaded: {self.session_name}")
            self.status_changed.emit(f"Interface: {self.interface_config['profile_name']}")
            self.status_changed.emit(f"Phases: {phase_summary}")
            self.status_changed.emit(f"Total: {self.total_uart_runs} UART runs, {self.total_i2c_commands} I2C commands")
            
            return True
            
        except Exception as e:
            self.error_occurred.emit(f"Failed to load session: {str(e)}")
            return False

    def _load_phase_profile(self, profile_id: int, phase_name: str) -> tuple[bool, Optional[dict]]:
        """
        Load a single session profile for a phase.

        Args:
            profile_id: Session profile ID
            phase_name: Phase name ("Init", "Execute", or "De-init")

        Returns:
            (success, phase_config) where phase_config is:
            {'name': str, 'commands': list, 'profile_id': int, 'profile_name': str}
            or None if profile has no commands
        """
        try:
            cursor = self.db_conn.execute("""
                SELECT profile_name, command_sequence, description, print_command_rx
                FROM session_profiles
                WHERE profile_id = ?
            """, (profile_id,))

            row = cursor.fetchone()
            if not row:
                self.error_occurred.emit(f"{phase_name} session profile {profile_id} not found")
                return (False, None)

            profile_name = row[0]
            command_sequence_json = row[1]
            print_command_rx = bool(row[3]) if len(row) > 3 else False

            # Parse JSON command sequence
            try:
                command_sequence = json.loads(command_sequence_json)
                commands = command_sequence.get('commands', [])
            except json.JSONDecodeError as e:
                self.error_occurred.emit(f"{phase_name}: Invalid JSON - {str(e)}")
                return (False, None)

            # Empty profile is okay (skip phase)
            if len(commands) == 0:
                self.status_changed.emit(f"{phase_name} phase has no commands, skipping")
                return (True, None)

            # Validate all commands exist in database
            for cmd in commands:
                cmd_id = cmd.get('command_id')
                if not cmd_id:
                    self.error_occurred.emit(f"{phase_name}: Command missing command_id")
                    return (False, None)

                # Check command type (default to 'uart' for backwards compatibility)
                command_type = cmd.get('command_type', 'uart')

                if command_type == 'i2c':
                    # Load I2C command from database
                    cursor = self.db_conn.execute("""
                        SELECT command_name, operation, register, hex_string, device_address
                        FROM i2c_commands
                        WHERE command_id = ?
                    """, (cmd_id,))

                    row = cursor.fetchone()
                    if not row:
                        self.error_occurred.emit(f"{phase_name}: I2C command {cmd_id} not found")
                        return (False, None)

                    # Store I2C command details
                    cmd['command_name'] = row[0]
                    cmd['operation'] = row[1]
                    cmd['register'] = row[2]
                    cmd['hex_string'] = row[3]
                    cmd['device_address'] = row[4]
                else:
                    # Load UART command from database (default)
                    cursor = self.db_conn.execute("""
                        SELECT command_name, hex_string
                        FROM uart_commands
                        WHERE command_id = ?
                    """, (cmd_id,))

                    row = cursor.fetchone()
                    if not row:
                        self.error_occurred.emit(f"{phase_name}: UART command {cmd_id} not found")
                        return (False, None)

                    # Store UART command details
                    cmd['command_name'] = row[0]
                    cmd['hex_string'] = row[1]

            # Create phase config
            phase_config = {
                'name': phase_name,
                'profile_name': profile_name,
                'profile_id': profile_id,
                'commands': commands,
                'print_command_rx': print_command_rx
            }

            return (True, phase_config)

        except Exception as e:
            self.error_occurred.emit(f"{phase_name}: Failed to load - {str(e)}")
            return (False, None)

    # =========================================================================
    # PORT MANAGEMENT
    # =========================================================================
    
    def _open_ports(self) -> bool:
        """
        Open UART and I2C ports based on interface configuration.
        
        This method:
        1. Opens UART Command port (TX/RX - for sending commands, always needed)
        2. Opens UART Logger port (RX - for telemetry, optional)
        3. Opens I2C port (for angle readings)
        4. Creates PacketConfig from interface profile
        
        Returns:
            True if successful, False on error
        """
        try:
            # ===================================================================
            # 1. Open UART Command Port (TX/RX - ALWAYS NEEDED)
            # ===================================================================
            
            # Create UART command config
            cmd_uart_config = UARTConfig(
                device=self.interface_config['uart_command_port'],
                baudrate=self.interface_config['uart_command_baud'],
                data_bits=self.interface_config['uart_command_data_bits'],
                stop_bits=self.interface_config['uart_command_stop_bits'],
                parity=self.interface_config['uart_command_parity'],
                buffer_size=40 * 1024 * 1024,  # 40MB buffer
                stop_timeout_ms=self.interface_config['uart_command_timeout_ms']
            )
            
            # Create UART command port
            status, self.uart_command_port = uart_create(cmd_uart_config)
            
            if status != UARTStatus.OK or self.uart_command_port is None:
                self.error_occurred.emit(f"Failed to create UART command port")
                return False
            
            # Open UART command port
            status = uart_open(self.uart_command_port)
            
            if status != UARTStatus.OK:
                self.error_occurred.emit(f"Failed to open UART command port {cmd_uart_config.device}")
                return False
            
            # Start UART command reader thread
            status = uart_start_reader(self.uart_command_port)
            if status != UARTStatus.OK:
                uart_close(self.uart_command_port)
                self.uart_command_port = None
                self.error_occurred.emit("Failed to start UART command reader")
                return False
            
            self.status_changed.emit(f"UART Command opened: {cmd_uart_config.device}")
            
            # ===================================================================
            # 2. Open UART Logger Port (RX - OPTIONAL)
            # ===================================================================
            
            if self.interface_config['uart_logger_enable']:
                # Logger enabled - open logger port

                # Create UART logger config
                log_uart_config = UARTConfig(
                    device=self.interface_config['uart_logger_port'],
                    baudrate=self.interface_config['uart_logger_baud'],
                    data_bits=self.interface_config['uart_logger_data_bits'],
                    stop_bits=self.interface_config['uart_logger_stop_bits'],
                    parity=self.interface_config['uart_logger_parity'],
                    buffer_size=40 * 1024 * 1024,  # 40MB buffer
                    stop_timeout_ms=self.interface_config['uart_logger_timeout_ms'],
                    grace_timeout_ms=self.interface_config['uart_logger_grace_ms'],
                    polling_mode=True  # Enable grace period for first byte
                )
                
                # Create UART logger port
                status, self.uart_logger_port = uart_create(log_uart_config)
                
                if status != UARTStatus.OK or self.uart_logger_port is None:
                    self.error_occurred.emit(f"Failed to create UART logger port")
                    # Close command port before returning
                    uart_stop_reader(self.uart_command_port)
                    uart_close(self.uart_command_port)
                    self.uart_command_port = None
                    return False
                
                # Open UART logger port
                status = uart_open(self.uart_logger_port)
                
                if status != UARTStatus.OK:
                    self.error_occurred.emit(f"Failed to open UART logger port {log_uart_config.device}")
                    # Close command port before returning
                    uart_stop_reader(self.uart_command_port)
                    uart_close(self.uart_command_port)
                    self.uart_command_port = None
                    return False
                
                # Start UART logger reader thread
                status = uart_start_reader(self.uart_logger_port)
                if status != UARTStatus.OK:
                    uart_close(self.uart_logger_port)
                    self.uart_logger_port = None
                    # Close command port before returning
                    uart_stop_reader(self.uart_command_port)
                    uart_close(self.uart_command_port)
                    self.uart_command_port = None
                    self.error_occurred.emit("Failed to start UART logger reader")
                    return False
                
                self.status_changed.emit(f"UART Logger opened: {log_uart_config.device}")
            else:
                # Logger disabled - skip logger port
                self.uart_logger_port = None
                self.status_changed.emit("UART Logger disabled (command port only)")
            
            # ===================================================================
            # 3. Create PacketConfig from interface profile
            # ===================================================================

            # Check if ANY phase has print_command_rx enabled (session profile override)
            any_print_rx = any(phase.get('print_command_rx', False) for phase in self.phases)
            if any_print_rx:
                # Force packet detection OFF - just print TX/RX to data monitor
                self.packet_config = PacketConfig(enable=False)
            elif not self.interface_config['uart_logger_enable']:
                # Logger disabled - no packet detection
                self.packet_config = PacketConfig(enable=False)
            elif self.interface_config['packet_detect_enable']:
                # Parse hex markers
                start_marker = bytes.fromhex(
                    self.interface_config['packet_detect_start'].replace(' ', '')
                ) if self.interface_config['packet_detect_start'] else None
                
                end_marker = bytes.fromhex(
                    self.interface_config['packet_detect_end'].replace(' ', '')
                ) if self.interface_config['packet_detect_end'] else None
                
                self.packet_config = PacketConfig(
                    enable=True,
                    start_marker=start_marker,
                    packet_length=self.interface_config['packet_detect_length'],
                    end_marker=end_marker,
                    on_packet_callback=None  # Will be set by run.py
                )
            else:
                self.packet_config = PacketConfig(enable=False)
            
            # ===================================================================
            # 4. Open I2C Port (optional - for angle readings)
            # ===================================================================

            if self.interface_config['i2c_port']:
                # Parse I2C address

                # Create I2C config
                i2c_config = I2CConfig(
                    bus_id=int(self.interface_config["i2c_port"]),
                    buffer_size=40 * 1024 * 1024  # 40MB buffer
                )
                
                # Create I2C handle
                status, self.i2c_handle = i2c_create(i2c_config)
                
                if status != I2CStatus.OK or self.i2c_handle is None:
                    # I2C is optional, just warn
                    self.status_changed.emit("Warning: Could not create I2C handle")
                    self.i2c_handle = None
                else:
                    # Open I2C
                    status = i2c_open(self.i2c_handle)
                    
                    if status != I2CStatus.OK:
                        # I2C is optional, just warn
                        self.status_changed.emit("Warning: I2C port not available")
                        self.i2c_handle = None
                    else:
                        self.status_changed.emit(f"I2C opened: bus {self.interface_config['i2c_port']}")
            
            return True

        except Exception as e:
            self.error_occurred.emit(f"Failed to open ports: {str(e)}")
            self._close_ports()
            return False

    def _close_ports(self):
        """
        Close UART and I2C ports.
        
        Called at end of session or on error.
        Ensures clean shutdown of hardware interfaces.
        """
        try:
            # Close UART Command port
            if self.uart_command_port:
                uart_stop_reader(self.uart_command_port)
                uart_close(self.uart_command_port)
                self.uart_command_port = None
                self.status_changed.emit("UART Command closed")
            
            # Close UART Logger port
            if self.uart_logger_port:
                uart_stop_reader(self.uart_logger_port)
                uart_close(self.uart_logger_port)
                self.uart_logger_port = None
                self.status_changed.emit("UART Logger closed")
            
            # Close I2C
            if self.i2c_handle:
                i2c_close(self.i2c_handle)
                self.i2c_handle = None
                self.status_changed.emit("I2C closed")
                
        except Exception as e:
            self.error_occurred.emit(f"Error closing ports: {str(e)}")
    
    # =========================================================================
    # SESSION EXECUTION
    # =========================================================================
    
    def start_session(self) -> bool:
        """
        Start session execution.
        
        This method:
        1. Creates session record in database
        2. Opens hardware ports
        3. Executes command loop
        4. Handles delays with countdown
        5. Checks pause/stop queue between runs
        6. Finalizes session on completion
        
        Returns:
            True if started successfully, False on error
        """
        if self.is_running:
            self.error_occurred.emit("Session already running")
            return False

        if not hasattr(self, 'phases') or len(self.phases) == 0:
            self.error_occurred.emit("No session loaded")
            return False
        
        try:
            # ===================================================================
            # 1. Create session record in database
            # ===================================================================
            
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            self.current_session_id = f"session_{timestamp}"
            session_date = datetime.now().strftime("%Y-%m-%d")

            # NOTE: Database schema currently supports single session_profile_id
            # We use the first phase's profile_id as the primary reference
            # All phases and their profiles are tracked in self.phases list
            first_phase_profile_id = self.phases[0]['profile_id'] if len(self.phases) > 0 else None

            self.db_conn.execute("""
                INSERT INTO sessions (
                    session_id, session_name, session_date,
                    interface_profile_id, session_profile_id,
                    status, total_runs
                ) VALUES (?, ?, ?, ?, ?, 'active', 0)
            """, (
                self.current_session_id,
                self.session_name,
                session_date,
                self.interface_profile_id,
                first_phase_profile_id
            ))

            self.db_conn.commit()
            
            # ===================================================================
            # 2. Open hardware ports
            # ===================================================================
            
            if not self._open_ports():
                self._finalize_session('error')
                return False
            
            # ===================================================================
            # 3. Set execution state
            # ===================================================================

            self.is_running = True
            self.is_paused = False
            self.pause_queued = False
            self.stop_queued = False
            self.current_command_index = 0
            self.i2c_zero_reference = 0  # Reset zero reference for new session

            # Emit session started
            self.session_started.emit(self.current_session_id)
            self.status_changed.emit(f"Session started: {self.session_name}")
            
            # ===================================================================
            # 4. Execute command loop
            # ===================================================================
            
            self._execute_command_loop()
            
            return True
            
        except Exception as e:
            self.error_occurred.emit(f"Failed to start session: {str(e)}")
            self._finalize_session('error')
            return False
    
    def _execute_command_loop(self):
        """
        Execute all commands in sequence across multiple phases.

        This is the main execution loop that:
        1. Iterates through all phases (Init, Execute, De-init)
        2. Iterates through commands within each phase
        3. Calls run.py for each command
        4. Handles delays with countdown
        5. Checks pause/stop queue between runs
        6. Updates database and emits signals

        CRITICAL: Pause/Stop are QUEUED and only execute between runs
        during the delay phase, never during a run itself. Stop cancels
        ALL remaining phases.
        """
        try:
            global_cmd_index = 0  # Track UART run number
            total_commands_executed = 0  # Track all commands (UART + I2C)

            # Loop through phases
            for phase_index, phase in enumerate(self.phases, 1):
                phase_name = phase['name']
                self.current_phase_name = phase_name  # Track current phase
                phase_commands = phase['commands']
                total_phases = len(self.phases)

                # Emit phase started
                self.status_changed.emit(f"Starting Phase {phase_index}/{total_phases}: {phase_name}")

                # Loop through commands in this phase
                for cmd_index_in_phase, cmd in enumerate(phase_commands, 1):
                    total_commands_executed += 1  # Count all commands
                    # ===============================================================
                    # 1. Check if stop was queued (before starting new command)
                    # ===============================================================

                    if self.stop_queued:
                        self.status_changed.emit("Session stopped by user")
                        self._finalize_session('aborted')
                        return

                    # ===============================================================
                    # 2. Check command type - I2C commands don't count as runs
                    # ===============================================================

                    command_type = cmd.get('command_type', 'uart')
                    command_name = cmd['command_name']

                    # Only increment run number for UART commands
                    if command_type == 'uart':
                        global_cmd_index += 1
                        self.current_command_index = global_cmd_index

                    # ===============================================================
                    # 3. Emit command started
                    # ===============================================================

                    if command_type == 'i2c':
                        # I2C command - formatted output: [Phase] [I2C] [INFO] message
                        self.raw_data_received.emit(
                            f"[{phase_name}] [I2C] [INFO]",
                            f"Command: {command_name}"
                        )
                    else:
                        # UART command - formatted output: [Phase] [UART] [INFO] message
                        self.command_started.emit(global_cmd_index, command_name)
                        self.raw_data_received.emit(
                            f"[{phase_name}] [UART] [INFO]",
                            f"Run {global_cmd_index}: {command_name}"
                        )

                    # ===============================================================
                    # 4. Execute command (UART or I2C) via run.py
                    # ===============================================================

                    if command_type == 'i2c':
                        # Execute I2C command (doesn't count as run, no telemetry saved)
                        # Callback formats as: [Phase] [I2C] [Action] message
                        status, packet_count, error_msg, updated_zero_ref = execute_i2c_command(
                            db_connection=self.db_conn,
                            session_id=self.current_session_id,
                            session_name=self.session_name,
                            run_no=0,  # Not a run
                            command_id=cmd['command_id'],
                            command_name=cmd['command_name'],
                            operation=cmd.get('operation', 'read'),
                            device_address=int(cmd.get('device_address', '0x40'), 16),
                            register=int(cmd.get('register', '0xFE'), 16),
                            hex_string=cmd.get('hex_string', '02'),
                            i2c_port=self.i2c_handle,
                            i2c_zero_ref=self.i2c_zero_reference,
                            raw_data_callback=lambda action, msg: self.raw_data_received.emit(
                                f"[{phase_name}] [I2C] [{action}]", msg
                            )
                        )
                        # Update session's zero reference (may have changed if zero command)
                        self.i2c_zero_reference = updated_zero_ref
                    else:
                        # Execute UART command (counts as run, saves telemetry)
                        # Callback formats as: [Phase] [UART] [Action] message
                        status, packet_count, error_msg = execute_run(
                            db_connection=self.db_conn,
                            session_id=self.current_session_id,
                            session_name=self.session_name,
                            run_no=global_cmd_index,
                            command_id=cmd['command_id'],
                            command_hex=cmd['hex_string'],
                            uart_command_port=self.uart_command_port,
                            uart_logger_port=self.uart_logger_port,
                            i2c_port=self.i2c_handle,
                            packet_config=self.packet_config,
                            i2c_address=int(self.interface_config['i2c_slave_address'], 16) if self.interface_config.get('i2c_slave_address') else 0x40,
                            i2c_register=int(self.interface_config['i2c_slave_read_register'], 16) if self.interface_config.get('i2c_slave_read_register') else 0xFE,
                            stop_timeout_ms=self.interface_config['uart_logger_timeout_ms'],
                            grace_timeout_ms=self.interface_config['uart_logger_grace_ms'],
                            i2c_zero_ref=self.i2c_zero_reference,
                            raw_data_callback=lambda action, msg: self.raw_data_received.emit(
                                f"[{phase_name}] [UART] [{action}]", msg
                            )
                        )

                    # ===============================================================
                    # 5. Handle command result
                    # ===============================================================

                    if status == "error":
                        # Command failed - abort session (all phases)
                        if command_type == 'i2c':
                            self.error_occurred.emit(f"[{phase_name}] I2C command '{command_name}' failed: {error_msg}")
                        else:
                            self.error_occurred.emit(f"[{phase_name}] Run {global_cmd_index} failed: {error_msg}")
                        self._finalize_session('error')
                        return

                    # Command succeeded
                    if command_type == 'uart':
                        # UART run - emit completion and update database
                        self.run_completed.emit(global_cmd_index, packet_count)
                        self.raw_data_received.emit(
                            f"[{phase_name}] [UART] [INFO]",
                            f"Complete: {packet_count} packets"
                        )

                        # Update total runs in database
                        self.db_conn.execute("""
                            UPDATE sessions SET total_runs = ? WHERE session_id = ?
                        """, (global_cmd_index, self.current_session_id))
                        self.db_conn.commit()
                    else:
                        # I2C command - just log completion (no run counting)
                        self.raw_data_received.emit(
                            f"[{phase_name}] [I2C] [INFO]",
                            f"Complete"
                        )
                
                    # ===============================================================
                    # 5. Delay between commands (with countdown and queue check)
                    # ===============================================================

                    # Get delay from command (default 3000ms if not specified)
                    delay_ms = cmd.get('delay_ms', 3000)

                    # Only delay if not last command overall
                    if total_commands_executed < self.total_commands:
                        self._execute_delay(delay_ms)

                        # Check if pause/stop was queued during delay
                        if self.pause_queued:
                            self._finalize_session('paused')
                            self.is_paused = True
                            self.session_paused.emit()
                            self.status_changed.emit("Session paused")
                            return

                        if self.stop_queued:
                            self._finalize_session('aborted')
                            self.status_changed.emit("Session stopped by user")
                            return

                # Phase completed
                self.status_changed.emit(f"Phase {phase_index}/{total_phases} completed: {phase_name}")
            
            # ===================================================================
            # 6. All phases completed successfully
            # ===================================================================

            self._finalize_session('completed')
            self.session_finished.emit()
            phase_list = ", ".join([p['name'] for p in self.phases])
            self.status_changed.emit(f"All phases completed successfully: {phase_list}")
            
        except Exception as e:
            self.error_occurred.emit(f"Exception during session: {str(e)}")
            self._finalize_session('error')
    
    def _execute_delay(self, delay_ms: int):
        """
        Execute delay with countdown.
        
        Emits delay_countdown signal every second so GUI can display
        countdown timer. Also checks pause/stop queue each second.
        
        Args:
            delay_ms: Delay in milliseconds
        """
        # Convert to seconds
        delay_sec = delay_ms / 1000.0
        
        # Countdown in 1-second steps
        for remaining in range(int(delay_sec), 0, -1):
            # Emit countdown
            self.delay_countdown.emit(remaining)
            self.status_changed.emit(f"Waiting... ({remaining}s remaining)")
            
            # Sleep for 1 second
            time.sleep(1.0)
            
            # Check if pause/stop was queued
            if self.pause_queued or self.stop_queued:
                return  # Exit delay early
        
        # Handle fractional second at end
        fractional = delay_sec - int(delay_sec)
        if fractional > 0:
            time.sleep(fractional)
    
    def _finalize_session(self, status: str):
        """
        Finalize session and cleanup.
        
        Args:
            status: 'completed', 'paused', 'aborted', or 'error'
        """
        try:
            # Update session status in database
            self.db_conn.execute("""
                UPDATE sessions 
                SET status = ?, ended_at = datetime('now')
                WHERE session_id = ?
            """, (status, self.current_session_id))
            
            self.db_conn.commit()
            
            # Close hardware ports
            self._close_ports()
            
            # Reset execution state (unless paused)
            if status != 'paused':
                self.is_running = False
                self.current_session_id = None
            
        except Exception as e:
            self.error_occurred.emit(f"Error finalizing session: {str(e)}")
    
    # =========================================================================
    # PAUSE/RESUME/STOP CONTROL
    # =========================================================================
    
    def pause_session(self):
        """
        Queue pause request.
        
        CRITICAL: Pause is QUEUED and executes AFTER current run completes.
        User can press pause anytime, but it only takes effect during
        the delay phase between runs.
        """
        if not self.is_running or self.is_paused:
            return
        
        self.pause_queued = True
        self.status_changed.emit("Pause queued (will execute after current run)")
    
    def resume_session(self):
        """
        Resume paused session.
        
        Continues execution from where it left off, immediately proceeding
        to the next command without delay.
        """
        if not self.is_paused:
            return
        
        # Clear pause flags
        self.is_paused = False
        self.pause_queued = False
        
        # Update database
        self.db_conn.execute("""
            UPDATE sessions SET status = 'active' WHERE session_id = ?
        """, (self.current_session_id,))
        self.db_conn.commit()
        
        # Reopen ports
        if not self._open_ports():
            self.error_occurred.emit("Failed to reopen ports")
            return
        
        # Resume command loop from where we left off
        self.status_changed.emit("Session resumed")
        self._execute_command_loop()
    
    def stop_session(self):
        """
        Queue stop request.
        
        CRITICAL: Stop is QUEUED and executes AFTER current run completes.
        User can press stop anytime, but it only takes effect during
        the delay phase between runs.
        """
        if not self.is_running:
            return
        
        self.stop_queued = True
        self.status_changed.emit("Stop queued (will execute after current run)")
    
    # =========================================================================
    # STATUS QUERIES
    # =========================================================================
    
    def get_current_session_id(self) -> Optional[str]:
        """Get current session ID."""
        return self.current_session_id
    
    def get_session_name(self) -> Optional[str]:
        """Get current session name."""
        return self.session_name
    
    def is_session_running(self) -> bool:
        """Check if session is running."""
        return self.is_running
    
    def is_session_paused(self) -> bool:
        """Check if session is paused."""
        return self.is_paused


# =============================================================================
# MAIN (for testing)
# =============================================================================

if __name__ == "__main__":
    print("Session Module - vzug-e-hinge")
    print("=" * 60)
    print()
    print("This module orchestrates automated test sessions.")
    print()
    print("Features:")
    print("  ✓ Profile loading (interface + session)")
    print("  ✓ Automatic port management")
    print("  ✓ Command sequence execution")
    print("  ✓ Queued pause/stop (executes after current run)")
    print("  ✓ Real-time status updates via signals")
    print("  ✓ Comprehensive error handling")
    print()
    print("Usage:")
    print("  from session import Session")
    print("  session = Session(db_manager)")
    print("  session.load_session(interface_id, session_id)")
    print("  session.start_session()")
    print()
    print("Ready to be used by session_widget.py!")
