-- 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 phy_chip = require 'domain.device.switch.phy.phy_chip'
local sw_chip = require 'domain.device.switch.chip.switch_chip'
local log = require 'mc.logging'
local skynet = require 'skynet'
local defs = require 'domain.defs'
local client = require 'lsw.client'
local context = require 'mc.context'
local monitor = require 'domain.debug.monitor'
local alarm = require 'alarm.alarm'
local _, lsw_drv = pcall(require, 'lsw_drv')
local switch = class()

local LSW_BUS_MDIO<const> = 0x1
local mtr = monitor.get_instance()

function switch:ctor(db, unit)
    self.component_order = {
        [1] = 'chip',
        [2] = 'phy'
    }
    self.component = {
        chip = sw_chip.new(db),
        phy = phy_chip.new()
    }
    self.obj = {}
    self.db = db
    self.unit = unit
    self.cfg_success = false
end

function switch:add_object(obj)
    self.component.chip:add_object(obj)
end

function switch:add_port(obj)
    self.component.chip:add_port(obj)
end

function switch:add_vlan(obj)
    self.component.chip:add_vlan(obj)
end

function switch:add_phy(obj)
    self.component.phy:add_object(obj)
end

local RETRY_TIMES<const> = 3
local GE_SWITCHOEM_BUTT<const> = 0xff
function switch:update_oem_type()
    for retry = 1, RETRY_TIMES do
        local res = self.component.chip:update_oem_type()
        if res then
            mtr:update_state(defs.NetConfigIdx.UpdateOemType, true)
            return
        end
        if retry == 2 then
            self:hard_reset()
        end
        skynet.sleep(100)
    end
    mtr:update_state(defs.NetConfigIdx.UpdateOemType, false)
    error('[lsw]update_oem_type fail')
end

function switch:hard_reset()
    for _, v in pairs(self.component_order) do
        self.component[v]:hard_reset()
    end
end

function switch:component_init()
    for _ = 1, 10 do
        local ok, _ = pcall(function()
            for _, v in pairs(self.component_order) do
                self.component[v]:start_init()
            end
        end)

        if ok then
            log:notice('[lsw]component init success')
            return
        end
    end
    log:error('[lsw]component init fail')
end

function switch:port_connect_phy()
    local ok, _ = pcall(function()
        self.component.chip:port_connect_phy(self.component.phy)
    end)
    mtr:update_state(defs.NetConfigIdx.PortConnectPhy, ok)
end

function switch:set_init_res(res)
    local row = self.db:select(self.db.LswInfo):where({Unit = self.unit}):first()
    if not row then
        self.db.LswInfo({
            Unit = self.unit,
            IsCfg = res
        }):save()
    else
        row.IsCfg = res
        row:save()
    end
end

function switch:is_need_init()
    local unit = self.unit
    local row = self.db:select(self.db.LswInfo):where({Unit = unit}):first()
    if not row then
        return true
    end
    return not row.IsCfg
end

function switch:bus_init()
    for _ = 1, 10 do
        local res = lsw_drv.l_bus_init(LSW_BUS_MDIO)
        if res then
            log:notice('[lsw]bus init success')
            mtr:update_state(defs.NetConfigIdx.BusInit, true)
            return
        end
        skynet.sleep(50)
    end
    log:error('[lsw]bus init fail')
    mtr:update_state(defs.NetConfigIdx.BusInit, false)
end

function switch:start_init()
    if not self:is_need_init() then
        log:notice('[lsw]already init success, not need init again')
        self:port_connect_phy()
        self:bus_init()
        self:start_monitor()
        return
    end
    skynet.fork(function()
        local ok, res = pcall(function()
            self:port_connect_phy()
            self:bus_init()
            self:update_oem_type()
            self:component_init()
            self:start_monitor()
        end)
        if not ok then
            self:set_init_res(false)
            log:error('[lsw]init fail, err %s', res)
        else
            self:set_init_res(true)
            pcall(self.cfg_link_up, self)
            log:notice('[lsw]init success')
            mtr:update_state(defs.NetConfigIdx.Finish, true)
        end
        self.component.chip:update_errcode()
    end)
