using System.Text.Json;
using Admin.NET.Plugin.AiDOP.Entity.S8;
using Admin.NET.Plugin.AiDOP.Infrastructure.S8;
using Admin.NET.Plugin.AiDOP.Service.S8.Rules.Definitions;
namespace Admin.NET.Plugin.AiDOP.Service.S8.Rules;
///
/// S8-RULE-GOVERNANCE-BATCH1:一条规则在某租户下的**运行参数**(B 类)。
///
/// 与 Definition(A 类)的分界线:改这里的值,规则「判定什么」不变,只是
/// 「多久跑一次、抖动几次算数、宽限多久、报多严重、兜底派给谁」变了。
/// 改 Definition 才会改变业务含义 —— 而 Definition 在代码里,没有 API 能改。
///
/// params_json 的新职责:只承载 /
/// / 三项。
/// 其余参数是实体列(poll / trigger / recover / severity),不进 JSON。
///
/// 历史 JSON 兼容(本批不做 migration):库里现存的 params_json 可能是
/// 旧的 A+B 全量 JSON、可能只有 B、也可能是 NULL。三种都必须能起来:
///
/// - 旧 A 字段(dueAtField / statusField / completedStates / objectIdField /
/// objectCodeField / exceptionTypeCode)→ 读到也忽略,绝不作为语义来源;
/// - B 字段存在 → 正常解析;
/// - params_json 为 NULL / 空白 / 非法 JSON → 全部回落 Definition 声明的默认值,
/// 不再抛 rule_not_configured。
///
/// 最后一条是 G1 的直接修复点:本地 Rule 01 的 params_json 已被
/// PUT /params {"enabled":false} 抹成 NULL,在旧代码里这条规则已经永久跑不起来。
///
public sealed class S8RuleRuntimeParameters
{
/// 轮询间隔(秒)。真源是实体列 poll_interval_seconds。
public int PollIntervalSeconds { get; init; }
/// 连续命中 N 次才建单。真源是实体列 trigger_count_required。
public int TriggerCountRequired { get; init; }
/// 连续未命中 N 次才标 recovered。真源是实体列 recover_count_required。
public int RecoverCountRequired { get; init; }
/// 严重度。真源是实体列 severity;非法值回落 Definition 默认,不静默降级为 FOLLOW。
public string Severity { get; init; } = S8SeverityCode.Follow;
/// 宽限分钟。真源是 params_json。
public int GraceMinutes { get; init; }
/// 发生部门兜底。真源是 params_json;未配置为 null。
public long? DefaultOccurrenceDeptId { get; init; }
/// 责任部门兜底。真源是 params_json;未配置为 null。
public long? DefaultResponsibleDeptId { get; init; }
// ─────────────────────────────────────────────────────────────
// params_json 的 B 类键名。写入与读取共用同一批常量,杜绝两处拼写漂移。
// ─────────────────────────────────────────────────────────────
internal const string KeyGraceMinutes = "graceMinutes";
internal const string KeyDefaultOccurrenceDeptId = "defaultOccurrenceDeptId";
internal const string KeyDefaultResponsibleDeptId = "defaultResponsibleDeptId";
///
/// 从实体行 + Definition 解析出生效运行参数。
/// 本方法不抛异常:任何解析失败都回落默认值。运行参数解析失败不应该让一条
/// 业务语义完全正确的规则停摆 —— 那正是 G1 造成的后果。
///
public static S8RuleRuntimeParameters Resolve(AdoS8WatchRule rule, S8RuleDefinition definition)
{
var policy = definition.Parameters ?? new S8RuleParameterPolicy();
var graceMinutes = policy.GraceMinutesDefault;
long? occurrenceDeptId = null;
long? responsibleDeptId = null;
if (!string.IsNullOrWhiteSpace(rule.ParamsJson))
{
try
{
using var doc = JsonDocument.Parse(rule.ParamsJson!);
var root = doc.RootElement;
if (root.ValueKind == JsonValueKind.Object)
{
// 只读 B 类三键。旧 JSON 里的 A 类字段就在同一个对象里,
// 这里**不读**它们 —— 忽略是刻意的,不是遗漏。
if (root.TryGetProperty(KeyGraceMinutes, out var g)
&& g.ValueKind == JsonValueKind.Number
&& g.TryGetInt32(out var gv))
graceMinutes = gv;
occurrenceDeptId = ReadNullableLong(root, KeyDefaultOccurrenceDeptId);
responsibleDeptId = ReadNullableLong(root, KeyDefaultResponsibleDeptId);
}
}
catch (JsonException)
{
// 非法 JSON:回落默认值。历史脏数据不该把规则打死。
}
}
if (!policy.AllowsDepartmentDefaults)
{
occurrenceDeptId = null;
responsibleDeptId = null;
}
return new S8RuleRuntimeParameters
{
PollIntervalSeconds = Clamp(rule.PollIntervalSeconds, policy.PollIntervalSecondsMin,
policy.PollIntervalSecondsMax, policy.PollIntervalSecondsDefault),
TriggerCountRequired = Clamp(rule.TriggerCountRequired, policy.TriggerCountRequiredMin,
policy.TriggerCountRequiredMax, policy.TriggerCountRequiredDefault),
RecoverCountRequired = Clamp(rule.RecoverCountRequired, policy.RecoverCountRequiredMin,
policy.RecoverCountRequiredMax, policy.RecoverCountRequiredDefault),
Severity = ResolveSeverity(rule.Severity, policy),
GraceMinutes = Clamp(graceMinutes, policy.GraceMinutesMin, policy.GraceMinutesMax,
policy.GraceMinutesDefault),
DefaultOccurrenceDeptId = occurrenceDeptId,
DefaultResponsibleDeptId = responsibleDeptId
};
}
///
/// 把 B 类参数序列化回 params_json。
/// 只写这三个键:任何旧 A 类字段在这次写入后自然消失,无需 migration 也不会被再次读到。
///
public string ToParamsJson()
{
var payload = new Dictionary(StringComparer.Ordinal)
{
[KeyGraceMinutes] = GraceMinutes
};
if (DefaultOccurrenceDeptId.HasValue) payload[KeyDefaultOccurrenceDeptId] = DefaultOccurrenceDeptId.Value;
if (DefaultResponsibleDeptId.HasValue) payload[KeyDefaultResponsibleDeptId] = DefaultResponsibleDeptId.Value;
return JsonSerializer.Serialize(payload);
}
private static long? ReadNullableLong(JsonElement root, string key)
{
if (!root.TryGetProperty(key, out var el)) return null;
if (el.ValueKind == JsonValueKind.Number && el.TryGetInt64(out var v)) return v > 0 ? v : null;
// 历史数据里存在字符串化的 id,容忍解析但不容忍非法值。
if (el.ValueKind == JsonValueKind.String && long.TryParse(el.GetString(), out var sv)) return sv > 0 ? sv : null;
return null;
}
private static int Clamp(int value, int min, int max, int fallback)
{
if (value < min || value > max) return fallback;
return value;
}
///
/// 严重度解析。**刻意不复用 S8SeverityCode.Normalize**:它的兜底分支是
/// _ => Follow,会把 legacy 的 HIGH / 拼写错误静默降级成 FOLLOW;
/// 那正是库里 severity='HIGH' 的行当年混进来的方式。这里回落 Definition 声明的默认值。
///
private static string ResolveSeverity(string? severity, S8RuleParameterPolicy policy)
{
if (!string.IsNullOrWhiteSpace(severity)
&& policy.AllowedSeverities.Contains(severity!.Trim(), StringComparer.Ordinal))
return severity!.Trim();
return policy.SeverityDefault;
}
}