--[[
@module aliyun
@summary AliYun阿里云物联网平台
@version 1.0
@date    2023.06.07
@author  wendal
@demo    aliyun
@usage
-- 请查阅demo
]]
_G.sys = require("sys")
--[[特别注意, 使用http库需要下列语句]]
_G.sysplus = require("sysplus")
local libfota = require("libfota")

-- 总的库对象
local aliyun = {}
local mqttc = nil

local ClientId,PassWord,UserName,SetClientidFnc,SetDeviceTokenFnc,SetDeviceSecretFnc
local EvtCb = {}
local opts = {}

-------------------------------------------------------
---- FOTA 相关
-------------------------------------------------------
-- fota的回调函数
local function libfota_cb(result)
    log.info("fota", "result", result)
    -- fota成功
    if result == 0 then
        rtos.reboot()
    end
end
--收到云端固件升级通知消息时的回调函数
local function aliyun_upgrade(payload)
    local jsonData, result = json.decode(payload)
    if result and jsonData.data and jsonData.data.url then
        log.info("aliyun", "ota.url", jsonData.data.url)
        libfota.request(EvtCb["ota"] or libfota_cb, jsonData.data.url)
    end
end

-------------------------------------------------------
--- 用户侧的回调处理
-------------------------------------------------------



--底层libMQTT回调函数，上层的回调函数，通过 aliyun.on注册
local function mqtt_cbevent(mqtt_client, event, data, payload,metas)
    log.debug("aliyun", "event", event, "data", data)
    if event == "conack" then
        log.info("aliyun", "conack")
        -- if opts.ProductKey and opts.DeviceName then
        aliyun.subscribe("/ota/device/upgrade/"..opts.ProductKey.."/"..opts.DeviceName,1)
        aliyun.publish("/ota/device/inform/"..opts.ProductKey.."/"..opts.DeviceName,1,"{\"id\":1,\"params\":{\"version\":\"".._G.VERSION.."\"}}")
        -- end
        sys.publish("aliyun_conack")
        if EvtCb["connect"] then
            EvtCb["connect"](true)
        end
    elseif event == "recv" then -- 服务器下发的数据
        log.debug("aliyun", "downlink", "topic", data, "payload", payload)
        --OTA消息
        if data =="/ota/device/upgrade/".. opts.ProductKey.."/".. opts.DeviceName then
            aliyun_upgrade(payload)
        end

        if EvtCb["receive"] then
            EvtCb["receive"](data, payload, metas.qos, metas.retain, metas.dup)
        end
    elseif event == "sent" then
        log.info("aliyun", "sent", data)
        if data then
            sys.publish("aliyun_evt", "sent", data)
            if EvtCb["publish"] then
                EvtCb["publish"](data)
            end
        end
    elseif event == "disconnect" then
        log.info("aliyun", "disconnect")
        if EvtCb["connect"] then
            EvtCb["connect"](false)
        end
    end
end

local function mqtt_task(mqtt_host, mqtt_port, mqtt_isssl, client_id, user_name, password)
    mqttc = mqtt.create(nil,mqtt_host, mqtt_port or 1883, mqtt_isssl)  --mqtt客户端创建
    log.debug("aliyun", "mqtt三元组", client_id,user_name,password)
    mqttc:auth(client_id,user_name,password) --mqtt三元组配置

    mqttc:keepalive(300) -- 默认值240s
    mqttc:autoreconn(true, 20000) -- 自动重连机制
    mqttc:on(mqtt_cbevent)  --mqtt回调注册
    -- mqttc:debug(true)
    mqttc:connect()
end

---------------------------------------------------
---              一型一密
--------------------------------------------------

-- 二次连接, 也就是真正的连接
local function clientDataTask(DeviceName,ProductKey,mqtt_host,mqtt_port,mqtt_isssl,passtoken,Registration)
    sys.taskInit(function()
        if not Registration then -- 预注册
            local client_id,user_name,password = iotauth.aliyun(opts.ProductKey, opts.DeviceName,SetDeviceSecretFnc)
            -- mqttc = mqtt.create(nil,opts.mqtt_host, opts.mqtt_port, opts.mqtt_isssl)  --mqtt客户端创建
            -- mqttc:auth(client_id,user_name,password) --mqtt三元组配置
            mqtt_task(mqtt_host or opts.mqtt_host, mqtt_port or opts.mqtt_port, mqtt_isssl or opts.mqtt_isssl, client_id, user_name, password)
        else -- 免预注册
            -- mqttc = mqtt.create(nil,opts.mqtt_host, opts.mqtt_port, opts.mqtt_isssl)  --mqtt客户端创建
            -- mqttc:auth(opts.DeviceName, opts.ProductKey, passtoken) --mqtt三元组配置
            mqtt_task(mqtt_host or opts.mqtt_host, mqtt_port or opts.mqtt_port, mqtt_isssl or opts.mqtt_isssl, opts.DeviceName, opts.ProductKey, passtoken)
        end
    end)