end

function switch:healthy_check_and_recover()
    local ok, res = pcall(function()
        self:update_oem_type()
        local chip_health = self.component.chip:healthy_check()
        if not chip_health then
            log:notice('chip not healthy')
            mtr:init_state()
            self.component.chip:start_init()
            self.component.chip:try_trigger_alarm()
            self.component.phy:start_init(true)
            pcall(self.cfg_link_up, self)
        end
        local phy_health = self.component.phy:healthy_check()
        if not phy_health then
            log:notice('phy not healthy')
            mtr:init_state()
            self.component.phy:start_init(true)
        end

        if not chip_health or not phy_health then
            mtr:update_state(defs.NetConfigIdx.Finish, true)
            self.component.chip:update_errcode()
            -- 仅记录自愈事件
            local alarm_instance = alarm.new()
            alarm_instance:event(defs.ALARM_TYPE.LSW_RECOVERY, self.unit, self.component.chip.obj.SlotNumber, true)
            skynet.sleep(10)
            alarm_instance:event(defs.ALARM_TYPE.LSW_RECOVERY, self.unit, self.component.chip.obj.SlotNumber, false)
        end
    end)
    if not ok then
        self:set_init_res(false)
        log:notice('[lsw]healthy check fail, %s', res)
    else
        self:set_init_res(true)
        mtr:update_state(defs.NetConfigIdx.Finish, true)
        log:info('[lsw]lsw healthy check success')
    end
end

function switch:cfg_link_up()
    if self.component.chip:is_power_off() then
        return
    end
    local objs = client:GetEthConfigObjects()
    local _, obj = next(objs)
    local ok, res
    for _ = 1, 10 do
        ok, res = pcall(function()
            obj:SetSpeed(context.new(), 'eth2', false, 3, 2)
        end)
        if ok then
            log:notice('[lsw]cfg linkup success')
            mtr:update_state(defs.NetConfigIdx.CfgLinkUp, true)
            return
        end
        skynet.sleep(50)
    end
    log:error('[lsw]cfg linkup fail ,err %s', res)
    mtr:update_state(defs.NetConfigIdx.CfgLinkUp, false)
end

function switch:update_port_info()
    local ok, res = pcall(self.component.chip.update_port_info, self.component.chip)
    if not ok then
        log:error("[lsw] update port info fail, err %s", res)
    else
        log:info("[lsw] update port info success")
    end
end

function switch:start_monitor()
    log:notice("[lsw]start monitor")
    skynet.fork(function()
        while true do
            self:update_port_info()
            self:healthy_check_and_recover()
            skynet.sleep(500)
        end
    end)
end

function switch:get_port(port_id)
    if self.component.chip.component.ports[port_id] then
        return self.component.chip.component.ports[port_id]
    end
    return nil
end

function switch:hard_reset_with_device(device)
    if device == defs.LSW_DEVICE.CHIP or device == defs.LSW_DEVICE.ALL then
        self.component.chip:hard_reset()
    end

    if device == defs.LSW_DEVICE.PHY or device == defs.LSW_DEVICE.ALL then
        self.component.phy:hard_reset()
    end
end

function switch:soft_reset_with_device(device)
    if device == defs.LSW_DEVICE.CHIP or device == defs.LSW_DEVICE.ALL then
        self.component.chip:soft_reset()
    end

    if device == defs.LSW_DEVICE.PHY or device == defs.LSW_DEVICE.ALL then
        self.component.phy:soft_reset()
    end
end

function switch:get_port_dynamic_info()
    local msg = ''
    for _, port in pairs(self.component.chip.component.ports) do
        msg = msg .. port:get_port_dynamic_info()
    end
    return msg
end

function switch:get_vlans_info()
    local msg = ''
    for _, vlan in pairs(self.component.chip.component.vlans) do
        msg = msg .. vlan:get_info()
    end
    return msg
end

return switch
