#!/usr/bin/env python3
"""Collecteur UDP signe. Aucun chiffrement : utiliser VPN 1NCE si confidentialite requise.
Cle 64 caracteres hex utilisee comme texte ASCII, identique au sketch.
Pas d'installation/activation automatique sur le VPS.
"""
import hashlib,hmac,json,os,re,socket,sqlite3,time
from pathlib import Path

def authenticate(data, keys):
    if len(data)>512: raise ValueError('size')
    text=data.decode('ascii').rstrip('\n')
    fields=text.split('|')
    if len(fields)!=6 or fields[0]!='v1': raise ValueError('format')
    _,device,seq,t,rh,mac=fields
    key=keys.get(device,'')
    if not re.fullmatch(r'[A-Za-z0-9_-]{1,32}',device) or not re.fullmatch(r'[0-9a-fA-F]{64}',key): raise ValueError('device')
    body='|'.join(fields[:-1])
    if not hmac.compare_digest(hmac.new(key.encode(),body.encode(),hashlib.sha256).hexdigest(),mac): raise ValueError('signature')
    if not re.fullmatch(r'[1-9][0-9]{0,17}',seq): raise ValueError('sequence')
    if not re.fullmatch(r'-?[0-9]+\.[0-9]{2}',t) or not re.fullmatch(r'[0-9]+\.[0-9]{2}',rh): raise ValueError('number')
    if not -40<=float(t)<=105 or not 0<=float(rh)<=100: raise ValueError('range')
    ack=f'ack|{device}|{seq}'
    ack+='|'+hmac.new(key.encode(),ack.encode(),hashlib.sha256).hexdigest()+'\n'
    return device,int(seq),float(t),float(rh),ack.encode()

def main():
    os.umask(0o077)
    keys=json.loads(Path(os.environ['TERRAIN_KEYS']).read_text())
    db=sqlite3.connect(os.environ.get('TERRAIN_DB','terrain.sqlite'))
    db.execute('PRAGMA journal_mode=WAL')
    db.execute('CREATE TABLE IF NOT EXISTS samples(device TEXT,seq INTEGER,temp_c REAL,rh REAL,received_at INTEGER,PRIMARY KEY(device,seq))')
    sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
    sock.bind((os.environ.get('TERRAIN_BIND','127.0.0.1'),int(os.environ.get('TERRAIN_PORT','9001'))))
    print('Collecteur pret (aucune cle journalisee)',flush=True)
    while True:
        data,address=sock.recvfrom(1024)
        try:
            device,seq,t,rh,ack=authenticate(data,keys)
            # Doublon exact => ACK ; ancien paquet jamais vu => refuse (anti-rejeu).
            row=db.execute('SELECT temp_c,rh FROM samples WHERE device=? AND seq=?',(device,seq)).fetchone()
            high=db.execute('SELECT MAX(seq) FROM samples WHERE device=?',(device,)).fetchone()[0] or 0
            if row:
                if row!=(t,rh):continue
            elif seq>high:
                db.execute('INSERT INTO samples VALUES(?,?,?,?,?)',(device,seq,t,rh,int(time.time())))
                db.commit() # ACK seulement apres ecriture durable.
            else:continue
            sock.sendto(ack,address)
        except (ValueError,UnicodeError,KeyError,TypeError):
            continue # Ne pas repondre aux paquets non authentifies.

if __name__=='__main__':main()
