-- Copyright (c) 2024 Huawei Technologies Co., Ltd.
-- openUBMC is licensed under Mulan PSL v2.
-- You can use this software according to the terms and conditions of the Mulan PSL v2.
-- You may obtain a copy of Mulan PSL v2 at: http://license.coscl.org.cn/MulanPSL2
-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
-- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
-- MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
-- See the Mulan PSL v2 for more details.

local class = require 'mc.class'
local utils = require 'device_tree.adapters.power_mgmt.utils'
local utils_core = require 'utils.core'
local skynet = require 'skynet'
local ctx = require 'mc.context'
local log = require 'mc.logging'
local org_freedesktop_dbus = require 'sd_bus.org_freedesktop_dbus'
local fw_def = require 'device_tree.adapters.power_mgmt.protocol.upgrade.fw_def'
local match_rule = org_freedesktop_dbus.MatchRule
local debounce = require 'mc.debounce.debounce'
local enums = require 'macros.power_mgmt_enums'
local add_event = require 'device_tree.adapters.power_mgmt.add_event'

local REMOTE_POWER_MONITOR_SIG_PATH<const> = '/bmc/kepler/power_mgmt/psu'

local TIME_SERVICE_INTERVAL<const> = 1000 * 60 * 5
local IS_NOT_HEALTH<const> = 0xff
local READ_FAIL_DEBOUNCE_COUNT <const> = 3
local E_OK = nil -- 函数执行成功返回nil
local E_FAILED = '' -- 空错误信息
local HEALTH_UPDATE_FAIL <const> = 0
local POWER_WATTS_UPDATE_FAIL <const> = 1
local PLUGIN_DEBOUNCE <const> = 2
-- 电源通讯异常初始值
local COMMUNICATION_STATUS_DEFAULT <const> = -1
local PAGE_PRIMARY<const> = 0
local PAGE_SECONDARY<const> = 1
local OUTPUT_METRICS<const> = {'output_voltage', 'output_current_amps', 'output_power_watts'}

local function get_pbit(val)
    return 1 << val
end

