#!/usr/bin/env python3
"""決策層：從序列埠讀取感測資料，依溫度門檻送回動作指令。

依賴：pip install pyserial

預設規則：溫度「超過」門檻（預設 30.0°C，採嚴格大於）時，
同時開啟 LED 與繼電器；溫度讀取失敗時關閉輸出。
"""

from __future__ import annotations

import argparse
import socket
import sys
import time
from typing import Optional

import serial
import serial.tools.list_ports

BAUD = 9600
TEMP_THRESHOLD_C = 30.0

Reading = dict[str, Optional[float] | Optional[int]]


def pick_port(explicit: Optional[str]) -> str:
    if explicit:
        return explicit
    ports = list(serial.tools.list_ports.comports())
    if not ports:
        sys.exit("找不到序列埠。請用 --port 指定，例如 COM3 或 /dev/ttyACM0。")
    keywords = (
        "arduino",
        "usb serial",
        "ch340",
        "cp210",
        "wch",
        "silicon labs",
        "usbmodem",
        "ttyacm",
        "ttyusb",
    )
    for item in ports:
        blob = f"{item.device} {item.description} {item.manufacturer or ''}".lower()
        if any(key in blob for key in keywords):
            return item.device
    return ports[0].device


def parse_data(line: str) -> Optional[dict]:
    """解析 DATA,temp,humidity,distance_cm,light。失敗回傳 None。"""
    parts = line.strip().split(",")
    if len(parts) != 5 or parts[0] != "DATA":
        return None

    def to_float(value: str) -> Optional[float]:
        if value.lower() == "nan":
            return None
        try:
            return float(value)
        except ValueError:
            return None

    def to_int(value: str) -> Optional[int]:
        try:
            return int(value)
        except ValueError:
            return None

    return {
        "temp": to_float(parts[1]),
        "humidity": to_float(parts[2]),
        "distance_cm": to_float(parts[3]),
        "light": to_int(parts[4]),
    }


def decide(reading: dict, temp_threshold: float) -> tuple[int, int, str]:
    """回傳 (led, relay, reason)。

    溫度超過門檻時兩者同時開啟；剛好等於門檻不觸發。
    溫度讀取失敗時關閉輸出。
    """
    temp = reading["temp"]
    if temp is None:
        return 0, 0, "溫度讀取失敗，先關閉輸出"
    if temp > temp_threshold:
        return (
            1,
            1,
            f"溫度 {temp:.2f}°C 超過門檻 {temp_threshold:.1f}°C，觸發警報",
        )
    return 0, 0, f"溫度 {temp:.2f}°C 未超過門檻，維持關閉"


def format_command(led: int, relay: int) -> str:
    return f"ALERT,{led},{relay}"


def process_line(line: str, temp_threshold: float) -> Optional[tuple[dict, str, str]]:
    """處理一列輸入。成功時回傳 (reading, command, reason)。"""
    reading = parse_data(line)
    if reading is None:
        return None
    led, relay, reason = decide(reading, temp_threshold)
    return reading, format_command(led, relay), reason


def run_serial(port: str, temp_threshold: float) -> None:
    print(f"開啟序列埠 {port}（{BAUD} bps），門檻 {temp_threshold:.1f}°C")
    with serial.Serial(port, BAUD, timeout=1) as ser:
        time.sleep(2.5)  # 許多 Arduino 在開啟序列埠時會重設
        ser.reset_input_buffer()
        last_cmd: Optional[str] = None
        while True:
            raw = ser.readline()
            if not raw:
                continue
            line = raw.decode("utf-8", errors="ignore").strip()
            result = process_line(line, temp_threshold)
            if result is None:
                if line:
                    print(f"略過無法解析的列：{line}")
                continue
            reading, cmd, reason = result
            print(
                f"T={reading['temp']} H={reading['humidity']} "
                f"D={reading['distance_cm']} L={reading['light']} → {cmd}（{reason}）"
            )
            if cmd != last_cmd:
                ser.write((cmd + "\n").encode("ascii"))
                last_cmd = cmd


def run_tcp_server(host: str, port: int, temp_threshold: float) -> None:
    """給 ESP32 選用路徑用的 TCP 決策伺服器，協定與序列埠版相同。"""
    print(f"在 {host}:{port} 等待 TCP 連線，門檻 {temp_threshold:.1f}°C")
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
        server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        server.bind((host, port))
        server.listen(1)
        while True:
            conn, addr = server.accept()
            print(f"已連線：{addr[0]}:{addr[1]}")
            last_cmd: Optional[str] = None
            with conn:
                buffer = b""
                while True:
                    chunk = conn.recv(1024)
                    if not chunk:
                        print("對端關閉連線")
                        break
                    buffer += chunk
                    while b"\n" in buffer:
                        raw, buffer = buffer.split(b"\n", 1)
                        line = raw.decode("utf-8", errors="ignore").strip()
                        result = process_line(line, temp_threshold)
                        if result is None:
                            if line:
                                print(f"略過無法解析的列：{line}")
                            continue
                        reading, cmd, reason = result
                        print(
                            f"T={reading['temp']} H={reading['humidity']} "
                            f"D={reading['distance_cm']} L={reading['light']} "
                            f"→ {cmd}（{reason}）"
                        )
                        if cmd != last_cmd:
                            conn.sendall((cmd + "\n").encode("ascii"))
                            last_cmd = cmd


def main() -> None:
    parser = argparse.ArgumentParser(description="Arduino / ESP32 感測資料決策層")
    parser.add_argument(
        "--port",
        help="序列埠，例如 COM3、/dev/ttyACM0、/dev/tty.usbmodem14101",
    )
    parser.add_argument(
        "--threshold",
        type=float,
        default=TEMP_THRESHOLD_C,
        help="溫度門檻（攝氏），預設 30",
    )
    parser.add_argument(
        "--tcp",
        action="store_true",
        help="改開 TCP 伺服器（ESP32 Wi-Fi 路徑）",
    )
    parser.add_argument("--bind", default="0.0.0.0", help="TCP 綁定位址")
    parser.add_argument("--tcp-port", type=int, default=5000, help="TCP 連接埠")
    args = parser.parse_args()

    if args.tcp:
        run_tcp_server(args.bind, args.tcp_port, args.threshold)
        return

    port = pick_port(args.port)
    run_serial(port, args.threshold)


if __name__ == "__main__":
    main()
