# Copyright 2026 Espressif Systems (Shanghai) Co., Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import json
import logging
import os
import queue
import re
import threading
import time
from typing import Any, Dict, List, Optional

from esp_viewer.core.chip_info import ChipInfoManager
from esp_viewer.core.protocol import (CommandType, EventType, JsonRpc,
                                      LogLevel, Packet)

logger = logging.getLogger("esp_viewer.backend")


class DeviceService(threading.Thread):
    def __init__(
        self, cmd_queue: queue.Queue, event_queue: queue.Queue, profile: Dict[str, Any]
    ):
        super().__init__()
        self.cmd_queue = cmd_queue
        self.event_queue = event_queue
        self.profile = profile
        self.running = True
        self.connected = False
        self.stream_enabled = False
        self.device_interface = None  # To be injected (Serial or Mock)
        self.current_baud = 115200
        self.is_operation_active = False  # Flag to block readline during esptool ops

        # Load rules from profile
        self.param_map = {p["id"]: p for p in profile.get("parameters", [])}
        self.parsers = profile.get("streams", [])

        # Protocol Configuration
        self.proto_config = profile.get("protocol", {})
        self.methods = self.proto_config.get("methods", {})
        self.notify_methods = self.proto_config.get("notification_methods", {})

        self.req_id_counter = 0
        self.recorders: Dict[str, Any] = {}
        self.raw_io_recorder: Optional[Any] = None
        self.io_logging_enabled = False
        self.current_log_path: Optional[str] = None

    def set_device_interface(self, interface):
        """Inject the driver and initialize recorders."""
        self.device_interface = interface

        # 1. Initialize Stream Recorders
        self.recorders = {}
        self._init_recorders()

        # 2. Reset Data IO Recorder state on re-inject
        if self.raw_io_recorder:
            try:
                self.raw_io_recorder.close()
            except:
                pass
            self.raw_io_recorder = None

        # Report current status to any new UI
        self._emit_io_log_status()

    def _init_recorders(self):
        import os

        for stream in self.profile.get("streams", []):
            if stream.get("log_to_file"):
                sid = stream.get("id", "unknown")
                filename = f"log_{sid}_{int(time.time())}.csv"
                try:
                    f = open(filename, "w")
                    # Write Header if available or just keys?
                    # For now just open it. writing happens in _emit_data
                    self.recorders[sid] = f
                    self._log(LogLevel.INFO, f"Recording stream '{sid}' to {filename}")
                except Exception as e:
                    self._log(LogLevel.ERROR, f"Failed to open recorder for {sid}: {e}")

    def run(self):
        self._log(LogLevel.INFO, "DeviceService Started")

        while self.running:
            # 1. Process Commands
            try:
                cmd = self.cmd_queue.get(timeout=0.01)
                self._handle_command(cmd)
            except queue.Empty:
                pass
            except Exception as e:
                self._log(LogLevel.ERROR, f"DeviceService Command Loop Error: {e}")

            # 2. Read Device Stream
            if self.connected and self.device_interface and not self.is_operation_active:
                try:
                    line = self.device_interface.readline()
                    if line:
                        # Log Raw IO always (capture unstripped for accuracy)
                        self._emit_io("RX", line)
                        self._parse_line(line)
                except Exception as e:
                    self._log(LogLevel.ERROR, f"Read Error: {e}")
                    self.connected = False
                    self._emit_status(False)

            # Reduce CPU usage
            # time.sleep(0.001)

    def stop(self):
        self.running = False
        if self.device_interface:
            self.device_interface.close()
        for f in self.recorders.values():
            if f:
                try:
                    f.close()
                except:
                    pass
        self.recorders.clear()

        if self.raw_io_recorder:
            try:
                self.raw_io_recorder.close()
            except:
                pass
            self.raw_io_recorder = None

    def _handle_command(self, packet: Packet):
        if packet.type == CommandType.CONNECT:
            self.current_baud = packet.payload.get("baud", 115200)
            self._connect()
        elif packet.type == CommandType.DISCONNECT:
            self._disconnect()
        elif packet.type == CommandType.SET_PARAM:
            self._set_param(packet.payload)
        elif packet.type == CommandType.START_STREAM:
            self._start_stream()
        elif packet.type == CommandType.STOP_STREAM:
            self._stop_stream()
        elif packet.type == CommandType.GET_PARAM:
            self._get_param(packet.payload)
        elif packet.type == CommandType.FETCH_PROFILE:
            self._fetch_profile_defs()
        elif packet.type == CommandType.WRITE_DATA:
            self._write_data(packet.payload)
        elif packet.type == CommandType.SET_IO_LOGGING:
            payload = packet.payload
            self._handle_set_io_logging(
                payload.get("enabled", False), payload.get("path")
            )
        elif packet.type == CommandType.GET_CHIP_INFO:
            self._get_chip_info()
        elif packet.type == CommandType.GET_PARTITION_TABLE:
            self._get_partition_table()
        elif packet.type == CommandType.GET_APP_INFO:
            self._get_app_info(packet.payload.get("partitions", []))
        elif packet.type == CommandType.LIST_PORTS:
            self._list_ports()
        elif packet.type == CommandType.READ_FIRMWARE:
            self._read_firmware(packet.payload)
        elif packet.type == CommandType.BACKUP_FIRMWARE:
            self._backup_firmware(packet.payload)
        elif packet.type == CommandType.RESET_STATE:
            self._reset_state()

    def _handle_set_io_logging(self, enabled: bool, path: Optional[str] = None):
        self.io_logging_enabled = enabled

        if enabled:
            if not self.raw_io_recorder or (path and path != self.current_log_path):
                # Close existing if path changed
                if self.raw_io_recorder:
                    try:
                        self.raw_io_recorder.close()
                    except:
                        pass
                    self.raw_io_recorder = None

                try:
                    import os
                    from datetime import datetime

                    if path:
                        log_file = path
                        # Ensure directory exists
                        log_dir = os.path.dirname(log_file)
                        if log_dir:
                            os.makedirs(log_dir, exist_ok=True)
                    else:
                        os.makedirs("Data", exist_ok=True)
                        log_file = os.path.join(
                            "Data", datetime.now().strftime("%Y%m%d_%H%M%S.log")
                        )

                    self.raw_io_recorder = open(log_file, "a", encoding="utf-8")
                    self.current_log_path = log_file
                    self._log(LogLevel.INFO, f"Started Data IO recording to {log_file}")
                except Exception as e:
                    self._log(LogLevel.ERROR, f"Failed to start Data IO recording: {e}")
                    self.io_logging_enabled = False
        else:
            if self.raw_io_recorder:
                try:
                    self.raw_io_recorder.close()
                    self._log(
                        LogLevel.INFO,
                        f"Stopped Data IO recording: {self.current_log_path}",
                    )
                except:
                    pass
                self.raw_io_recorder = None
                self.current_log_path = None

        self._emit_io_log_status()

    def _emit_io_log_status(self):
        self.event_queue.put(
            Packet(
                EventType.IO_LOG_STATUS,
                {"enabled": self.io_logging_enabled, "path": self.current_log_path},
            )
        )

    def _connect(self, silent=False):
        if self.device_interface:
            try:
                self.device_interface.open()
                self.connected = True
                if not silent:
                    self._emit_status(True)
                    self._emit_stream_status()
                self._log(LogLevel.INFO, "Connected to Device")

                # Send ping to verify protocol?
                self._send_json_request(self.methods.get("ping", "ping"))

            except Exception as e:
                self._log(LogLevel.ERROR, f"Connection Failed: {e}")
                self._emit_status(False)

    def _disconnect(self, silent=False):
        if self.device_interface:
            self.device_interface.close()
        self.connected = False
        self.stream_enabled = False
        if not silent:
            self._emit_status(False)
            self._emit_stream_status()
        self._log(LogLevel.INFO, "Disconnected")

    def _reset_state(self):
        """Force reset backend to initial state when UI reloads."""
        self._disconnect()
        self._handle_set_io_logging(False)
        self.req_id_counter = 0
        self._log(LogLevel.INFO, "Backend state reset to initial due to UI refresh.")

    def _write_data(self, payload):
        if not self.connected:
            self._log(LogLevel.WARNING, "Not connected")
            return

        data = payload.get("data")
        is_hex = payload.get("is_hex")

        try:
            if is_hex:
                # Hex String "AA BB"
                raw = bytes.fromhex(data.replace(" ", ""))
                self.device_interface.write_raw(raw)
                self._emit_io("TX", f"[HEX] {raw.hex().upper()}")
            else:
                # String + EOL
                eol = self.profile.get("connection", {}).get("eol", "\n")
                full = data + eol
                self.device_interface.write(full)
                self._emit_io("TX", full)
        except Exception as e:
            self._log(LogLevel.ERROR, f"Write Failed: {e}")

    def _start_stream(self):
        self.stream_enabled = True
        self._emit_stream_status()
        method = self.methods.get("stream_start", "start_stream")
        self._send_json_request(method)

    def _stop_stream(self):
        self.stream_enabled = False
        self._emit_stream_status()
        method = self.methods.get("stream_stop", "stop_stream")
        self._send_json_request(method)

    def _set_param(self, payload: Dict[str, Any]):
        """
        Sends a parameter update command to the device.
        Supports both template-based string commands and JSON-RPC.
        """
        if not self.connected:
            return

        param_id = payload.get("id")
        value = payload.get("value")

        param_def = self.param_map.get(param_id)
        if param_def:
            # 1. Check for set_template (Highest priority)
            if "set_template" in param_def:
                template = param_def["set_template"]
                try:
                    full_cmd = template.replace("{value}", str(value))
                    eol = self.profile.get("connection", {}).get("eol", "\n")
                    full = full_cmd + eol
                    self.device_interface.write(full)
                    self._emit_io("TX", full)
                    self._log(LogLevel.INFO, f"[Control] -> {full_cmd}")
                except Exception as e:
                    self._log(LogLevel.ERROR, f"Template send failed: {e}")
            else:
                # 2. Fallback to JSON-RPC
                method = self.methods.get("set", "set_config")  # Default to set_config
                params = {param_id: value}
                self._send_json_request(method, params)
        else:
            self._log(LogLevel.WARNING, f"Unknown param: {param_id}")

    def _get_param(self, payload):
        if not self.connected:
            return

        param_id = payload.get("id")
        if param_id:
            method = self.methods.get("get", "get_config")
            self._send_json_request(method, {param_id: None})
        else:
            method = self.methods.get("get", "get_config")
            self._send_json_request(method)

    def _fetch_profile_defs(self):
        self._log(LogLevel.INFO, "Fetching full profile definition from device...")
        # Try get_profile first
        method = self.methods.get("get_profile", "get_profile")
        self._send_json_request(method)
        # Fallback to get_config if device doesn't support get_profile?
        # For now, let's assume get_profile is the intended way.

    def _send_json_request(self, method: str, params: Dict[str, Any] = None):
        if not self.connected or not method:
            return

        self.req_id_counter += 1
        req = JsonRpc.create_request(method, params, self.req_id_counter)

        try:
            json_str = json.dumps(req)
            self._log(
                LogLevel.INFO,
                f"[Control] -> {method}({params if params else ''}) [ID:{self.req_id_counter}]",
            )
            eol = self.profile.get("connection", {}).get("eol", "\n")
            full = json_str + eol
            self.device_interface.write(full)
            self._emit_io("TX", full)
        except Exception as e:
            self._log(LogLevel.ERROR, f"JSON Send Failed: {e}")

    def _parse_line(self, line: str):
        line = line.strip()
        if not line:
            return

        # Hybrid Strategy:
        # 1. Check if JSON (Control Plane)
        if line.startswith("{"):
            try:
                data = json.loads(line)
                self._handle_json_message(data)
                return  # Handled as JSON
            except json.JSONDecodeError:
                # Not valid JSON, fall through to stream parser
                pass

        # 2. Stream Parser (Data Plane)
        self._parse_stream_line(line)

    def _handle_json_message(self, data: Dict[str, Any]):
        # JSON-RPC Handling

        # Response?
        if "result" in data or "error" in data:
            # It's a response
            rid = data.get("id")
            res = data.get("result")
            err = data.get("error")
            if err:
                self._log(LogLevel.ERROR, f"[Control] <- Error [ID:{rid}]: {err}")
            else:
                self._log(LogLevel.INFO, f"[Control] <- Response [ID:{rid}]: {res}")

                # Check if this is a response to fetch/get profile
                if isinstance(res, dict):
                    # If it looks like a full profile definition
                    if "parameters" in res and "meta" in res:
                        self._log(
                            LogLevel.INFO,
                            "Received full profile definition from device",
                        )
                        self.profile.update(res)
                        # Re-initialize mappings
                        self.param_map = {
                            p["id"]: p for p in self.profile.get("parameters", [])
                        }
                        self.parsers = self.profile.get("streams", [])
                        # Notify UI that profile definition is fully updated
                        self.event_queue.put(
                            Packet(
                                EventType.PROFILE_UPDATED,
                                {"id": "__full_profile__", "val": json.dumps(res)},
                            )
                        )
                    else:
                        # Treat as standard ID -> Value updates
                        for pid, pval in res.items():
                            if isinstance(pval, dict):
                                val = pval.get("val", str(pval))
                            else:
                                val = str(pval)

                            self.event_queue.put(
                                Packet(
                                    EventType.PROFILE_UPDATED, {"id": pid, "val": val}
                                )
                            )
            return

        # Notification or Request from device?
        elif "method" in data:
            method = data.get("method")
            params = data.get("params", {})

            # Map Notification Methods
            stream_method = self.notify_methods.get("stream_data", "stream_data")
            log_method = self.notify_methods.get("log", "log")

            if method == stream_method:
                # Handle as stream data, but via JSON
                self._emit_data(params)
            elif method == log_method:
                level = params.get("level", "INFO")
                msg = params.get("message", "")
                self._log(
                    LogLevel(level) if level in LogLevel.__members__ else LogLevel.INFO,
                    msg,
                )
            elif method == "ping":
                # Device pinging tool?
                pass

    def _parse_stream_line(self, line: str):
        # Existing Regex/Delimiter Logic
        for parser in self.parsers:
            values = {}
            if "regex" in parser:
                regex = parser["regex"]
                match = re.search(regex, line)
                if match:
                    fields = parser.get("fields", [])
                    if parser.get("type") == "text" and not fields:
                        if match.groups():
                            values["text"] = match.group(1)
                        else:
                            values["text"] = line
                    for i, field_name in enumerate(fields):
                        if i < len(match.groups()):
                            val_str = match.group(i + 1)
                            try:
                                if "." in val_str:
                                    values[field_name] = float(val_str)
                                else:
                                    values[field_name] = int(val_str)
                            except ValueError:
                                values[field_name] = val_str
            elif "delimiter" in parser:
                delimiter = parser["delimiter"]
                parts = line.split(delimiter)
                if "header" in parser:
                    if parts[0] != parser["header"]:
                        continue
                    parts = parts[1:]
                fields = parser.get("fields", [])
                if len(parts) >= len(fields):
                    for i, field_name in enumerate(fields):
                        val_str = parts[i].strip()
                        try:
                            if "." in val_str:
                                values[field_name] = float(val_str)
                            else:
                                values[field_name] = int(val_str)
                        except ValueError:
                            values[field_name] = val_str

            if values:
                # Auto-inject timestamp if missing for "General" usage
                if "ts" not in values and "time" not in values:
                    values["ts"] = int(time.time() * 1000)

                static = parser.get("static_attributes", {})
                values.update(static)
                if "id" in parser:
                    values["_stream_id"] = parser["id"]

                stream_type = parser.get("type", "data")
                if stream_type == "log":
                    msg = f"[{values.get('level', 'INFO')}] {values.get('message', '')}"
                    self._log(LogLevel.INFO, msg)
                elif stream_type == "marker":
                    self.event_queue.put(
                        Packet(EventType.STREAM_DATA, {"marker": values})
                    )
                elif stream_type == "text":
                    self.event_queue.put(
                        Packet(
                            EventType.TEXT_STREAM,
                            {"id": parser["id"], "text": values.get("text", line)},
                        )
                    )
                else:
                    self._emit_data(values)
                return

        # No parser match
        self._log(LogLevel.INFO, f"RAW: {line}")

    def _emit_status(self, connected: bool):
        self.event_queue.put(Packet(EventType.STATUS_CHANGED, {"connected": connected}))

    def _emit_stream_status(self):
        self.event_queue.put(
            Packet(EventType.STREAM_STATUS, {"enabled": self.stream_enabled})
        )

    def _emit_data(self, values: Dict):
        """
        Processes and emits parsed data to the frontend and active recorders.

        Args:
            values: Dictionary containing parsed field values and metadata.
        """
        # Recording Logic
        sid = values.get("_stream_id")
        if sid and sid in self.recorders:
            try:
                line = ",".join(
                    [f"{k}={v}" for k, v in values.items() if not k.startswith("_")]
                )
                self.recorders[sid].write(f"{time.time():.3f},{line}\n")
                self.recorders[sid].flush()
            except Exception as e:
                self._log(LogLevel.ERROR, f"Write error {sid}: {e}")

        if self.stream_enabled:
            self.event_queue.put(Packet(EventType.STREAM_DATA, {"values": values}))

    def _log(self, level: LogLevel, msg: str):
        """Internal logging helper."""
        self.event_queue.put(
            Packet(EventType.LOG_MSG, {"level": level.value, "msg": msg})
        )

        # Write to system logger for crash persistence
        if level == LogLevel.DEBUG:
            logger.debug(msg)
        elif level == LogLevel.INFO:
            logger.info(msg)
        elif level == LogLevel.WARNING:
            logger.warning(msg)
        elif level == LogLevel.ERROR:
            logger.error(msg)

    def _emit_io(self, direction: str, data: str):
        """
        Emits raw IO data to the UI and writes to file if recording is enabled.

        Args:
            direction: "RX" or "TX"
            data: The raw string data
        """
        self.event_queue.put(
            Packet(EventType.IO_DATA, {"dir": direction, "data": data})
        )

        if self.raw_io_recorder:
            try:
                ts = time.strftime("%H:%M:%S")
                # Ensure data ends with a newline in the file if it doesn't already
                content = data if data.endswith("\n") else data + "\n"
                self.raw_io_recorder.write(f"[{ts}] [{direction}] {content}")
                self.raw_io_recorder.flush()
            except:
                pass

    def _get_chip_info(self):
        """Retrieves chip information by temporarily pausing the driver."""
        self._log(LogLevel.INFO, "Starting Chip Info retrieval...")

        # 1. Store state and disconnect
        was_connected = self.connected
        port = None

        if self.device_interface:
            # Try to get port from driver
            if hasattr(self.device_interface, "port"):
                port = self.device_interface.port

            # If mock, just return mock data
            from esp_viewer.core.mock_device import MockDevice

            if isinstance(self.device_interface, MockDevice):
                self._log(LogLevel.INFO, "Simulating Chip Info for Mock Device")

                # Try to load real captured mock data if exists
                # __file__ is esp_viewer/core/backend.py
                mock_path = os.path.abspath(
                    os.path.join(
                        os.path.dirname(__file__), "mock_chip_info.json"
                    )
                )

                if os.path.exists(mock_path):
                    try:
                        with open(mock_path, "r") as f:
                            mock_info = json.load(f)
                        self._log(
                            LogLevel.INFO, f"Loaded real mock data from {mock_path}"
                        )
                    except Exception as e:
                        self._log(
                            LogLevel.WARNING,
                            f"Failed to load mock data from {mock_path}: {e}",
                        )
                        mock_info = self._get_default_mock_info()
                else:
                    mock_info = self._get_default_mock_info()

                self.event_queue.put(Packet(EventType.CHIP_INFO_RECEIVED, mock_info))
                return

            self._disconnect(silent=True)

        if not port:
            self._log(
                LogLevel.ERROR, "Cannot read chip info: Port unknown or device not set"
            )
            self.event_queue.put(Packet(EventType.ERROR, {"msg": "Port unknown"}))
            return

        # 2. Run retrieval
        try:
            manager = ChipInfoManager(port, baudrate=self.current_baud)
            info = manager.get_all_info()
            self.event_queue.put(Packet(EventType.CHIP_INFO_RECEIVED, info))
            self._log(LogLevel.INFO, "Chip Info successfully retrieved")
        except Exception as e:
            self._log(LogLevel.ERROR, f"Failed to retrieve chip info: {e}")
            self.event_queue.put(Packet(EventType.ERROR, {"msg": str(e)}))
        finally:
            # 3. Restore state
            if was_connected:
                self._connect(silent=True)
            if not self.connected and was_connected:
                # If we failed to reconnect silently
                self._emit_status(False)

    def _get_default_mock_info(self) -> Dict[str, Any]:
        return {
            "chip": {
                "model": "ESP32-D0WDQ6",
                "revision": "v1.0",
                "mac": "AC:63:BE:86:12:34",
                "features": "WiFi, BT, Dual Core, 240MHz",
            },
            "flash": {
                "manufacturer_id": "20",
                "device_id": "4016",
                "size": "4MB",
                "status": "0x0200",
            },
            "adc": "ADC VRef calibration: 1100mV",
            "efuse": "EFUSE_SUMMARY: [Simplified for Mock]",
            "security": {
                "flash_encryption": "Disabled",
                "secure_boot": "Disabled",
                "raw": "Flash Encryption: Disabled\nSecure Boot: Disabled",
            },
        }

    def _get_partition_table(self):
        self._log(LogLevel.INFO, "Requesting Partition Table...")
        port = None
        if self.device_interface and hasattr(self.device_interface, "port"):
            port = self.device_interface.port

        # Try various methods to run the tool
        from esp_viewer.core.mock_device import MockDevice

        if isinstance(self.device_interface, MockDevice):
            mock_path = os.path.abspath(
                os.path.join(os.path.dirname(__file__), "mock_chip_info.json")
            )
            if os.path.exists(mock_path):
                try:
                    with open(mock_path, "r") as f:
                        mock_data = json.load(f)
                    pt_data = mock_data.get(
                        "partition_table", {"status": "success", "partitions": []}
                    )
                    self.event_queue.put(
                        Packet(EventType.PARTITION_TABLE_RECEIVED, pt_data)
                    )
                    return
                except:
                    pass
            self.event_queue.put(
                Packet(
                    EventType.PARTITION_TABLE_RECEIVED,
                    {"status": "success", "partitions": []},
                )
            )
            return

        if not port:

            self._log(
                LogLevel.ERROR,
                "Cannot get PT: Unknown port or device not a Serial device",
            )
            self.event_queue.put(Packet(EventType.ERROR, {"msg": "Port unknown"}))
            return

        was_connected = self.connected
        if was_connected:
            self._disconnect(silent=True)

        try:
            from esp_viewer.core.chip_info import ChipInfoManager

            manager = ChipInfoManager(port, baudrate=self.current_baud)
            pt_data = manager.get_partition_table()
            self.event_queue.put(Packet(EventType.PARTITION_TABLE_RECEIVED, pt_data))
            self._log(LogLevel.INFO, "Partition Table received")
        except Exception as e:
            self.event_queue.put(Packet(EventType.ERROR, {"msg": str(e)}))
            self._log(LogLevel.ERROR, f"Failed getting PT: {e}")
        finally:
            if was_connected:
                self._connect(silent=True)
            if not self.connected and was_connected:
                self._emit_status(False)

    def _get_progress_callback(self, operation_name: str):
        def callback(percent):
            self.event_queue.put(Packet(EventType.FIRMWARE_PROGRESS, {
                "operation": operation_name,
                "progress": percent
            }))
        return callback

    def _get_app_info(self, partitions: list):
        self._log(LogLevel.INFO, "Requesting App Info...")
        port = None
        if self.device_interface and hasattr(self.device_interface, "port"):
            port = self.device_interface.port

        # Try various methods to run the tool
        from esp_viewer.core.mock_device import MockDevice

        if isinstance(self.device_interface, MockDevice):
            mock_info = {
                "status": "success",
                "apps": [{
                    "label": "factory",
                    "project_name": "ESP-Viewer-Mock",
                    "app_version": "v1.0.0-mock",
                    "compile_time": "2026-03-10 12:00:00",
                    "raw": "Project name: ESP-Viewer-Mock\nApp version: v1.0.0-mock\nCompile time: 2026-03-10 12:00:00\n[mock] Extracted from 4KB header simulation."
                }]
            }
            self.event_queue.put(Packet(EventType.APP_INFO_RECEIVED, mock_info))
            return

        if not port:
            self._log(
                LogLevel.ERROR,
                "Cannot get App Info: Unknown port or device not a Serial device",
            )
            self.event_queue.put(Packet(EventType.ERROR, {"msg": "Port unknown"}))
            return

        was_connected = self.connected
        if was_connected:
            self._disconnect(silent=True)
        
        self.is_operation_active = True

        try:
            from esp_viewer.core.chip_info import ChipInfoManager
            manager = ChipInfoManager(port, baudrate=self.current_baud)
            
            if not partitions:
                pt_data = manager.get_partition_table()
                if pt_data.get("status") == "success":
                    partitions = [p for p in pt_data.get("partitions", []) if p.get("type") == "app"]
            
            if not partitions:
                self.event_queue.put(Packet(EventType.ERROR, {"msg": "No app partitions found."}))
                return
            
            self._log(LogLevel.INFO, f"Starting read for {len(partitions)} App partition(s)...")
            results = []
            
            for idx, p in enumerate(partitions):
                offset = p.get('offset', "0x10000")
                label = p.get('label', f"app_{idx}")
                # We must read entire partition for image-info to parse successfully without fatal error
                size = p.get('size', "0x200000")
                app_data = manager.get_app_info(offset, size=size, progress_callback=self._get_progress_callback(f"App Info ({label})"))
                
                if app_data.get("status") == "success":
                    app_data["label"] = label
                    app_data["offset"] = offset
                    results.append(app_data)
            
            final_data = {"status": "success", "apps": results}
            self.event_queue.put(Packet(EventType.APP_INFO_RECEIVED, final_data))
            self._log(LogLevel.INFO, "App Info received for all partitions.")
        except Exception as e:
            self.event_queue.put(Packet(EventType.ERROR, {"msg": str(e)}))
            self._log(LogLevel.ERROR, f"Failed getting App Info: {e}")
        finally:
            self.is_operation_active = False
            if was_connected:
                try:
                    if self.device_interface and not self.device_interface.connected:
                        self.device_interface.open()
                    self.connected = True
                except:
                    pass
                self._emit_status(False)

    def _list_ports(self):
        """Lists available serial ports and emits them."""
        # If forced to mock mode, skip physical port scanning
        if self.profile.get("connection", {}).get("port") == "MOCK":
            self.event_queue.put(Packet(EventType.PORTS_LISTED, {"ports": []}))
            return

        try:
            from esp_viewer.core.drivers import SerialDriver

            ports = SerialDriver.list_ports()
            self.event_queue.put(Packet(EventType.PORTS_LISTED, {"ports": ports}))
        except Exception as e:
            self._log(LogLevel.ERROR, f"Failed to list ports: {e}")

    def _read_firmware(self, payload):
        """Read a segment of flash."""
        offset = payload.get("offset", "0x0")
        size = payload.get("size", "0x1000")
        filename = payload.get("filename", "dump.bin")
        dest_dir = "Data/dumps"
        os.makedirs(dest_dir, exist_ok=True)
        output_path = os.path.join(dest_dir, filename)

        self._log(LogLevel.INFO, f"Reading Flash at {offset} (Size: {size}) to {output_path}...")

        # Mock Handling
        from esp_viewer.core.mock_device import MockDevice
        if isinstance(self.device_interface, MockDevice):
            try:
                with open(output_path, "wb") as f:
                    f.write(b"\xFF" * int(size, 16))
                self.event_queue.put(Packet(EventType.FIRMWARE_READ_COMPLETE, {"status": "success", "path": output_path, "filename": filename}))
                self._log(LogLevel.INFO, "Mock Flash read successful.")
            except Exception as e:
                self._log(LogLevel.ERROR, f"Mock read failed: {e}")
            return

        # Real Device
        port = getattr(self.device_interface, "port", None)
        if not port:
            self._log(LogLevel.ERROR, "Port unknown")
            return

        was_connected = self.connected
        if was_connected: self._disconnect(silent=True)
        
        self.is_operation_active = True

        try:
            manager = ChipInfoManager(port, baudrate=self.current_baud)
            res = manager.read_flash(offset, size, output_path, progress_callback=self._get_progress_callback("Firmware Backup"))
            if res["status"] == "success":
                # Save metadata JSON here too for consistency
                # Use current appInfo if available or create a basic one
                metadata = getattr(self, 'last_app_info', {}).copy()
                metadata.update({
                    "backup_time": time.ctime(),
                    "filename": filename,
                    "offset": offset,
                    "size": size
                })
                json_filename = filename.replace(".bin", "_meta.json")
                json_path = os.path.join(dest_dir, json_filename)
                with open(json_path, 'w', encoding='utf-8') as f:
                    json.dump(metadata, f, indent=4)
                
                self.event_queue.put(Packet(EventType.FIRMWARE_READ_COMPLETE, {
                    "status": "success", 
                    "path": output_path, 
                    "filename": filename,
                    "json_filename": json_filename,
                    "metadata": metadata
                }))
                self._log(LogLevel.INFO, f"Flash read complete: {output_path}")
            else:
                self._log(LogLevel.ERROR, f"Flash read failed: {res.get('error')}")
        except Exception as e:
            self._log(LogLevel.ERROR, f"Read firmware exception: {e}")
        finally:
            self.is_operation_active = False
            if was_connected:
                # Minimal restoration: reopen port if it was closed, but don't re-emit full connect event
                try:
                    if self.device_interface and not self.device_interface.connected:
                        self.device_interface.open()
                    self.connected = True
                except:
                    pass

    def _backup_firmware(self, payload):
        """Helper to trigger a full backup of an app partition."""
        # This can be implemented as a wrapper around _read_firmware
        # usually called with partition offset and size.
        self._read_firmware(payload)

