import sys
import json
import subprocess
import threading
import queue
import os
import time
import select
from datetime import datetime

# Queue for communication from main thread to helper threads
command_queue = queue.Queue()
# Queue for communication from helper threads to main thread
result_queue = queue.Queue()
# Event to signal all helper threads to exit
exit_event = threading.Event()
# Event specific to stopping the tethering monitor
tethering_exit_event = threading.Event()

# --- Global state for tethering ---
tethering_process = None
tethering_monitor_thread = None
current_camera_port = None
# ---

def run_command(command):
    """Runs a short-lived gphoto2 command and returns the output or error."""
    print(f"Executing gphoto2 command: {' '.join(command)}", file=sys.stderr)
    try:
        result = subprocess.run(command, capture_output=True, text=True, check=True)
        print(f"gphoto2 stdout:\n{result.stdout}", file=sys.stderr)
        print(f"gphoto2 stderr:\n{result.stderr}", file=sys.stderr)
        return {"status": "success", "output": result.stdout.strip()}
    except subprocess.CalledProcessError as e:
        print(f"gphoto2 command failed: {e}", file=sys.stderr)
        print(f"gphoto2 stdout:\n{e.stdout}", file=sys.stderr)
        print(f"gphoto2 stderr:\n{e.stderr}", file=sys.stderr)
        return {"status": "error", "message": str(e), "stdout": e.stdout.strip(), "stderr": e.stderr.strip()}
    except FileNotFoundError:
        print("gphoto2 command not found. Is gphoto2 installed and in the PATH?", file=sys.stderr)
        return {"status": "error", "message": "gphoto2 command not found."}
    except Exception as e:
        print(f"An unexpected error occurred: {e}", file=sys.stderr)
        return {"status": "error", "message": str(e)}

def start_tethering(port, filename_template):
    """Starts the persistent gphoto2 tethering process."""
    global tethering_process
    # Corrected: --port is not needed because gphoto2 auto-detects perfectly
    command = ['gphoto2', '--capture-tethered', '--filename', filename_template]
    print(f"Starting tethering: {' '.join(command)}", file=sys.stderr)
    try:
        # Start the process, capturing stdout/stderr
        tethering_process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1)
        return tethering_process
    except Exception as e:
        print(f"Failed to start tethering process: {e}", file=sys.stderr)
        result_queue.put({"status": "error", "message": f"Failed to start tethering: {e}"})
        return None

def stop_tethering():
    """Stops the tethering process and monitoring thread."""
    global tethering_process, tethering_monitor_thread, current_camera_port
    print("Stopping tethering...", file=sys.stderr)
    tethering_exit_event.set() # Signal monitor thread to exit

    if tethering_monitor_thread and tethering_monitor_thread.is_alive():
        print("Waiting for tethering monitor thread to join...", file=sys.stderr)
        tethering_monitor_thread.join(timeout=2) # Wait for thread
        if tethering_monitor_thread.is_alive():
             print("Warning: Tethering monitor thread did not exit cleanly.", file=sys.stderr)
        tethering_monitor_thread = None

    if tethering_process:
        print("Terminating gphoto2 tethering process...", file=sys.stderr)
        tethering_process.terminate() # Send SIGTERM
        try:
            tethering_process.wait(timeout=2) # Wait for termination
            print("Tethering process terminated.", file=sys.stderr)
        except subprocess.TimeoutExpired:
            print("Tethering process did not terminate gracefully, killing...", file=sys.stderr)
            tethering_process.kill() # Force kill
            try:
                tethering_process.wait(timeout=1)
            except subprocess.TimeoutExpired:
                 print("Warning: Failed to kill tethering process.", file=sys.stderr)
        tethering_process = None

    current_camera_port = None # Clear port
    tethering_exit_event.clear() # Reset event for next connection
    print("Tethering stopped.", file=sys.stderr)


