# Copyright (c) Huawei Technologies Co., Ltd. 2026-2026. All rights reserved.
# OpenOLC 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.

import logging
from dataclasses import dataclass
from typing import Dict, Any

from olc.alg.alg_context import AlgContext
from olc.alg.algorithm import T, ParaName

from olc.alg.flow.flow_alg import (
    FlowAbstractAlgorithm,
    MIN_WAL,
    MAX_WAL,
    DELTA,
    CURRENT_USAGE,
)
from olc.event.sny_event import Event

logger = logging.getLogger(__name__)

# PID 参数
KP = "Kp"
KI = "Ki"
KD = "kd"

# 资源使用率目标区间
MIN_RESOURCE = "minResource"
MAX_RESOURCE = "maxResource"


@dataclass
class PidParams:
    kp: float
    ki: float
    kd: float


@dataclass
class ResourceRange:
    min_resource: float
    max_resource: float


class PidAlgorithm(FlowAbstractAlgorithm):
    NAME = "PID"

    def __init__(self, auto: bool = False):
        super().__init__(auto)
        # 设置父类已经定义参数的默认值
        self.para_names[MIN_WAL] = ParaName(MIN_WAL, 0.1)
        self.para_names[MAX_WAL] = ParaName(MAX_WAL, 3)
        self.para_names[DELTA] = ParaName(DELTA, 5)

        # PID 特有参数
        self.para_names[KP] = ParaName(KP, 0.5)
        self.para_names[KI] = ParaName(KI, 0)
        self.para_names[KD] = ParaName(KD, 0.5)
        self.para_names[MIN_RESOURCE] = ParaName(MIN_RESOURCE, 0.5)
        self.para_names[MAX_RESOURCE] = ParaName(MAX_RESOURCE, 0.5)

        # 动态参数
        self.para_names[CURRENT_USAGE] = ParaName(
            CURRENT_USAGE, "localresource.memory", True
        )

        # PID 状态存储
        self._pid_states = {}

        # 订阅规则清理事件
        Event.get_instance().olc_clean_rule.connect(self.subscribe_clean)

    def _get_pid_state(self, group_name):
        if group_name not in self._pid_states:
            self._pid_states[group_name] = {
                "integral": 0.0,
                "lastError": 0.0,
                "lastDirection": 0.0,
            }
        return self._pid_states[group_name]

    def make_decision(self, alg_context: AlgContext[T]) -> T:
        previous = alg_context.previous
        init_result = alg_context.init_result
        group_name = alg_context.group.name if alg_context.group else "default"

        # 获取参数
        kp = alg_context.get_float_value_by_param(KP, self.para_names[KP].default_value)
        ki = alg_context.get_float_value_by_param(KI, self.para_names[KI].default_value)
        kd = alg_context.get_float_value_by_param(KD, self.para_names[KD].default_value)
        delta = alg_context.get_float_value_by_param(
            DELTA, self.para_names[DELTA].default_value
        )
        min_resource = alg_context.get_float_value_by_param(
            MIN_RESOURCE, self.para_names[MIN_RESOURCE].default_value
        )
        max_resource = alg_context.get_float_value_by_param(
            MAX_RESOURCE, self.para_names[MAX_RESOURCE].default_value
        )
        current_usage = alg_context.get_float_value_by_dynamic(CURRENT_USAGE)

        current_usage = max(0.0, min(1.0, current_usage))

        state = self._get_pid_state(group_name)

        if current_usage < min_resource:
            direction = 1
            error = min_resource - current_usage
        elif current_usage > max_resource:
            direction = -1
            error = current_usage - max_resource
        else:
            return previous

        pid_out = self._calculate_output(
            error,
            PidParams(kp, ki, kd),
            state,
            direction,
            ResourceRange(min_resource, max_resource),
        )

        error_rate = abs(error - state.get("lastError", 0))
        if error_rate > 0.2:
            pid_out *= 0.5
        # 计算使用增量调整(基于初始值调整,避免累计放大)
        adjustment = direction * pid_out * init_result

        if adjustment > 0:
            adjustment = max(adjustment, delta)
        elif adjustment < 0:
            adjustment = min(adjustment, -delta)

        new_threshold = previous + adjustment

        min_wal = alg_context.get_float_value_by_param(
            MIN_WAL, self.para_names[MIN_WAL].default_value
        )
        max_wal = alg_context.get_float_value_by_param(
            MAX_WAL, self.para_names[MAX_WAL].default_value
        )

        new_threshold = max(
            init_result * min_wal, min(init_result * max_wal, new_threshold)
        )

        return int(new_threshold)

    @staticmethod
    def _calculate_output(
        error: float,
        pid_params: PidParams,
        state: Dict[str, float],
        direction: int,
        resource_range: ResourceRange,
    ):
        # 计算PID输出方向
        if state.get("lastError") != 0 and state.get("lastDirection") != direction:
            state["integral"] = 0
            logger.debug("direction switching =%s, state=%s", direction, state)

        state["lastDirection"] = direction
        state["integral"] += error

        integral_limit = (resource_range.max_resource - resource_range.min_resource) * 10
        state["integral"] = max(-integral_limit, min(integral_limit, state["integral"]))

        p = pid_params.kp * error
        i = pid_params.ki * state["integral"]
        d = pid_params.kd * (error - state["lastError"])

        state["lastError"] = error
        return p + i + d

    def subscribe_clean(self, sender, **kwargs):
        self._pid_states.clear()
        logger.info("pid_states clear")

    def check(self, params: dict[str, Any]) -> bool:
        if not super().check(params):
            return False
        if not self.valid_min_max_param(MIN_RESOURCE, MAX_RESOURCE, params):
            return False
        if not self.valid_min_max_param(MIN_WAL, MAX_WAL, params):
            return False

        return True