end

--根据返回的数据进行二次加密
local function directProc(DeviceName,ProductKey,mqtt_host,mqtt_port,mqtt_isssl,Registration)
    if not Registration then
        local ClientId = DeviceName.."|securemode=3,signmethod=hmacmd5,timestamp=789|"
        local UserName = DeviceName.."&"..ProductKey

        local content = "ClientId"..DeviceName.."deviceName"..DeviceName.."productKey"..ProductKey.."timestamp789"
        local signKey= SetDeviceSecretFnc
        PassWord = crypto.hmac_md5(content,signKey)

        clientDataTask(ClientId,UserName,PassWord,mqtt_host,mqtt_port,mqtt_isssl,DeviceName,ProductKey)
    else
        local ClientId = SetClientidFnc.."|securemode=-2,authType=connwl|"
        local UserName = DeviceName.."&"..ProductKey
        local PassWord = SetDeviceTokenFnc

        clientDataTask(ClientId,UserName,mqtt_host,mqtt_port,mqtt_isssl,PassWord,Registration)
    end
end

--获取一型一密的连接参数
local function clientEncryptionTask(Registration,DeviceName,ProductKey,ProductSecret,InstanceId,mqtt_host,mqtt_port,mqtt_isssl)
    sys.taskInit(function()
        --预注册
        if not Registration then
            ClientId = DeviceName.."|securemode=2,authType=register,random=123,signmethod=hmacmd5|"
        --免预注册
        else
            if InstanceId and #InstanceId > 0 then
                ClientId = DeviceName.."|securemode=-2,authType=regnwl,random=123,signmethod=hmacmd5,instanceId="..InstanceId.."|"
            else
                ClientId = DeviceName.."|securemode=-2,authType=regnwl,random=123,signmethod=hmacmd5|"
            end
        end
        local UserName = DeviceName.."&"..ProductKey
        local content = "deviceName"..DeviceName.."productKey"..ProductKey.."random123"
        local PassWord = crypto.hmac_md5(content, ProductSecret)

        local mqttClient = mqtt.create(nil, mqtt_host, mqtt_port, true)  --客户端创建
        if mqttClient == nil then
            log.error("aliyun", "一型一密要求固件支持TLS加密, 当前固件不支持!!!")
            return
        end
        log.debug("aliyun", "一型一密认证三元组", ClientId, UserName, PassWord)
        mqttClient:auth(ClientId,UserName,PassWord) --三元组配置
        mqttClient:autoreconn(true, 30000)
        local flag = true
        mqttClient:on(function(mqtt_client, event, data, payload)  --mqtt回调注册
            if event == "recv" then
                -- 无需订阅topic, 阿里云会主动下发通知
                log.info("aliyun", "downlink", "topic", data, "payload", payload)
                if payload then
                    local tJsonDecode,res = json.decode(payload)
                    if not Registration then
                        --预注册
                        if res and tJsonDecode["deviceName"] and tJsonDecode["deviceSecret"] then
                            SetDeviceSecretFnc = tJsonDecode["deviceSecret"]
                            log.debug("aliyun", "一型一密(预注册)", tJsonDecode["deviceName"], SetDeviceSecretFnc)
                            if EvtCb["reg"] then
                                EvtCb["reg"](tJsonDecode)
                            else
                                aliyun.store(tJsonDecode)
                            end
                            mqttClient:autoreconn(false)
                            mqttClient:disconnect()
                            flag = false
                            clientDataTask(DeviceName,ProductKey,mqtt_host,mqtt_port,mqtt_isssl)
                        end
                    else
                         --免预注册
                        if res and tJsonDecode["deviceName"] and tJsonDecode["deviceToken"] then
                            SetDeviceTokenFnc = tJsonDecode["deviceToken"]
                            SetClientidFnc = tJsonDecode["clientId"]
                            log.debug("aliyun", "一型一密(免预注册)", SetClientidFnc, SetDeviceTokenFnc)
                            if EvtCb["reg"] then
                                EvtCb["reg"](tJsonDecode)
                            else
                                aliyun.store(tJsonDecode)
                            end
                            mqttClient:autoreconn(false)
                            mqttClient:disconnect()
                            flag = false
                            directProc(DeviceName, ProductKey, mqtt_host, mqtt_port, mqtt_isssl, Registration)
                        end
                    end
                end
            end
        end)
        mqttClient:connect()
        while flag do
            sys.wait(1000)
        end
    end)
end