def monitor_tethering_output():
    """Thread function to monitor the stdout of the tethering process."""
    global tethering_process, result_queue
    print("Tethering monitor thread started.", file=sys.stderr)
    # time.sleep(0.01) # Removed tiny sleep

    # Use select for non-blocking read on stdout/stderr
    process = tethering_process
    streams = [process.stdout, process.stderr]

    while not tethering_exit_event.is_set():
        try:
            # Wait for data on stdout/stderr or timeout - Reduced timeout
            readable, _, _ = select.select(streams, [], [], 0.02) # Reduced from 0.1

            for stream in readable:
                # Read line normally
                line = stream.readline()
                if not line: # Stream closed (often indicates process ended or disconnected)
                    if stream is process.stdout:
                        print("Tethering stdout stream closed.", file=sys.stderr)
                    else:
                        print("Tethering stderr stream closed.", file=sys.stderr)
                    # Assume disconnection if a stream closes unexpectedly
                    if not tethering_exit_event.is_set():
                         print("Stream closed unexpectedly, sending disconnect message.", file=sys.stderr)
                         result_queue.put({"status": "disconnected", "message": "Tethering stream closed unexpectedly."})
                    tethering_exit_event.set() # Signal exit
                    break

                line = line.strip()
                if not line: continue # Check if the stripped line is empty

                if stream is process.stdout:
                    # Removed explicit ignore for 'Waiting for events...' line

                    # Don't print normal stdout to stderr. Only process relevant lines.
                    if "Saving file as" in line:
                        try:
                            captured_filepath = line.split("Saving file as")[-1].strip()
                            print(f"Detected captured file: {captured_filepath}", file=sys.stderr)
                            # Change ownership back to the original user
                            try:
                                uid = int(os.environ.get('SUDO_UID', -1))
                                gid = int(os.environ.get('SUDO_GID', -1))
                                if uid != -1 and gid != -1:
                                    os.chown(captured_filepath, uid, gid)
                                    print(f"Changed ownership of {captured_filepath} to UID={uid}, GID={gid}", file=sys.stderr)
                                else:
                                    print("Warning: SUDO_UID or SUDO_GID not found. Cannot change file ownership.", file=sys.stderr)
                            except Exception as chown_e:
                                print(f"Error changing ownership of {captured_filepath}: {chown_e}", file=sys.stderr)
                                # Report error but continue if chown fails?
                            result_queue.put({"status": "event_captured", "filepath": captured_filepath})
                        except Exception as e:
                            print(f"Error parsing captured file path from stdout: {e}", file=sys.stderr)
                            result_queue.put({"status": "error", "message": f"Error parsing captured file path: {e}"})
                    # Add other stdout parsing if needed
                elif stream is process.stderr:
                     # Print actual stderr lines for debugging purposes
                     print(f"Tethering stderr: {line}", file=sys.stderr)
                     # Check for critical errors indicating disconnection (case-insensitive)
                     lower_line = line.lower()
                     if "ptp i/o error" in lower_line or \
                        ("port " in lower_line and " not found" in lower_line) or \
                        "unknown port" in lower_line or \
                        "could not claim the usb device" in lower_line or \
                        "error (-7: 'i/o problem')" in lower_line: # Added specific check for I/O problem error
                         print(f"Critical tethering error detected: {line}. Assuming disconnection.", file=sys.stderr)
                         if not tethering_exit_event.is_set(): # Avoid duplicate messages if already stopping
                            result_queue.put({"status": "disconnected", "message": f"Camera disconnected or error: {line}"})
                         tethering_exit_event.set() # Signal exit
                         break # Exit inner loop
                     # Report other potential errors? Maybe less critical ones?
                     # elif "error" in lower_line:
                     #    result_queue.put({"status": "warning", "message": f"Tethering stderr: {line}"})


            if tethering_exit_event.is_set():
                 print("Tethering monitor received exit signal, breaking loop.", file=sys.stderr)
                 break # Exit outer loop if signaled

        except select.error as e:
             # Handle potential errors from select itself, e.g., bad file descriptor if process closes abruptly
             print(f"Select error in monitor_tethering_output: {e}", file=sys.stderr)
             if not tethering_exit_event.is_set():
                 result_queue.put({"status": "disconnected", "message": f"Tethering monitoring error (select): {e}"})
             tethering_exit_event.set()
             break
        except Exception as e:
            print(f"Error in monitor_tethering_output loop: {e}", file=sys.stderr)
            result_queue.put({"status": "error", "message": f"Error monitoring tethering: {e}"})
            tethering_exit_event.set() # Exit on unexpected error
            break

    # Cleanup after loop exits (either normally or due to error/signal)
    print("Tethering monitor loop finished. Checking process status...", file=sys.stderr)
    process_was_running = bool(process)
    final_return_code = None

    if process:
        # Close streams safely
        try: process.stdout.close()
        except IOError: pass
        try: process.stderr.close()
        except IOError: pass

        # Check final process status
        final_return_code = process.poll()
        if final_return_code is None:
             # Process still running? Should have been terminated by stop_tethering if exit was clean.
             print("Warning: Tethering process still running after monitor loop exit. Attempting termination.", file=sys.stderr)
             try:
                 process.terminate()
                 process.wait(timeout=1)
                 final_return_code = process.poll()
                 if final_return_code is None:
                     process.kill()
                     final_return_code = process.poll()
             except Exception as term_e:
                 print(f"Error during final termination attempt: {term_e}", file=sys.stderr)

        # Report disconnection if process ended unexpectedly (non-zero code)
        # and we weren't already signaled to exit cleanly (e.g., by DISCONNECT command)
        # Report disconnection if process ended unexpectedly (non-zero code)
        # Send the message even if the event was set by stderr parsing or stream closure,
        # let the receiver handle potential duplicates.
        if final_return_code is not None and final_return_code != 0:
             print(f"Tethering process ended unexpectedly with code {final_return_code}. Sending disconnect message.", file=sys.stderr)
             result_queue.put({"status": "disconnected", "message": f"Tethering process ended unexpectedly (code: {final_return_code})."})
        elif final_return_code is not None:
             # Process exited cleanly (code 0)
             print(f"Tethering process exited cleanly with code {final_return_code}", file=sys.stderr)
             # Ensure disconnect message is sent if not already signaled by DISCONNECT command
             if not exit_event.is_set(): # Check main exit event, not just tethering
                  if not tethering_exit_event.is_set(): # Double check tethering event wasn't set just before poll
                    print("Tethering process exited cleanly but unexpectedly. Sending disconnect.", file=sys.stderr)
                    result_queue.put({"status": "disconnected", "message": "Tethering process ended."})
        else: # final_return_code is None
             print("Tethering process status could not be determined after loop exit.", file=sys.stderr)


    print("Tethering monitor thread finished.", file=sys.stderr) # Note: This outer function finishes quickly now


