using System.Security.Claims;
using System.Text.Json;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using MiGu.Server.Auth;
using MiGu.Server.Configs;
using MiGu.Server.Launcher;

namespace MiGu.Server.Controllers;

/// <summary>
/// 运维白名单网关。运营端（RCSMonitor）通过本控制器执行受控运维动作。
///
/// AR-4：[Authorize] 要求登录，再按 op 白名单 + JWT ops claim 双校验。
///
/// M4 修复（运维操作真实下发 + 审计落库）：
/// - 旧实现只把动作塞进静态内存队列、永远回 success=true，是「假操作」（前端显示
///   “暂停成功”但内核毫无反应，且重启审计全丢）；
/// - 现在：命中 <c>Ops:Dispatch</c> 映射的 op 会**真实转发**到 SimpleLite 反射 execute，
///   按内核返回如实记成功 / 失败；未映射的 op 不再假成功，明确回「未下发」；
/// - 审计统一经 <see cref="OpsAuditStore"/> 落盘（重启不丢）。
///
/// 关于映射：运营语义（暂停 / 恢复 / 回库 / 重置会话 / 手动充电）与 SimpleLite 内核反射
/// 方法（OnlineCar/OfflineCar/Repair/Blown/Reset… 见 Car.cs <c>[MethodMember]</c>）并非
/// 一一对应。为避免「猜错方法名 → 误操作车辆」，默认不预置车辆映射，由部署方在
/// appsettings.json <c>Ops:Dispatch</c> 显式配置 <c>"opCode": "kind:Method"</c> 后即真实下发。
/// </summary>
[ApiController]
[Authorize]
[Route("api/sl/ops")]
public class OpsController : ControllerBase
{
    public record ExecuteRequest(string OpCode, string TargetId, string? Reason, string? IdempotencyKey);
    public record ExecuteResponse(bool Ok, string AuditId, string? Message);

    private static readonly HashSet<string> Whitelist = new(StringComparer.Ordinal)
    {
        "ops.car.pause", "ops.car.resume", "ops.car.gohome", "ops.car.resetSession",
        "ops.car.manualCharge", "ops.task.pause", "ops.task.cancel", "ops.task.reassign",
        "ops.task.boostPriority", "monitor.note.write"
    };

    private readonly OpsAuditStore _audits;
    private readonly IHttpClientFactory _httpFactory;
    private readonly InternalTokenStore _internalToken;
    private readonly SimpleLiteOptions _sl;
    private readonly ILogger<OpsController> _log;
    private readonly IReadOnlyDictionary<string, (string Kind, string Method)> _dispatch;

    public OpsController(
        OpsAuditStore audits,
        IHttpClientFactory httpFactory,
        InternalTokenStore internalToken,
        IOptions<SimpleLiteOptions> sl,
        IConfiguration config,
        ILogger<OpsController> log)
    {
        _audits = audits;
        _httpFactory = httpFactory;
        _internalToken = internalToken;
        _sl = sl.Value;
        _log = log;
        _dispatch = LoadDispatch(config);
    }

    /// <summary>从 appsettings <c>Ops:Dispatch</c> 读取 opCode → "kind:Method" 映射（忽略空值与 _ 注释键）。</summary>
    private static IReadOnlyDictionary<string, (string, string)> LoadDispatch(IConfiguration config)
    {
        var map = new Dictionary<string, (string, string)>(StringComparer.Ordinal);
        foreach (var kv in config.GetSection("Ops:Dispatch").GetChildren())
        {
            var op = kv.Key;
            var spec = kv.Value;
            if (op.StartsWith('_') || string.IsNullOrWhiteSpace(spec)) continue;
            var parts = spec.Split(':', 2, StringSplitOptions.TrimEntries);
            if (parts.Length == 2 && parts[0].Length > 0 && parts[1].Length > 0)
                map[op] = (parts[0], parts[1]);
        }
        return map;
    }

