using Microsoft.AspNetCore.Mvc;
namespace Admin.NET.Plugin.AiDOP.Infrastructure;
///
/// S0 组织归属(company_ref_id / factory_ref_id)的服务端唯一裁决者。
///
/// 背景(S0-ORG-REFERENCE-MODEL-FIX-B1):
/// 2026-06-14 commit a18c8a6c5 在「单部署」假设下给 S0 仓储页写死了
/// DEFAULT_COMPANY_REF_ID=1329900200001 / DEFAULT_FACTORY_REF_ID=1329900200002;
/// 其后 1.0.317.sql 把这两个 SysOrg 改挂到 Demo 租户 797403760988230,
/// 前端常量却从未同步 → 任何租户新增都写入别租户的 SysOrg.Id,形成跨租户组织引用。
///
/// 铁律:
/// - 组织归属**只由服务端**在当前租户内解析,一律忽略 payload 的组织值;
/// 前端 payload 不可信(老前端 / 手工 API / 其它客户端都可能带错值)。
/// - tenantId 必须来自 (JWT claim),
/// 绝不接受前端传入的 tenantId。
/// - **绝不跨租户查找**:候选集一律先按 TenantId 收窄。
/// - **绝不新增组织**:解析不出就返回 (0,0),由调用方转 400 业务错误,不自动建公司/工厂。
///
/// 本实现自 AdoS0ProcessFlowCardsController.ResolveDefaultOrgAsync 原样抽取
/// (该实现已实测产出 8/8 SAME-TENANT),语义保持不变,避免多份重复 resolver 漂移。
///
public static class AdoS0OrgScope
{
/// 租户内优先匹配的公司编码(仅作优先条件,取不到即按机构类型/根节点在本租户内兜底)。
private const string PreferredCompanyCode = "S0-C001";
/// 租户内优先匹配的工厂编码(同上,绝不跨租户查找)。
private const string PreferredFactoryCode = "S0-F001";
/// 机构类型-公司(SysOrg.Type 数据字典值)。
private const string CompanyOrgType = "201";
///
/// 机构类型-工厂。
/// ⚠️ S0 legacy compatibility: Type=501 used as factory.
/// 平台数据字典 org_type 中 501 的官方语义是「组」(101=集团 / 201=公司 / 301=部门 / 501=组),
/// S0 自 S0OrgSeedData 起单方面把 501 复用为「工厂」。当前全库 Type=501 的组织均为 AiDOP
/// 自建工厂节点,无真实「组」,故保持兼容不重构;后续若出现真实「组」需另开批次消歧。
///
private const string FactoryOrgType = "501";
///
/// 在**当前租户内**解析 S0 默认公司/工厂组织。
///
/// 优先级——公司:
/// C1 本租户 Code=S0-C001 → C3 本租户根节点(Pid=0)且 Type=201
/// → C2 本租户任意 Type=201 → C4 本租户任意根节点(Pid=0)
/// 优先级——工厂:
/// F1 本租户 Code=S0-F001 且父级为已解析公司 → F2 本租户 Type=501 且父级为已解析公司
/// → F3 本租户任意 Type=501 → F4 回落到公司自身(兼容未建工厂节点的租户,不新增组织)。
///
/// 确定性:一次取本租户机构并按 (OrderNo, Id) 稳定排序,各级用 FirstOrDefault 在有序集合上取首个,
/// 避免多候选时结果随执行计划漂移。
///
/// 无解:返回 (0, 0),由调用方转 400 业务错误,**不抛异常**——
/// AdoS0ExceptionFilter 会把 /api/s0/* 上的未处理异常统一映射为 500 S01999,
/// 抛异常会把「租户未配置机构」这类业务态误报成系统故障。
///
/// SqlSugar 上下文(调用方一般传 _rep.Context,无需额外注入 SysOrg 仓储)。
/// AdoS0TenantScope 解析出的可信租户(JWT claim),不接受 payload 传入。
public static async Task<(long CompanyId, long FactoryId)> ResolveAsync(ISqlSugarClient db, long tenantId)
{
if (tenantId <= 0) return (0, 0);
// 单次取数 + 内存分级,避免逐级往返;排序在 SQL 侧一次确定,后续各级取首个即稳定。
var orgs = await db.Queryable()
.Where(x => x.TenantId == tenantId)
.OrderBy(x => new { x.OrderNo, x.Id })
.Select(x => new { x.Id, x.Code, x.Type, x.Pid })
.ToListAsync();
if (orgs.Count == 0) return (0, 0);
var company =
orgs.FirstOrDefault(o => o.Code == PreferredCompanyCode) // C1
?? orgs.FirstOrDefault(o => o.Pid == 0 && o.Type == CompanyOrgType) // C3
?? orgs.FirstOrDefault(o => o.Type == CompanyOrgType) // C2
?? orgs.FirstOrDefault(o => o.Pid == 0); // C4
if (company == null) return (0, 0);
var factory =
orgs.FirstOrDefault(o => o.Code == PreferredFactoryCode && o.Pid == company.Id) // F1
?? orgs.FirstOrDefault(o => o.Type == FactoryOrgType && o.Pid == company.Id) // F2
?? orgs.FirstOrDefault(o => o.Type == FactoryOrgType); // F3
return (company.Id, factory?.Id ?? company.Id); // F4
}
///
/// 解析失败时的统一业务错误(各 Controller 复用同一文案,便于前端与运维识别)。
///
public static IActionResult NotConfiguredError(string entityLabel)
=> AdoS0ApiErrors.InvalidRequest($"当前租户未配置机构,无法确定{entityLabel}的公司/工厂归属,请先在机构管理中维护");
}