---------------------------------------------
-- 一机一密
---------------------------------------------
local function confiDentialTask()
    sys.taskInit(function()
        local client_id,user_name,password = iotauth.aliyun(opts.ProductKey, opts.DeviceName, opts.DeviceSecret)
        mqtt_task(opts.mqtt_host, opts.mqtt_port, opts.mqtt_isssl, client_id, user_name, password)
    end)
end


--正常连接 预注册一型一密获取DeviceSecret后就是正常的一机一密连接
function aliyun.clientGetDirectDataTask(DeviceName,ProductKey,mqtt_host,mqtt_port,mqtt_isssl,Registration,DeviceSecret,deviceToken,cid)
    sys.taskInit(function()
        if not Registration then
            local client_id,user_name,password = iotauth.aliyun(ProductKey,DeviceName,DeviceSecret)
            -- mqttc = mqtt.create(nil,mqtt_host, mqtt_port,mqtt_isssl)  --mqtt客户端创建
            -- mqttc:auth(client_id,user_name,password) --mqtt三元组配置
            mqtt_task(mqtt_host, mqtt_port,mqtt_isssl, client_id, user_name, password)
        else
            local clientId = cid.."|securemode=-2,authType=connwl|"
            local client_id,user_name,password = iotauth.aliyun(ProductKey,DeviceName,deviceToken)
            -- mqttc = mqtt.create(nil,mqtt_host, mqtt_port,mqtt_isssl)  --mqtt客户端创建
            -- mqttc:auth(clientId, user_name, deviceToken) --mqtt三元组配置
            mqtt_task(mqtt_host, mqtt_port,mqtt_isssl, clientId, user_name, deviceToken)
        end
    end)
end


--[[
订阅主题
@api aliyun.subscribe(topic,qos)
@string 主题内容为UTF8编码
@number qos为number类型(0/1，默认1)
@return nil
@usage
aliyun.subscribe("/b0FMK1Ga5cp/862991234567890/get", 1)
]]
function aliyun.subscribe(topic,qos)
    if mqttc and mqttc:ready() then
        mqttc:subscribe(topic,qos or 1)
    end
end


--[[
发布一条消息
@api aliyun.publish(topic,qos,payload,cbFnc,cbPara)
@string UTF8编码的主题
@number qos质量等级，0/1，默认0
@string payload 负载内容，UTF8编码
@function cbFnc 消息发布结果的回调函数,回调函数的调用形式为：cbFnc(result,cbPara)。result为true表示发布成功，false或者nil表示订阅失败；cbPara为本接口中的第5个参数
@param cbPara 消息发布结果回调函数的回调参数
@return nil
@usage
aliyun.publish("/b0FMK1Ga5cp/862991234567890/update",0,"test")
aliyun.publish("/b0FMK1Ga5cp/862991234567890/update",1,"test",cbFnc,"cbFncPara")
]]
function aliyun.publish(topic,qos,payload,cbFnc,cbPara)
    if mqttc and mqttc:ready() then
        local pkgid = mqttc:publish(topic, payload, qos)
        if cbFnc then
            if pkgid then
                sys.taskInit(function()
                    local timeout = 15000
                    while timeout > 0 do
                        local result, evt, tmp = sys.waitUntil("aliyun_evt", 1000)
                        -- log.debug("aliyun", "等待publish的sent事件", result, evt, tmp)
                        if evt == "sent" and pkgid == tmp then
                            cbFnc(true, cbPara)
                            return
                        end
                        timeout = timeout - 1000
                    end
                    cbFnc(false, cbPara)
                end)
            else
                cbFnc(true, cbPara)
            end
        end
    else
        cbFnc(false, cbPara)
    end
end


--[[
注册事件的处理函数
@api aliyun.on(evt,cbFnc)
@string evt事件，
"connect"表示接入服务器连接结果事件，
"receive"表示接收到接入服务器的消息事件，
"publish"表示发送消息的结果事件
@function cbFnc 事件的处理函数
当evt为"connect"时，cbFnc的调用形式为：cbFnc(result)，result为true表示连接成功，false或者nil表示连接失败，
当evt为"receive"时，cbFnc的调用形式为：cbFnc(topic,payload)，topic为UTF8编码的主题(string类型)，payload为原始编码的负载(string类型)，
当evt为"publish"时，cbFnc的调用形式为：cbFnc(result)，result为true表示发送成功，false或者nil表示发送失败
@return nil
@usage
aliyun.on("connect",cbFnc)
]]
function aliyun.on(evt,cbFnc)
	EvtCb[evt] = cbFnc
end


--[[
@api aliyun.getDeviceSecret()
@return string 预注册一型一密阿里云返回的DeviceSecret
可以在应用层使用kv区来保存该参数并使用判断来避免重启后无法连接
]]
function aliyun.getDeviceSecret()
    return SetDeviceSecretFnc