    [HttpPost("execute")]
    public async Task<ActionResult<ExecuteResponse>> Execute([FromBody] ExecuteRequest req)
    {
        if (req is null || string.IsNullOrWhiteSpace(req.OpCode))
            return BadRequest(new { message = "opCode 不能为空" });
        if (!Whitelist.Contains(req.OpCode))
            return BadRequest(new { message = $"非白名单 op：{req.OpCode}" });

        // AR-4：JWT ops claim 二次校验 —— (op 在白名单) AND (op 在用户 ops claim)；admin 的 "*" 特判通过。
        var opsClaim = User.FindFirst("ops")?.Value ?? "";
        var userOps = opsClaim.Split(' ', StringSplitOptions.RemoveEmptyEntries);
        if (!userOps.Contains("*") && !userOps.Contains(req.OpCode))
            return StatusCode(403, new { message = $"当前账号无权执行 {req.OpCode}" });

        var user = User.Identity?.Name ?? User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "anonymous";
        var scope = User.FindFirst("scope")?.Value ?? "unknown";

        // 幂等：同一 IdempotencyKey 若已有成功审计，直接复用上次结果，避免前端重试 / 双击造成重复下发与重复审计。
        if (!string.IsNullOrWhiteSpace(req.IdempotencyKey))
        {
            var dup = _audits.FindSuccessByIdempotencyKey(req.IdempotencyKey);
            if (dup is not null)
                return Ok(new ExecuteResponse(true, dup.Id, dup.Message ?? "幂等命中：已执行过相同请求，未重复下发"));
        }

        // monitor.note.write：运营备注，非内核动作，仅审计。
        if (req.OpCode == "monitor.note.write")
            return Ok(Done(user, scope, req, "ok", req.Reason));

        // 未配置映射：不再「假成功」，如实告知未下发（在 appsettings Ops:Dispatch 绑定后即真实生效）。
        if (!_dispatch.TryGetValue(req.OpCode, out var map))
            return Ok(Done(user, scope, req, "unmapped",
                $"运维动作 {req.OpCode} 尚未绑定 SimpleLite 内核方法，已记录审计但未下发。" +
                $"请在 appsettings.json 的 Ops:Dispatch 配置 \"{req.OpCode}\": \"kind:Method\"。",
                ok: false));

        var numericId = ExtractNumericId(req.TargetId);
        if (numericId is null)
            return Ok(Done(user, scope, req, "failed", $"目标 ID『{req.TargetId}』无法解析为数字", ok: false));

        // M4：真实转发到 SimpleLite 反射 execute（与前端 reflectionApi.execute 同路径，本机直连 8222）。
        string result;
        string? message;
        try
        {
            var url = $"http://127.0.0.1:{_sl.ProjectionPort}/projection/reflection/execute/" +
                      $"{map.Kind}/{numericId}/{Uri.EscapeDataString(map.Method)}";
            using var client = _httpFactory.CreateClient();
            client.Timeout = TimeSpan.FromSeconds(8);
            using var msg = new HttpRequestMessage(HttpMethod.Post, url);
            // 即使 SimpleLite 默认放行 loopback，也带上 internal token，兼容其严格模式(AllowLoopback=false)。
            if (!string.IsNullOrEmpty(_internalToken.Token))
                msg.Headers.TryAddWithoutValidation("X-Platform-Internal-Token", _internalToken.Token);
            // 运维面板已对 needConfirm 动作做过二次确认；内核 RequiresPlatformConfirm 方法需此头。
            msg.Headers.TryAddWithoutValidation("X-Platform-Confirmed", "1");
            if (!string.IsNullOrWhiteSpace(user))
                msg.Headers.TryAddWithoutValidation("X-Platform-User", user);
            using var resp = await client.SendAsync(msg);
            var body = await resp.Content.ReadAsStringAsync();
            var success = resp.IsSuccessStatusCode && ParseSuccess(body);
            result = success ? "ok" : "failed";
            message = success ? null : $"SimpleLite 返回 {(int)resp.StatusCode}：{ExtractMessage(body)}";
        }
        catch (Exception ex)
        {
            result = "failed";
            message = $"下发 SimpleLite 失败：{ex.GetType().Name}: {ex.Message}";
            _log.LogWarning(ex, "ops execute 转发失败 op={Op} target={Target}", req.OpCode, req.TargetId);
        }

        return Ok(Done(user, scope, req, result, message, ok: result == "ok"));
    }

    [HttpGet("audits")]
    public IActionResult Audits200() => Ok(_audits.Recent());

    /// <summary>写一条审计并组装响应（成功时审计落 Reason，失败 / 未下发落具体 message）。</summary>
    private ExecuteResponse Done(string user, string scope, ExecuteRequest req, string result, string? message, bool ok = true)
    {
        var entry = _audits.Append(user, scope, req.OpCode, req.TargetId, result, message ?? req.Reason, req.IdempotencyKey);
        return new ExecuteResponse(ok, entry.Id, message);
    }

    /// <summary>
    /// 前端可能传 "C01" / "M03" / "5"，抽取「首段」连续数字作为内核对象 id。
    /// 取首段而非拼接所有数字，避免 "AGV-12-3" 被误合并成 123。
    /// </summary>
    private static int? ExtractNumericId(string? raw)
    {
        if (string.IsNullOrWhiteSpace(raw)) return null;
        var m = System.Text.RegularExpressions.Regex.Match(raw, @"\d+");
        return m.Success && int.TryParse(m.Value, out var n) ? n : null;
    }

    private static bool ParseSuccess(string body)
    {
        try
        {
            using var doc = JsonDocument.Parse(body);
            return doc.RootElement.TryGetProperty("success", out var s) && s.ValueKind == JsonValueKind.True;
        }
        catch { return false; }
    }

    private static string ExtractMessage(string body)
    {
        try
        {
            using var doc = JsonDocument.Parse(body);
            if (doc.RootElement.TryGetProperty("message", out var m) && m.ValueKind == JsonValueKind.String)
                return m.GetString() ?? "";
        }
        catch { /* ignore，下面回退裁剪原文 */ }
        return body.Length <= 200 ? body : body[..200] + "…";
    }
}