# Helper function for monitor threads
def _monitor_stream(stream, stream_name, process_pid):
    global result_queue, tethering_exit_event
    print(f"Monitor thread started for {stream_name} (PID: {process_pid}).", file=sys.stderr)
    try:
        for line in iter(stream.readline, ''):
            if tethering_exit_event.is_set():
                print(f"Monitor thread for {stream_name} exiting due to event.", file=sys.stderr)
                break
            line = line.strip()
            if not line: continue

            # print(f"DEBUG: Raw {stream_name} line read: '{line}'", file=sys.stderr) # Optional debug

            if stream_name == "stdout":
                if "Saving file as" in line:
                    captured_filepath = None
                    try:
                        captured_filepath = line.split("Saving file as")[-1].strip()
                        print(f"Detected captured file path: {captured_filepath}", file=sys.stderr)
                    except Exception as e:
                        print(f"Error parsing captured file path from stdout: {e}", file=sys.stderr)
                        result_queue.put({"status": "error", "message": f"Error parsing captured file path: {e}"})
                        continue # Skip processing if path parsing failed

                    if captured_filepath:
                        # Wait briefly for the file to appear, with a timeout
                        file_exists = False
                        max_wait_time = 2.0 # seconds
                        start_wait = time.time()
                        while time.time() - start_wait < max_wait_time:
                            if os.path.exists(captured_filepath):
                                file_exists = True
                                print(f"File {captured_filepath} confirmed to exist.", file=sys.stderr)
                                break
                            time.sleep(0.05) # Check every 50ms

                        if not file_exists:
                            print(f"Error: File {captured_filepath} did not appear within {max_wait_time} seconds.", file=sys.stderr)
                            result_queue.put({"status": "error", "message": f"File {captured_filepath} not found after capture."})
                            continue # Skip chown and event reporting for this file

                        # Change ownership back to the original user ONLY if file exists
                        try:
                            uid = int(os.environ.get('SUDO_UID', -1))
                            gid = int(os.environ.get('SUDO_GID', -1))
                            if uid != -1 and gid != -1:
                                os.chown(captured_filepath, uid, gid)
                                print(f"Changed ownership of {captured_filepath} to UID={uid}, GID={gid}", file=sys.stderr)
                                # Ownership changed successfully, now report the event
                                result_queue.put({"status": "event_captured", "filepath": captured_filepath})
                            else:
                                # Not running via sudo or env vars not set. Cannot chown.
                                # The main app likely won't be able to move the file. Report error.
                                print(f"Warning: Cannot determine original user (SUDO_UID/GID not set). File {captured_filepath} owned by root.", file=sys.stderr)
                                result_queue.put({"status": "error", "message": f"Cannot change ownership of {captured_filepath}. File may be inaccessible."})

                        except Exception as chown_e:
                            print(f"Error changing ownership of {captured_filepath}: {chown_e}", file=sys.stderr)
                            # Report error and do NOT send event_captured, as controller can't handle it
                            result_queue.put({"status": "error", "message": f"Failed to change ownership of {captured_filepath}: {chown_e}"})

                # Add other stdout parsing if needed (e.g., specific event types)

            elif stream_name == "stderr":
                 print(f"Tethering stderr: {line}", file=sys.stderr)
                 # Check for critical errors indicating disconnection (case-insensitive)
                 lower_line = line.lower()
                 if "ptp i/o error" in lower_line or \
                    ("port " in lower_line and " not found" in lower_line) or \
                    "unknown port" in lower_line or \
                    "could not claim the usb device" in lower_line or \
                    "error (-7: 'i/o problem')" in lower_line:
                     print(f"Critical tethering error detected: {line}. Assuming disconnection.", file=sys.stderr)
                     if not tethering_exit_event.is_set(): # Avoid duplicate messages if already stopping
                        result_queue.put({"status": "disconnected", "message": f"Camera disconnected or error: {line}"})
                     tethering_exit_event.set() # Signal exit for all monitor threads
                     break # Exit this loop

    except ValueError:
        # This can happen if the stream is closed while readline is waiting
        if not tethering_exit_event.is_set():
             print(f"Stream {stream_name} closed unexpectedly (ValueError).", file=sys.stderr)
             result_queue.put({"status": "disconnected", "message": f"Tethering stream {stream_name} closed unexpectedly."})
        tethering_exit_event.set()
    except Exception as e:
        # Handle other exceptions during read
        if not tethering_exit_event.is_set():
            print(f"Error reading from {stream_name}: {e}", file=sys.stderr)
            result_queue.put({"status": "disconnected", "message": f"Error reading stream {stream_name}: {e}"})
        tethering_exit_event.set()
    finally:
        print(f"Monitor loop for {stream_name} finished.", file=sys.stderr)
        # Ensure exit event is set if one stream closes/errors
        tethering_exit_event.set()


