# 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 asyncio
import json
import os
from typing import List

import uvicorn
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles

from esp_viewer.core.adapter import BaseFrontendAdapter
from esp_viewer.core.protocol import CommandType, Packet


class WebAdapter(BaseFrontendAdapter):
    """
    Adapter that exposes the EspViewerEngine to a Web Frontend via WebSockets.
    It runs a FastAPI server with an active WebSocket endpoint.
    """

    def __init__(self, port: int = 8080):
        super().__init__()
        self.port = port
        self.app = FastAPI(title="ESP Viewer Web API")
        self.active_connections: List[WebSocket] = []

        # Asyncio event loop running the server
        self.loop = None

        self._setup_routes()

    def _setup_routes(self):
        # Allow CORS for development (e.g., if UI runs on Vite dev server port 5173)
        self.app.add_middleware(
            CORSMiddleware,
            allow_origins=["*"],
            allow_credentials=True,
            allow_methods=["*"],
            allow_headers=["*"],
        )

        @self.app.websocket("/ws")
        async def websocket_endpoint(websocket: WebSocket):
            await websocket.accept()

            if self.engine:
                self.engine.send_command(CommandType.RESET_STATE)

            self.active_connections.append(websocket)
            try:
                # Send the initial profile configuration so the UI can build itself
                await websocket.send_json(
                    {
                        "event": "PROFILE_LOADED",
                        "payload": self.engine.profile if self.engine else {},
                    }
                )

                # Send current states immediately after profile
                if self.engine:
                    # Sync Connection Status
                    await websocket.send_json(
                        {
                            "event": "STATUS_CHANGED",
                            "payload": {"connected": self.engine.device_service.connected},
                        }
                    )
                    # Sync Stream Status
                    await websocket.send_json(
                        {
                            "event": "STREAM_STATUS",
                            "payload": {
                                "enabled": self.engine.device_service.stream_enabled
                            },
                        }
                    )
                    # Sync IO Log Status
                    await websocket.send_json(
                        {
                            "event": "IO_LOG_STATUS",
                            "payload": {
                                "enabled": self.engine.device_service.io_logging_enabled,
                                "path": self.engine.device_service.current_log_path,
                            },
                        }
                    )

                # Request initial port list
                self._handle_incoming_command({"command": "LIST_PORTS"})

                # Listen for incoming commands from the Web UI
                while True:
                    data = await websocket.receive_text()
                    try:
                        cmd_msg = json.loads(data)
                        self._handle_incoming_command(cmd_msg)
                    except json.JSONDecodeError:
                        # Silencing JSON decode errors to avoid log spam,
                        # or could route to self.engine.log if Engine is fully typed here.
                        pass

            except WebSocketDisconnect:
                self.active_connections.remove(websocket)

        # Serve firmware dumps for browser download
        # Use absolute path derived from current working directory so user data is not written to site-packages
        dumps_dir = os.path.join(os.getcwd(), "Data", "dumps")
        os.makedirs(dumps_dir, exist_ok=True)
        self.app.mount("/dumps", StaticFiles(directory=dumps_dir), name="dumps")

        # Serve static Vue/React build files if they exist
        # Order is important: sub-mounts like /dumps must come before the root / mount
        static_dir = os.path.join(os.path.dirname(__file__), "dist")
        if os.path.exists(static_dir):
            self.app.mount(
                "/", StaticFiles(directory=static_dir, html=True), name="static"
            )

    def _handle_incoming_command(self, msg: dict):
        """Translate JSON from WebSocket into Engine Commands."""
        if not self.engine:
            return

        cmd = msg.get("command")
        payload = msg.get("payload", {})

        if cmd == "CONNECT":
            port = payload.get("port")
            if not port:
                if self.engine.profile.get("meta", {}).get("mock_type"):
                    mock_type = self.engine.profile.get("meta", {}).get("mock_type")
                    port = f"mock://{mock_type}"

            if port:
                if port.startswith("mock://"):
                    from esp_viewer.core.mock_device import MockDevice

                    driver = MockDevice(port="MOCK", profile=self.engine.profile)
                else:
                    from esp_viewer.core.drivers import SerialDriver

                    baud = payload.get("baud")
                    if not baud:
                        baud = self.engine.profile.get("connection", {}).get(
                            "default_baud", 115200
                        )
                    driver = SerialDriver(port, baud)

                self.engine.set_device_interface(driver)

            self.engine.send_command(CommandType.CONNECT, payload)
        elif cmd == "DISCONNECT":
            self.engine.send_command(CommandType.DISCONNECT)
        elif cmd == "SET_PARAM":
            self.engine.send_command(CommandType.SET_PARAM, payload)
        elif cmd == "START_STREAM":
            self.engine.send_command(CommandType.START_STREAM)
        elif cmd == "STOP_STREAM":
            self.engine.send_command(CommandType.STOP_STREAM)
        elif cmd == "WRITE_RAW":
            data = payload.get("data", "")
            self.engine.send_command(CommandType.WRITE_RAW, {"data": data})
        elif cmd == "GET_CHIP_INFO":
            self.engine.send_command(CommandType.GET_CHIP_INFO)
        elif cmd == "GET_PARTITION_TABLE":
            self.engine.send_command(CommandType.GET_PARTITION_TABLE)
        elif cmd == "GET_APP_INFO":
            self.engine.send_command(CommandType.GET_APP_INFO, payload)
        elif cmd == "LIST_PORTS":
            self.engine.send_command(CommandType.LIST_PORTS)
        elif cmd == "RESET_STATE":
            self.engine.send_command(CommandType.RESET_STATE)
        elif cmd == "READ_FIRMWARE":
            self.engine.send_command(CommandType.READ_FIRMWARE, payload)
        elif cmd == "BACKUP_FIRMWARE":
            self.engine.send_command(CommandType.BACKUP_FIRMWARE, payload)

    def start_server(self):
        """Blocking call to start the ASGI server. Should be run in main thread."""
        config = uvicorn.Config(
            app=self.app, host="0.0.0.0", port=self.port, log_level="info"
        )
        server = uvicorn.Server(config)
        self.loop = asyncio.get_event_loop()
        self.loop.run_until_complete(server.serve())

    # --- BaseFrontendAdapter Abstract Methods ---

    def on_event_received(self, packet: Packet):
        """
        Receives packet from the Engine's background thread.
        Schedules it to be sent to all WebSocket clients on the async loop.
        """
        if not self.loop or self.loop.is_closed():
            return

        # Serialize packet info
        event_dict = {"event": packet.type.name, "payload": packet.payload}

        # We must schedule the async broadcast in the uvicorn event loop
        asyncio.run_coroutine_threadsafe(self._broadcast(event_dict), self.loop)

    async def _broadcast(self, event_dict: dict):
        # Convert to JSON string once for all connections
        # Handle non-serializable objects (like LogLevel Enum) if necessary
        try:
            msg = json.dumps(event_dict, default=str)
        except TypeError:
            # Drop invalid serialization silently for production stability
            return

        for connection in self.active_connections.copy():
            try:
                await connection.send_text(msg)
            except Exception:
                # Connection dropped mid-send
                if connection in self.active_connections:
                    self.active_connections.remove(connection)
