import socket, struct, time, threading
from datetime import datetime
import serial
import re

# ── 서버/프로토콜 설정 ─────────────────────────────────────────
SERVER_IP   = "222.108.131.130"
SERVER_PORT = 15010

USER_ID   = "ilshin"
USER_PASS = "ilshin1234"

HEADERKEY         = 0x12345678
AIR_QUALITY_TYPE  = 0x38000000
MSGTYPE_BIND      = AIR_QUALITY_TYPE | 0
MSGTYPE_ALIVE     = AIR_QUALITY_TYPE | 4
MSGTYPE_ADD       = AIR_QUALITY_TYPE | 178

# ── 시리얼 설정 ────────────────────────────────────────────────
SERIAL_PORT = "/dev/ttyS0"
BAUD_RATE   = 9600
SERIAL_TIMEOUT_SEC = 1.0    # readline 타임아웃

# ── “맑음” 기본값 & 신호 유효시간 ─────────────────────────────
CLEAR_PM25 = 12
CLEAR_PM10 = 20
STALE_SECONDS = 15          # 최근 시리얼 수신 없으면 맑음 전송
SEND_INTERVAL = 10          # 전송 주기(초)

# ── 응답 헤더 파싱 ─────────────────────────────────────────────
def parse_header(resp: bytes):
    if len(resp) < 28: 
        return None
    try:
        hk, mt, bl, r1, r2, r3, res = struct.unpack("<IIIIIII", resp[:28])
        return {"headerKey": hk, "msgType": mt, "bodyLen": bl, "result": res}
    except Exception:
        return None

# ── C 문자열 고정폭 ────────────────────────────────────────────
def fixed_cstr(s: str, size: int) -> bytes:
    b = s.encode("utf-8")
    if len(b) >= size:  # 마지막은 '\x00'로 보장
        b = b[:size-1]
    b = b + b"\x00"
    return b.ljust(size, b"\x00")

# ── BIND/ALIVE/ADD 패킷 빌드 ───────────────────────────────────
def build_bind_packet(user_id: str, user_pass: str) -> bytes:
    home_version = struct.pack("<I", 0)
    nKind        = struct.pack("<I", 0)
    nVersion     = (0).to_bytes(16, "little")
    szId         = fixed_cstr(user_id, 40)
    szPass       = fixed_cstr(user_pass, 40)
    body = home_version + nKind + nVersion + szId + szPass
    header = struct.pack("<IIIIIII", HEADERKEY, MSGTYPE_BIND, len(body), 0,0,0,0)
    return header + body

def build_alive_packet() -> bytes:
    home_version = struct.pack("<I", 0)
    nKind        = struct.pack("<I", 0)
    nVersion     = (0).to_bytes(16, "little")
    body = home_version + nKind + nVersion
    header = struct.pack("<IIIIIII", HEADERKEY, MSGTYPE_ALIVE, len(body), 0,0,0,0)
    return header + body

def build_air_quality_add(pm25, pm10, *, n_area=1) -> bytes:
    now = datetime.now().strftime("%Y-%m-%d %H:%M:%S").encode("utf-8").ljust(20, b"\x00")
    body  = struct.pack("<I", n_area)    # nArea
    body += now
    body += b"\x00"*20 + b"\x00"*20 + b"\x00"*20  # szMangName/Term/Station
    body += struct.pack("<ffff", 0.0,0.0,0.0,0.0) # SO2, CO, O3, NO2
    # 순서: PM10, 0, PM2.5, 0, 그 뒤 8개는 0
    body += struct.pack("<IIIIIIIIIIII",
                        int(pm10), 0, int(pm25), 0,
                        0,0,0,0, 0,0,0,0)
    header = struct.pack("<IIIIIII", HEADERKEY, MSGTYPE_ADD, len(body), 0,0,0,0)
    return header + body

# ── 서버 연결 + BIND ───────────────────────────────────────────
def try_connect_and_bind():
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(5)
        sock.connect((SERVER_IP, SERVER_PORT))
        sock.sendall(build_bind_packet(USER_ID, USER_PASS))
        resp = sock.recv(1024)
        hdr = parse_header(resp)
        if hdr and hdr.get("result", 1) == 0:
            print("✅ BIND 성공")
            return sock
        else:
            print("❌ BIND 실패:", hdr)
            sock.close()
            return None
    except Exception as e:
        print("⚠️ 연결 실패:", e)
        return None

# ── KeepAlive 쓰레드 ───────────────────────────────────────────
def keepalive_loop(sock: socket.socket, stop_event: threading.Event):
    while not stop_event.is_set():
        try:
            sock.sendall(build_alive_packet())
            resp = sock.recv(64)
            print("💓 ALIVE sent, ack:", parse_header(resp))
        except Exception as e:
            print("⚠️ ALIVE 실패:", e)
            break
        # 30초 주기
        if stop_event.wait(30):
            break
    print("💤 KeepAlive thread 종료")