return function (obj)
    local psu_slot = class(obj)

    function psu_slot:ctor(protocol)
        self.protocol = protocol
        self.remote_power_monitor_task = false
        self.read_fail_count = 0
        self.remote_powe_signal = false
        self.last_read_interval = 10
        self.read_interval = 100
        self.read_output_metrics_fail_count = 0
        self.read_status_a_fail_count = 0
        self.read_status_b_fail_count = 0
        self.switch_page_a_fail = 255
        self.switch_page_b_fail = 255
        self.monitor_tasks = {}
        self.comm_status = {
            [HEALTH_UPDATE_FAIL] = COMMUNICATION_STATUS_DEFAULT,
            [POWER_WATTS_UPDATE_FAIL] = COMMUNICATION_STATUS_DEFAULT,
            [PLUGIN_DEBOUNCE] = debounce[debounce.DEBOUNCED_CONT_BIN].new(3, 1, 0)
        }
    end

    function psu_slot:is_present()
        return self.Presence == 1
    end

    function psu_slot:add_monitor_tasks(cb)
        table.insert(self.monitor_tasks, self:next_tick(cb))
    end

    function psu_slot:stop_monitor_tasks()
        local ts = self.monitor_tasks
        self.monitor_tasks = {}
        for _, task in pairs(ts) do
            task:stop()
        end
    end

    function psu_slot:psm_monitor_stop()
        self:stop_monitor_tasks()
        self:stop_remote_power_monitor()
    end

    function psu_slot:fetch_power_supply_info()
        self:get_power_supply_info('manufacturer')
        self:get_power_supply_info('model')
        self:get_power_supply_info('production_date')
        self:get_power_supply_info('product_version')
        self:get_power_supply_info('firmware_version')
        self:get_power_supply_info('serial_number')
        self:get_power_supply_info('part_number')
    end

    function psu_slot:remote_power_monitor()
        if self.remote_power_monitor_task and self.read_interval == self.last_read_interval then
            return true
        end
        local chip = self.PsuChip[enums.PSU_CHIP_OBJECT.BLOCKIO]
        local ok, resp = pcall(function ()
            local args = skynet.packstring(self.ps_id, REMOTE_POWER_MONITOR_SIG_PATH .. self.ps_id, self.read_interval)
            return skynet.unpack(chip:PluginRequest(ctx.new(), 'power_mgmt', "start_power_monitor", args))
        end)
        if not ok then
            local debounced_val = self.comm_status[PLUGIN_DEBOUNCE]:get_debounced_val(1)
            if debounced_val == 1 then
                self:process_communication_status(POWER_WATTS_UPDATE_FAIL, 1)
            end
            return
        end
        self.comm_status[PLUGIN_DEBOUNCE]:get_debounced_val(0)
        log:notice('start psu power monitor remote task, ps_id=%s, task_id=%s', self.ps_id, resp)
        self.last_read_interval = self.read_interval
        self.remote_power_monitor_task = resp
    end

    function psu_slot:start_to_receive_remote_power()
        local sig = match_rule.signal('UpdateProps'):with_path_namespace(REMOTE_POWER_MONITOR_SIG_PATH .. self.ps_id)
        self.remote_powe_signal = self.bus:match(sig, function(message)
            local _, need_update_vlaue = message:read('a{ss}a{sv}')
            for prop, val in pairs(need_update_vlaue) do
                if prop == 'communication_status' then
                    self:process_communication_status(POWER_WATTS_UPDATE_FAIL, val)
                else
                    log:debug("update %s to %s", prop, val)
                    self:dal_refresh_property(prop, val)
                    self.scan_status = self.scan_status | get_pbit(1)
                    self:dal_refresh_property('scan_status', self.scan_status)
                end
            end
        end)
    end

    function psu_slot:stop_remote_power_monitor()
        log:notice('stop psu power monitor remote task, ps_id=%s, monitor task_id=%s',
            self.ps_id, self.remote_power_monitor_task)
        local args = skynet.packstring(self.remote_power_monitor_task)
        self.PsuChip[enums.PSU_CHIP_OBJECT.BLOCKIO]:PluginRequest(ctx.new(), 'power_mgmt', "stop_power_monitor", args)

        self.remote_power_monitor_task = false
    end

    function psu_slot:process_communication_status(err_type, flag)
        self.comm_status[err_type] = flag
        if err_type == POWER_WATTS_UPDATE_FAIL and flag == 1 then
            -- 电源通讯丢失，设置电源输入功耗扫描状态位为0
            self.scan_status = self.scan_status & ~get_pbit(1)
            self:dal_refresh_property('scan_status', self.scan_status)
            self:dal_refresh_property('input_power_watts', 0)
        end
        if self.comm_status[HEALTH_UPDATE_FAIL] == COMMUNICATION_STATUS_DEFAULT or
            self.comm_status[POWER_WATTS_UPDATE_FAIL] == COMMUNICATION_STATUS_DEFAULT then
            return
        end

        if self:is_present() and self.comm_status[HEALTH_UPDATE_FAIL] == 1
            and self.comm_status[POWER_WATTS_UPDATE_FAIL] == 1 then
            log:debug('update communication_status to 1')
            self:dal_refresh_property('communication_status', 1)
        else
            log:debug('update communication_status to 0')
            self:dal_refresh_property('communication_status', 0)
        end
    end

    -- scan_status 位定义
    -- bit 0: A路健康状态采集成功
    -- bit 1: 输入功率扫描成功  
    -- bit 2: 输入参数采集成功
    -- bit 3: (保留)
    -- bit 4: B路健康状态采集成功
    -- bit 5: A路输出参数采集成功
    function psu_slot:apply_status_bit(bit_val, counter, failed_flag, bit_pos)
        if failed_flag then
            counter = counter + 1
            if counter < READ_FAIL_DEBOUNCE_COUNT then
                return bit_val, counter
            end
            return bit_val & ~get_pbit(bit_pos), counter
        else
            return bit_val | get_pbit(bit_pos), 0
        end
    end

    function psu_slot:set_page_with_retry(page, retries)
        for i = 1, retries do
            local ok = self.protocol:set_page(page)
            if ok then
                return E_OK, i - 1
            end
        end
        return E_FAILED, retries
    end

    -- 输入告警采用 A -> B -> 上一轮 的优先级
    function psu_slot:pick_with_prev(a_val, b_val, prev_val)
        if a_val then
            return a_val
        end
        if b_val then
            return b_val
        end
        return prev_val
    end

    -- 输出告警采用 A/B 按位或；同字段A/B都无值时回退上一轮
    function psu_slot:merge_or_with_prev(a_val, b_val, prev_val)
        if not a_val and not b_val then
            return prev_val
        end
        return (a_val or 0) | (b_val or 0)
    end

    function psu_slot:collect_primary_page_data()
        local ret, rsp, value
        local failed_flag_a = false
        local health_event_a

        -- ========== A路采集 ==========
        -- 切到A路
        local page_a_ret = self:set_page_with_retry(PAGE_PRIMARY, READ_FAIL_DEBOUNCE_COUNT)
        local page_a_ok = (page_a_ret == E_OK)
        if not page_a_ok then
            -- 切A路失败：产生维护事件，跳过A路参数和告警读取
            if self.switch_page_a_fail == 0 or self.switch_page_a_fail == 255 then
                add_event.get_instance().generate_switch_page_failure('true', self.ps_id, 'A')
                self.switch_page_a_fail = 1
            end
            failed_flag_a = true
            self.scan_status, self.read_output_metrics_fail_count = self:apply_status_bit(
                self.scan_status, self.read_output_metrics_fail_count, true, 5)
            self.scan_status, self.read_status_a_fail_count = self:apply_status_bit(
                self.scan_status, self.read_status_a_fail_count, true, 0)
            return health_event_a, failed_flag_a
        end

        if self.switch_page_a_fail == 1 or self.switch_page_a_fail == 255 then
            add_event.get_instance().generate_switch_page_failure('false', self.ps_id, 'A')
            self.switch_page_a_fail = 0
        end

        -- 读A路输出参数
        for _, metric in ipairs(OUTPUT_METRICS) do
            rsp, value = self:get_dynamic_data(metric)
            log:debug('ps%d read %s: %s', self.ps_id, metric, value or 'nil')
            ret = ret or rsp
        end
        self.scan_status, self.read_output_metrics_fail_count =
            self:apply_status_bit(self.scan_status, self.read_output_metrics_fail_count, ret ~= E_OK, 5)

        -- 读A路告警
        -- 协议层按PAGE维护A/B两份health_event缓存，寄存器读失败时保留该页旧字段值。
        health_event_a, failed_flag_a = self.protocol:get_health_event(PAGE_PRIMARY)
        log:debug(
            'ps%d read health_event_a: input_voltage_fault=%s, temper_fault=%s, output_voltage_fault=%s, output_current_fault=%s, cml_fault=%s, other_fault=%s, mfr_specific_status=%s, event=%s',
            self.ps_id,
            health_event_a and health_event_a.input_voltage_fault or 'nil',
            health_event_a and health_event_a.temper_fault or 'nil',
            health_event_a and health_event_a.output_voltage_fault or 'nil',
            health_event_a and health_event_a.output_current_fault or 'nil',
            health_event_a and health_event_a.cml_fault or 'nil',
            health_event_a and health_event_a.other_fault or 'nil',
            health_event_a and health_event_a.mfr_specific_status or 'nil',
            health_event_a and health_event_a.event or 'nil')
        if not health_event_a or failed_flag_a then
            log:debug('get health_event_a failed')
        end
        if not health_event_a then
            failed_flag_a = true
        end
        self.scan_status, self.read_status_a_fail_count = self:apply_status_bit(
            self.scan_status, self.read_status_a_fail_count, failed_flag_a, 0)
        return health_event_a, failed_flag_a
    end

    function psu_slot:collect_secondary_page_data()
        local failed_flag_b = false
        local health_event_b
        -- ========== B路采集 ==========
        -- 切到B路
        local page_b_ret = self:set_page_with_retry(PAGE_SECONDARY, READ_FAIL_DEBOUNCE_COUNT)
        local page_b_ok = (page_b_ret == E_OK)
        if not page_b_ok then
            -- 切B路失败：产生维护事件，跳过B路告警读取
            if self.switch_page_b_fail == 0 or self.switch_page_b_fail == 255 then
                add_event.get_instance().generate_switch_page_failure('true', self.ps_id, 'B')
                self.switch_page_b_fail = 1
            end
            failed_flag_b = true
            self.scan_status, self.read_status_b_fail_count = self:apply_status_bit(
                self.scan_status, self.read_status_b_fail_count, true, 4)
            return health_event_b, failed_flag_b
        end

        if self.switch_page_b_fail == 1 or self.switch_page_b_fail == 255 then
            add_event.get_instance().generate_switch_page_failure('false', self.ps_id, 'B')
            self.switch_page_b_fail = 0
        end
        -- 读B路告警
        -- B路同样读取独立缓存，便于monitor直接做A/B合并。
        health_event_b, failed_flag_b = self.protocol:get_health_event(PAGE_SECONDARY)
        log:debug(
            'ps%d read health_event_b: input_voltage_fault=%s, temper_fault=%s, output_voltage_fault=%s, output_current_fault=%s, cml_fault=%s, other_fault=%s, mfr_specific_status=%s, event=%s',
            self.ps_id,
            health_event_b and health_event_b.input_voltage_fault or 'nil',
            health_event_b and health_event_b.temper_fault or 'nil',
            health_event_b and health_event_b.output_voltage_fault or 'nil',
            health_event_b and health_event_b.output_current_fault or 'nil',
            health_event_b and health_event_b.cml_fault or 'nil',
            health_event_b and health_event_b.other_fault or 'nil',
            health_event_b and health_event_b.mfr_specific_status or 'nil',
            health_event_b and health_event_b.event or 'nil')
        if not health_event_b then
            log:debug('get health_event_b failed')
            failed_flag_b = true
        end
        self.scan_status, self.read_status_b_fail_count = self:apply_status_bit(
            self.scan_status, self.read_status_b_fail_count, failed_flag_b, 4)
        return health_event_b, failed_flag_b
    end

    function psu_slot:merge_health_event_with_prev(health_event_a, health_event_b, prev_health_event)
        -- A/B路状态综合处理，确保任何一路输出状态异常均能上报告警
        return {
            -- 输入状态优先A路，再回退B路，最后回退上一轮值
            input_voltage_fault = self:pick_with_prev(
                health_event_a and health_event_a.input_voltage_fault,
                health_event_b and health_event_b.input_voltage_fault,
                prev_health_event.input_voltage_fault),
            -- 输出状态按A/B按位或，同字段A/B均无值时回退上一轮
            temper_fault = self:merge_or_with_prev(
                health_event_a and health_event_a.temper_fault,
                health_event_b and health_event_b.temper_fault,
                prev_health_event.temper_fault),
            output_voltage_fault = self:merge_or_with_prev(
                health_event_a and health_event_a.output_voltage_fault,
                health_event_b and health_event_b.output_voltage_fault,
                prev_health_event.output_voltage_fault),
            output_current_fault = self:merge_or_with_prev(
                health_event_a and health_event_a.output_current_fault,
                health_event_b and health_event_b.output_current_fault,
                prev_health_event.output_current_fault),
            cml_fault = self:merge_or_with_prev(
                health_event_a and health_event_a.cml_fault,
                health_event_b and health_event_b.cml_fault,
                prev_health_event.cml_fault),
            other_fault = self:merge_or_with_prev(
                health_event_a and health_event_a.other_fault,
                health_event_b and health_event_b.other_fault,
                prev_health_event.other_fault),
            mfr_specific_status = self:merge_or_with_prev(
                health_event_a and health_event_a.mfr_specific_status,
                health_event_b and health_event_b.mfr_specific_status,
                prev_health_event.mfr_specific_status),
            event = self:merge_or_with_prev(
                health_event_a and health_event_a.event,
                health_event_b and health_event_b.event,
                prev_health_event.event)
        }
    end

    function psu_slot:update_health_status_and_props(failed_flag_a, failed_flag_b)
        -- 通讯异常状态：A/B两路健康状态均读取失败时，置HEALTH_UPDATE_FAIL=1
        local all_health_fail = (self.read_status_a_fail_count >= READ_FAIL_DEBOUNCE_COUNT)
            and (self.read_status_b_fail_count >= READ_FAIL_DEBOUNCE_COUNT)
        self:process_communication_status(HEALTH_UPDATE_FAIL, all_health_fail and 1 or 0)

        -- 更新资源树属性
        self:dal_refresh_property('scan_status', self.scan_status)
        if failed_flag_a or failed_flag_b then
            self:dal_refresh_property('health', get_pbit(4))
        else
            self:dal_refresh_property('health', self.health_event.event)
        end
        self:dal_refresh_property('health_event', self.health_event)
    end

    function psu_slot:update_dual_output()
        local prev_health_event = self.health_event or {}
        local health_event_a, failed_flag_a = self:collect_primary_page_data()
        local health_event_b, failed_flag_b = self:collect_secondary_page_data()

        -- ========== 状态合并与更新 ==========
        self.health_event = self:merge_health_event_with_prev(health_event_a, health_event_b, prev_health_event)
        self:update_health_status_and_props(failed_flag_a, failed_flag_b)
        return E_OK
    end

    -- power_monitor 实际功率任务
    function psu_slot:power_monitor()
        if self:update_dual_output() ~= E_OK then
            -- 接口调用失败， 获取不到属性，直接返回
            return E_FAILED
        end
        if self.prop_value.health_event.event & get_pbit(4) == 0 then
            -- 开始监听 InputPowerWatts属性（频率100ms）
            self:remote_power_monitor()
        end
        return E_OK
    end

    function psu_slot:psm_task_power()
        local ret, _ = self:get_dynamic_data('input_power_watts')
        if ret == E_OK then
            self.scan_status = self.scan_status | get_pbit(1)
            self:dal_refresh_property('scan_status', self.scan_status)
        end

        self.read_output_metrics_fail_count = 0
        self.read_status_a_fail_count = 0
        self.read_status_b_fail_count = 0
        self.switch_page_a_fail = 255
        self.switch_page_b_fail = 255
        while true do
            self:power_monitor()
            self:sleep_ms(500)
        end
    end

    -- 查询电源输入 电流/电压 等信息
    function psu_slot:psm_task_v_i()
        local ret, rsp, _
        local read_fail_count = 0
        while true do
            rsp, _ = self:get_dynamic_data('rate')
            ret = ret or rsp
            rsp, _ = self:get_dynamic_data('input_current_amps')
            ret = ret or rsp

            rsp, _ = self:get_dynamic_data('input_voltage')
            ret = ret or rsp
            rsp, _ = self:get_dynamic_data('power_supply_type')
            ret = ret or rsp
            rsp, _ = self:get_dynamic_data('env_temperature_celsius')
            ret = ret or rsp
            rsp, _ = self:get_dynamic_data('primary_chip_temperature_celsius')
            ret = ret or rsp
            rsp, _ = self:get_dynamic_data('secondary_chip_temperature_celsius')
            ret = ret or rsp

            if ret ~= E_OK then
                if read_fail_count < READ_FAIL_DEBOUNCE_COUNT then
                    read_fail_count = read_fail_count + 1
                else
                    self.scan_status = self.scan_status & ~get_pbit(2)
                end
            else
                read_fail_count = 0
                self.scan_status = self.scan_status | get_pbit(2)
            end
            self:dal_refresh_property('scan_status', self.scan_status)
            ret = nil
            self:sleep_ms(3000)
        end
    end

    function psu_slot:time_service_task()
        while true do
            self.protocol:time_service()
            self:sleep_ms(TIME_SERVICE_INTERVAL)
        end
    end

    function psu_slot:power_monitor_entry()
        self:add_monitor_tasks(
            function()
                self:start_to_receive_remote_power()
                self:psm_task_power()
            end
        )
        self:add_monitor_tasks(
            function()
                self:psm_task_v_i()
            end
        )
        self:add_monitor_tasks(
            function()
                self:time_service_task()
            end
        )
    end

    function psu_slot:power_monitor_start()
        local ok, uptime = pcall(utils_core.get_bmc_uptime)
        if not ok then
            log:error('Get bmc uptime failed')
        else
            log:notice('Current bmc uptime is %s', uptime)
            local wait_time = enums.UPTIME - uptime
            if wait_time > 0 then
                skynet.sleep(wait_time * 100) -- BMC启动前UPTIME时间内不进行电源数据采集
            end
        end
        log:notice('ps%d power monitor start', self.ps_id)
        self:power_monitor_entry()
    end

    return psu_slot
end
