#!/usr/bin/env python3

import base64
import json
import os
import ssl
import struct
import subprocess
import sys
import threading
import uuid

import paho.mqtt.client as mqtt
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

BROKER = "127.0.0.1"
PORT = 8883
USERNAME = "sts_pairing_server"
PASSWORD_FILE = "/etc/sts_secure/mqtt_pairing.pass"

AAD_C2S = b"STS1:C2S"
AAD_S2C = b"STS1:S2C"

done = threading.Event()
result = {"ok": False, "status": "TIMEOUT"}

device_key = None
client_nonce_b64 = None
sid_hex = None
command_sent = False


def read_request():
    raw = sys.stdin.read()
    try:
        req = json.loads(raw)
    except Exception:
        raise RuntimeError("invalid_json")

    imei = str(req.get("imei", "")).strip()
    pin = str(req.get("pin", "")).strip()

    if len(imei) != 15 or not imei.isdigit():
        raise RuntimeError("invalid_imei")
    if not pin.isdigit() or len(pin) < 4 or len(pin) > 8:
        raise RuntimeError("invalid_pin")

    return imei, pin


def get_device_key(imei):
    php = r'''
require "/var/www/html/sts_secure/lib/db.php";
require "/var/www/html/sts_secure/lib/crypto.php";

$imei = $argv[1] ?? "";
$pdo = sts_db();
$s = $pdo->prepare(
    "SELECT device_key_ciphertext, device_key_nonce, device_key_tag
     FROM devices
     WHERE imei = :imei AND status = 'active'"
);
$s->execute([":imei" => $imei]);
$r = $s->fetch();
if (!$r) {
    fwrite(STDERR, "Device non disponibile\n");
    exit(2);
}
$k = sts_decrypt_device_key_from_storage(
    $r["device_key_ciphertext"],
    $r["device_key_nonce"],
    $r["device_key_tag"]
);
if (strlen($k) !== 32) {
    fwrite(STDERR, "DeviceKey non valida\n");
    exit(3);
}
echo bin2hex($k);
'''

    try:
        out = subprocess.check_output(
            ["php", "-r", php, imei],
            text=True,
            stderr=subprocess.DEVNULL,
            timeout=5,
        ).strip()
    except Exception as exc:
        raise RuntimeError("device_key_unavailable") from exc

    key = bytes.fromhex(out)
    if len(key) != 32:
        raise RuntimeError("device_key_unavailable")
    return key


def decrypt_s1o(payload):
    if not payload.startswith("S1O|"):
        return None
    parts = payload.split("|")
    if len(parts) != 4:
        return None

    nonce = base64.b64decode(parts[1], validate=True)
    ciphertext = base64.b64decode(parts[2], validate=True)
    tag = base64.b64decode(parts[3], validate=True)

    plain = AESGCM(device_key).decrypt(
        nonce,
        ciphertext + tag,
        AAD_S2C,
    )
    return json.loads(plain.decode("utf-8"))


def make_s1d(sid, seq, plain_obj):
    sid_bytes = bytes.fromhex(sid)
    if len(sid_bytes) != 8:
        raise RuntimeError("invalid_sid")

    nonce = sid_bytes + struct.pack(">I", seq)
    plain = json.dumps(plain_obj, separators=(",", ":")).encode("utf-8")
    encrypted = AESGCM(device_key).encrypt(nonce, plain, AAD_C2S)
    ciphertext = encrypted[:-16]
    tag = encrypted[-16:]

    return (
        f"S1D|{sid}|{seq:08X}|"
        f"{base64.b64encode(ciphertext).decode()}|"
        f"{base64.b64encode(tag).decode()}"
    )


def main():
    global result, device_key, client_nonce_b64, sid_hex, command_sent

    imei, pin = read_request()
    device_key = get_device_key(imei)

    with open(PASSWORD_FILE, "r", encoding="utf-8") as f:
        password = f.read().strip()

    topic_in = f"leo/centrin/{imei}/s1"
    topic_out = f"leo/centrout/{imei}/s1"
    app_id = "STS-PAIR-" + uuid.uuid4().hex[:12]
    client_nonce_b64 = base64.b64encode(os.urandom(16)).decode()

    def on_connect(client, userdata, flags, rc):
        global result
        if rc != 0:
            result = {"ok": False, "status": "MQTT_ERROR"}
            done.set()
            return
        client.subscribe(topic_out, qos=0)

    def on_subscribe(client, userdata, mid, granted_qos):
        hello = f"S1H|{app_id}|{client_nonce_b64}"
        client.publish(topic_in, hello, qos=0)

    def on_message(client, userdata, msg):
        global result, sid_hex, command_sent

        try:
            data = decrypt_s1o(msg.payload.decode("utf-8"))
        except Exception:
            return

        if not isinstance(data, dict):
            return

        if data.get("Sec") == "WELCOME":
            if data.get("Nonce") != client_nonce_b64:
                return

            received_sid = str(data.get("Sid", "")).upper()
            if len(received_sid) != 16:
                return

            if sid_hex is None:
                sid_hex = received_sid
            elif sid_hex != received_sid:
                return

            if command_sent:
                return

            command_sent = True
            envelope = make_s1d(
                sid_hex,
                1,
                {
                    "Id": app_id,
                    "Type": "CHECKPIN",
                    "Pin": pin,
                },
            )
            client.publish(topic_in, envelope, qos=0)
            return

        if data.get("Type") != "CHECKPIN":
            return

        status = str(data.get("Pin", "")).upper()
        if status == "OK":
            result = {"ok": True, "status": "OK"}
        elif status == "NOK":
            result = {"ok": False, "status": "NOK"}
        elif status.startswith("WAIT"):
            result = {"ok": False, "status": status}
        else:
            return

        done.set()

    ctx = ssl.create_default_context()
    ctx.check_hostname = False
    ctx.verify_mode = ssl.CERT_NONE

    client = mqtt.Client(client_id=app_id, protocol=mqtt.MQTTv311)
    client.username_pw_set(USERNAME, password)
    client.tls_set_context(ctx)
    client.on_connect = on_connect
    client.on_subscribe = on_subscribe
    client.on_message = on_message

    client.connect(BROKER, PORT, 20)
    client.loop_start()
    done.wait(10)
    client.disconnect()
    client.loop_stop()

    print(json.dumps(result, separators=(",", ":")))


if __name__ == "__main__":
    try:
        main()
    except RuntimeError as exc:
        print(json.dumps({"ok": False, "status": str(exc).upper()}, separators=(",", ":")))
        sys.exit(1)
    except Exception:
        print(json.dumps({"ok": False, "status": "ERROR"}, separators=(",", ":")))
        sys.exit(1)