def monitor_tethering_output():
    """Sets up and waits for threads monitoring gphoto2 output."""
    global tethering_process, tethering_monitor_thread # tethering_monitor_thread is no longer used here
    process = tethering_process
    if not process: return

    print("Starting separate monitor threads for stdout/stderr.", file=sys.stderr)
    stdout_thread = threading.Thread(target=_monitor_stream, args=(process.stdout, "stdout", process.pid))
    stderr_thread = threading.Thread(target=_monitor_stream, args=(process.stderr, "stderr", process.pid))

    stdout_thread.daemon = True
    stderr_thread.daemon = True
    stdout_thread.start()
    stderr_thread.start()

    # Wait for the exit event (set by stream closure, error, or stop_tethering)
    tethering_exit_event.wait()
    print("Tethering monitor detected exit signal. Waiting for stream threads...", file=sys.stderr)

    # Wait briefly for monitor threads to finish processing last lines
    stdout_thread.join(timeout=0.5)
    stderr_thread.join(timeout=0.5)
    if stdout_thread.is_alive(): print("Warning: stdout monitor thread did not join cleanly.", file=sys.stderr)
    if stderr_thread.is_alive(): print("Warning: stderr monitor thread did not join cleanly.", file=sys.stderr)

    # --- Final process check ---
    # This check should ideally happen after stop_tethering ensures termination,
    # but we add a safety check here in case the monitor exited unexpectedly.
    final_return_code = process.poll()
    if final_return_code is None:
         print("Warning: Tethering process still running after monitor exit. Should be handled by stop_tethering.", file=sys.stderr)
         # Let stop_tethering handle the termination.
    elif final_return_code != 0:
         # Report disconnection if process ended unexpectedly (non-zero code)
         # and we weren't already signaled to exit cleanly (e.g., by DISCONNECT command)
         # Check if a disconnect message was already sent by _monitor_stream
         # This logic might need refinement to avoid duplicates if stop_tethering also sends one.
         print(f"Tethering process ended unexpectedly with code {final_return_code} after monitor exit.", file=sys.stderr)
         # Consider if another disconnect message is needed here. For now, assume _monitor_stream or stop_tethering handles it.
    else: # final_return_code == 0
         print(f"Tethering process exited cleanly with code {final_return_code} after monitor exit.", file=sys.stderr)

    print("Tethering monitoring coordination finished.", file=sys.stderr)