end

--[[
@api aliyun.getDeviceToken()
@return string 免预注册一型一密阿里云返回的DeviceToken
可以在应用层使用kv区来保存该参数并使用判断来避免重启后无法连接
]]
function aliyun.getDeviceToken()
    return SetDeviceTokenFnc
end

--[[
@api aliyun.getClientid()
@return string 免预注册一型一密阿里云返回的Clientid
可以在应用层使用kv区来保存该参数并使用判断来避免重启后无法连接
]]
function aliyun.getClientid()
    return SetClientidFnc
end



--[[
配置阿里云物联网套件的产品信息和设备信息
@api aliyun.setup(tPara)
@table 阿里云物联网套件的产品信息和设备信息
@return nil
@usage
aliyun.setup(tPara)
-- 参数说明
一机一密认证方案时，ProductSecret参数传入nil
一型一密认证方案时，ProductSecret参数传入真实的产品密钥
Registration 是否是预注册 已预注册为false，未预注册为true
DeviceName 设备名称
ProductKey 产品key
ProductSecret 产品secret，根据此信息判断是一机一密还是一型一密
DeviceSecret 设备secret
InstanceId 如果没有注册需要填写实例id，在实例详情页面
mqtt_port mqtt端口
mqtt_isssl 是否使用ssl加密连接，true为无证书最简单的加密
]]
function aliyun.setup(tPara)
    opts = tPara
    aliyun.opts = opts
    if not opts.mqtt_host then
        if opts.host then
            opts.mqtt_host = opts.host
        elseif tPara.InstanceId and #tPara.InstanceId > 0 then
            opts.mqtt_host = tPara.InstanceId..".mqtt.iothub.aliyuncs.com"
        else
            opts.mqtt_host = tPara.ProductKey .. ".iot-as-mqtt."..tPara.RegionId..".aliyuncs.com"
        end
    end
    -- log.debug("aliyun", "mqtt host", opts.mqtt_host)
    if not tPara.ProductSecret or #tPara.ProductSecret == 0 then
        log.info("aliyun", "一机一密模式")
        confiDentialTask()
    else
        log.info("aliyun", string.format("一型一密(%s)模式 - %s", tPara.Registration and "预注册" or "免预注册", tPara.reginfo and "已获取注册信息" or "开始获取注册信息"))
        if tPara.reginfo then
            aliyun.clientGetDirectDataTask(tPara.DeviceName,tPara.ProductKey, opts.mqtt_host, 1883, tPara.mqtt_isssl,
                tPara.Registration,
                tPara.deviceSecret, tPara.deviceToken, tPara.clientId)
        else
            clientEncryptionTask(tPara.Registration,tPara.DeviceName,tPara.ProductKey,tPara.ProductSecret,tPara.InstanceId, opts.mqtt_host, 1883,tPara.mqtt_isssl)
        end
    end
end

--[[
判断阿里云物联网套件是否已经连接
@api aliyun.ready()
@return boolean 阿里云物联网套件是否已经连接
@usage
-- 本函数于2024.6.17新增
if aliyun.ready() then
    log.info("aliyun", "已连接")
end
]]
function aliyun.ready()
    if mqttc and mqttc:ready() then
        return true
    end
end

--[[
获取或存储注册信息
@api aliyun.store(result)
@table result 注册结果，如果为nil则表示获取注册信息
@return table 注册信息，如果为nil则表示获取失败
@usage
-- 获取注册信息
local store = aliyun.store()
-- 存储注册信息
aliyun.store(result)
]]
function aliyun.store(result)
    if result then
        log.debug("aliyun", "注册结果", json.encode(result))
        if fskv then
            fskv.set("ProductKey", result["productKey"])
            fskv.set("DeviceName",result["deviceName"])
            if result["deviceSecret"] then
                fskv.set("deviceSecret",result["deviceSecret"])
            else
                fskv.set("deviceToken",result["deviceToken"])
                fskv.set("clientId", result["clientId"])
            end
        else
            log.debug("aliyun", "fskv not found, use io/fs")
            io.writeFile("/alireg.json", json.encode(result))
        end
    else
        local store = {}
        if fskv then
            store.deviceName = fskv.get("DeviceName") or fskv.get("deviceName")
            store.productKey = fskv.get("ProductKey") or fskv.get("productKey")
            store.deviceSecret = fskv.get("deviceSecret")
            store.deviceToken = fskv.get("deviceToken")
            store.clientid = fskv.get("clientid")
        else
            local tmp = io.readFile("/alireg.json")
            if tmp then
                store = json.decode(tmp)
                if not store then
                    store = {}
                end
            end
        end
        return store
    end
end

return aliyun