# ── 시리얼 리더 + 최신값 공유 ──────────────────────────────────
class SerialState:
    def __init__(self):
        self.lock = threading.Lock()
        self.last_ts = 0.0
        self.pm25 = None
        self.pm10 = None

    def update(self, pm25, pm10):
        with self.lock:
            self.pm25 = int(pm25)
            self.pm10 = int(pm10)
            self.last_ts = time.time()

    def get_fresh(self):
        with self.lock:
            if self.last_ts and (time.time() - self.last_ts) <= STALE_SECONDS:
                return self.pm25, self.pm10
            return None

def open_serial():
    while True:
        try:
            ser = serial.Serial(SERIAL_PORT, BAUD_RATE, timeout=SERIAL_TIMEOUT_SEC)
            print(f"✅ 시리얼 포트 {SERIAL_PORT} opened @ {BAUD_RATE}")
            return ser
        except Exception as e:
            print(f"❌ 시리얼 오픈 실패: {e} → 3초 후 재시도")
            time.sleep(3)

def parse_serial_line(line: str):
    """
    지원 형식:
      1) DATA,pm1,pm25,pm10
      2) 임의 라인에서 숫자 2~3개 추출 → (pm25, pm10) 유추
    """
    line = line.strip()
    if not line:
        return None

    # 1) 정식 프로토콜
    if line.startswith("DATA,"):
        parts = line.split(",")
        if len(parts) >= 4:
            try:
                pm25 = int(parts[2])
                pm10 = int(parts[3])
                return pm25, pm10
            except ValueError:
                pass

    # 2) 숫자만 긁어오는 유연 파싱 (정수 최대 3개)
    nums = re.findall(r"\d+", line)
    if len(nums) >= 2:
        try:
            vals = list(map(int, nums[:3]))
            # heuristic: (pm1, pm25, pm10) 또는 (pm25, pm10)
            if len(vals) == 3:
                return vals[1], vals[2]
            else:
                return vals[0], vals[1]
        except Exception:
            return None

    return None

def serial_reader_loop(state: SerialState, stop_event: threading.Event):
    ser = open_serial()
    while not stop_event.is_set():
        try:
            raw = ser.readline()  # bytes
            if not raw:
                continue
            try:
                s = raw.decode("utf-8", errors="ignore")
            except Exception:
                continue
            parsed = parse_serial_line(s)
            if parsed:
                pm25, pm10 = parsed
                state.update(pm25, pm10)
                print(f"📥 시리얼 수신 → PM2.5={pm25}, PM10={pm10}")
        except serial.SerialException as e:
            print(f"⚠️ 시리얼 예외: {e} → 재연결")
            try:
                ser.close()
            except:
                pass
            time.sleep(1)
            ser = open_serial()
        except Exception as e:
            print(f"⚠️ 시리얼 읽기 오류: {e}")

    try:
        ser.close()
    except:
        pass
    print("💤 Serial reader thread 종료")

# ── 전송 쓰레드: 시리얼 신호 우선, 없으면 '맑음' ───────────────
def add_send_loop(sock: socket.socket, state: SerialState, stop_event: threading.Event):
    while not stop_event.is_set():
        try:
            fresh = state.get_fresh()
            if fresh:
                pm25, pm10 = fresh
                tag = "serial"
            else:
                pm25, pm10 = CLEAR_PM25, CLEAR_PM10
                tag = "clear"

            pkt = build_air_quality_add(pm25, pm10)
            sock.sendall(pkt)
            ack = sock.recv(128)
            print(f"🛰️ ADD sent ({tag}) PM2.5={pm25}, PM10={pm10} ack:", parse_header(ack))

        except Exception as e:
            print("❌ ADD 전송 실패:", e)
            break

        if stop_event.wait(SEND_INTERVAL):
            break

    print("💤 ADD sender thread 종료")

# ── 메인 ───────────────────────────────────────────────────────
def main():
    sock = try_connect_and_bind()
    if not sock:
        return

    stop_event = threading.Event()
    # KeepAlive thread
    th_alive = threading.Thread(target=keepalive_loop, args=(sock, stop_event), daemon=True)
    th_alive.start()

    # Serial reader state + thread
    state = SerialState()
    th_serial = threading.Thread(target=serial_reader_loop, args=(state, stop_event), daemon=True)
    th_serial.start()

    # Sender thread (serial 우선, 없으면 맑음)
    th_add = threading.Thread(target=add_send_loop, args=(sock, state, stop_event), daemon=True)
    th_add.start()

    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        pass
    finally:
        stop_event.set()
        try: sock.close()
        except: pass
        print("🛑 종료")

if __name__ == "__main__":
    main()