def process_commands():
    """Thread function to process commands from the queue."""
    global current_camera_port, tethering_process, tethering_monitor_thread

    while not exit_event.is_set():
        try:
            # Wait for a command indefinitely, but check exit_event frequently
            command_data = command_queue.get(timeout=0.1)
        except queue.Empty:
            # No command, just loop and check exit_event
            continue

        command_type = command_data.get("command")
        payload = command_data.get("payload")
        print(f"Received command: {command_type}", file=sys.stderr)

        if command_type == "SCAN":
            # Ensure tethering is stopped before scanning
            if tethering_process:
                print("Stopping existing tethering before scan...", file=sys.stderr)
                stop_tethering()
            result = run_command(['gphoto2', '--auto-detect'])
            if result["status"] == "success":
                try:
                    cameras = []
                    output_lines = result["output"].splitlines()
                    if len(output_lines) > 2: # Skip header lines
                        for line in output_lines[2:]:
                            parts = line.strip().split()
                            if len(parts) >= 2:
                                port = parts[-1]
                                model = line[:line.rfind(port)].strip() # Model is everything before the port
                                if model and port.startswith('usb:'): # Basic validation
                                    cameras.append({"model": model, "port": port})
                    result_queue.put({"status": "scan_complete", "cameras": cameras})
                except Exception as e:
                     print(f"Error parsing scan results: {e}\nOutput was:\n{result['output']}", file=sys.stderr)
                     result_queue.put({"status": "error", "message": f"Error parsing scan results: {e}"})
            else:
                result_queue.put(result) # Pass the error result

        elif command_type == "CONNECT":
            if tethering_process:
                 print("Already tethering. Disconnect first.", file=sys.stderr)
                 result_queue.put({"status": "error", "message": "Already tethering. Disconnect first."})
                 continue

            port = payload.get("port")
            filename_template = payload.get("filename_template", '/tmp/gphoto2_capture_%Y%m%d%H%M%S_%f.%C') # Default template

            if port:
                # Optional: Check connection validity first (e.g., with --summary)
                check_result = run_command(['gphoto2', '--port', port, '--summary'])
                if check_result["status"] == "error":
                    result_queue.put(check_result)
                    continue # Don't proceed if summary fails

                # Start the tethering process
                process = start_tethering(port, filename_template)
                if process:
                    current_camera_port = port # Store the connected port
                    # Start the monitoring thread
                    tethering_monitor_thread = threading.Thread(target=monitor_tethering_output)
                    tethering_monitor_thread.daemon = True # Allow main thread to exit even if this fails
                    tethering_monitor_thread.start()
                    result_queue.put({"status": "connected", "port": port})
                # else: start_tethering already put an error on the queue
            else:
                result_queue.put({"status": "error", "message": "CONNECT command requires 'port' payload."})

        elif command_type == "DISCONNECT":
            if tethering_process:
                stop_tethering()
                result_queue.put({"status": "disconnected"})
            else:
                result_queue.put({"status": "error", "message": "Not currently tethering."})


        elif command_type == "CAPTURE":
            # This command uses --capture-image-and-download, which should NOT be run
            # while a tethering session (--capture-tethered) is active.
            if tethering_process:
                result_queue.put({"status": "error", "message": "Cannot capture image while tethering is active. Disconnect first."})
            else:
                # Rely on gphoto2 auto-detection for the camera.
                # Use a specific filename if provided in the payload.
                filename_template = None # Default to None
                if payload: # Check if payload exists before accessing it
                    filename_template = payload.get("filename_template", None) # Optional override

                cmd = ['gphoto2', '--capture-image-and-download']
                if filename_template:
                    # Note: The template here might include path components.
                    # Ensure the target directory exists or handle potential errors.
                    # For simplicity, assuming the path is valid for now.
                    cmd.extend(['--filename', filename_template])

                result = run_command(cmd)
                if result["status"] == "success":
                    output_lines = result.get("output", "").splitlines()
                    captured_filepath = None
                    # Find the line indicating the saved file
                    for line in output_lines:
                        if "Saving file as" in line:
                            try:
                                captured_filepath = line.split("Saving file as")[-1].strip()
                                print(f"Detected captured file via command output: {captured_filepath}", file=sys.stderr)
                                break # Found it
                            except Exception as e:
                                print(f"Error parsing captured file path from command output: {e}", file=sys.stderr)
                                result_queue.put({"status": "error", "message": f"Capture successful, but error parsing file path: {e}"})
                                captured_filepath = None # Reset on error
                                break

                    if captured_filepath:
                        # Change ownership back to the original user if run via sudo
                        try:
                            uid = int(os.environ.get('SUDO_UID', -1))
                            gid = int(os.environ.get('SUDO_GID', -1))
                            if uid != -1 and gid != -1:
                                os.chown(captured_filepath, uid, gid)
                                print(f"Changed ownership of {captured_filepath} to UID={uid}, GID={gid}", file=sys.stderr)
                            # else: Not running via sudo or env vars not set, skip chown
                        except Exception as chown_e:
                            print(f"Warning: Error changing ownership of {captured_filepath}: {chown_e}", file=sys.stderr)
                            # Report error but still report success with the path
                        result_queue.put({"status": "capture_complete", "filepath": captured_filepath})
                    elif "New file is located on the camera" in result.get("output", ""):
                         # Should ideally not happen with --capture-image-and-download, but handle defensively
                         print("Capture stored on camera (download did not occur as expected).", file=sys.stderr)
                         result_queue.put({"status": "capture_initiated_on_camera"})
                    else:
                         # Command succeeded but couldn't find the filepath in the output
                         print("Capture command successful, but could not determine saved file path from output.", file=sys.stderr)
                         result_queue.put({"status": "capture_command_sent", "message": "Capture command sent, but file path not detected in output."})
                else:
                    # Pass the error result from run_command
                    result_queue.put(result)

        # Removed WAIT_EVENT command case

        elif command_type == "EXIT":
            print("Helper received EXIT command.", file=sys.stderr)
            stop_tethering() # Stop tethering if active
            exit_event.set() # Signal main loop and other threads to exit
            break # Exit the command processing loop

        else:
            result_queue.put({"status": "error", "message": f"Unknown command: {command_type}"})

        command_queue.task_done() # Mark the command as processed

    print("Command processor thread finished.", file=sys.stderr)


def read_commands():
    """Reads commands from stdin using select for non-blocking checks and puts them in the command queue."""
    print("Command reader thread started. Helper ready.", file=sys.stderr)
    while not exit_event.is_set():
        # Use select to check if stdin has data or if we should check the exit event
        readable, _, _ = select.select([sys.stdin], [], [], 0.1) # Timeout allows checking exit_event

        if readable:
            line = sys.stdin.readline()
            if not line: # stdin closed
                print("Command reader detected stdin closed.", file=sys.stderr)
                break # Exit loop if stdin is closed

            try:
                command_data = json.loads(line)
                command_queue.put(command_data)
            except json.JSONDecodeError:
                print(f"Invalid JSON received: {line.strip()}", file=sys.stderr)
                result_queue.put({"status": "error", "message": "Invalid JSON received."})
            except Exception as e:
                print(f"Error reading/parsing command from stdin: {e}", file=sys.stderr)
                result_queue.put({"status": "error", "message": f"Error reading command: {e}"})
                # Consider breaking here if errors are frequent or critical
        # No need for an else clause, the loop continues and checks exit_event

    print("Command reader thread finished (stdin closed or exit event).", file=sys.stderr)
    # Ensure the main exit event is set if stdin closes unexpectedly or loop finishes
    exit_event.set()


def send_results():
    """Sends results from the result queue to stdout."""
    print("Result sender thread started.", file=sys.stderr)
    while not exit_event.is_set() or not result_queue.empty():
        try:
            result = result_queue.get(timeout=0.1) # Check exit_event periodically
            print(json.dumps(result), flush=True)
            result_queue.task_done()
        except queue.Empty:
            continue # No results, check exit_event again
        except Exception as e:
             print(f"Error sending result: {e}", file=sys.stderr)
             # Continue trying?

    print("Result sender thread finished.", file=sys.stderr)

if __name__ == "__main__":
    print("Starting gphoto_helper...", file=sys.stderr)

    # Ensure any previous tethering is stopped on startup (e.g., if script crashed)
    # This might require knowing the process ID, which we don't have.
    # Relying on clean shutdown/disconnect for now.

    # Start threads
    command_processor_thread = threading.Thread(target=process_commands, name="CommandProcessor")
    result_sender_thread = threading.Thread(target=send_results, name="ResultSender")
    command_reader_thread = threading.Thread(target=read_commands, name="CommandReader")
    # command_reader_thread.daemon = True # No longer needed with select

    command_processor_thread.start()
    result_sender_thread.start()
    command_reader_thread.start()

    # Wait for the main exit event (set by EXIT command or stdin close)
    exit_event.wait()
    print("Main thread detected exit event.", file=sys.stderr)

    # Ensure tethering is stopped cleanly
    if tethering_process:
        print("Main thread ensuring tethering is stopped...", file=sys.stderr)
        stop_tethering()

    # Wait for threads to finish
    print("Waiting for threads to join...", file=sys.stderr)
    command_reader_thread.join(timeout=1) # Give reader a chance to exit cleanly
    command_processor_thread.join(timeout=2)
    result_sender_thread.join(timeout=2)

    # Check if threads are still alive
    if command_reader_thread.is_alive(): print("Warning: Command reader thread did not join.", file=sys.stderr)
    if command_processor_thread.is_alive(): print("Warning: Command processor thread did not join.", file=sys.stderr)
    if result_sender_thread.is_alive(): print("Warning: Result sender thread did not join.", file=sys.stderr)


    # Final cleanup check for the process, though stop_tethering should handle it
    if tethering_process and tethering_process.poll() is None:
        print("Warning: Tethering process still running after cleanup attempts. Killing.", file=sys.stderr)
        tethering_process.kill()

    print("gphoto_helper finished.", file=sys.stderr)
    sys.exit(0)
