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 会**真实转发**到 Simple3 反射 execute，
///   按内核返回如实记成功 / 失败；未映射的 op 不再假成功，明确回「未下发」；
/// - 审计统一经 <see cref="OpsAuditStore"/> 落盘（重启不丢）。
///
/// 关于映射：
/// - 地图监控选中车辆：<c>ops.car.execute</c> 按管理端「运营维护」<c>carActionByType</c>
///   勾选的 Simple3 方法下发（与管理端同一份 monitor-config）。
/// - 运维操作页的 pause/resume/gohome 等仍走 <c>Ops:Dispatch</c> 显式映射。
/// </summary>
[ApiController]
[Authorize]
[Route("api/sl/ops")]
public class OpsController : ControllerBase
{
    public const string CarExecuteOp = "ops.car.execute";

    public record ExecuteRequest(
        string OpCode,
        string TargetId,
        string? Reason,
        string? IdempotencyKey,
        string? Method,
        Dictionary<string, string>? Params,
        int? SiteId);
    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", CarExecuteOp,
        "ops.task.pause", "ops.task.cancel", "ops.task.reassign",
        "ops.task.boostPriority", "monitor.note.write"
    };

    private static readonly HashSet<string> GotoMethods = new(StringComparer.OrdinalIgnoreCase)
    {
        "Goto", "GotoSite", "WebGotoSite"
    };

    /// <summary>
    /// 地图监控车辆列表快捷动作（上线/下线/结束任务等），与 carActionByType 勾选并集。
    /// </summary>
    private static readonly HashSet<string> MapMonitorQuickMethods = new(StringComparer.Ordinal)
    {
        "OnlineCar", "OfflineCar", "DisableCar", "EnableCar",
        "ForceStopUI", "ForceStop", "UIIntercept"
    };

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

    public OpsController(
        OpsAuditStore audits,
        IHttpClientFactory httpFactory,
        InternalTokenStore internalToken,
        IOptions<Simple3Options> 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 (!CanRunOp(userOps, req.OpCode, User.FindFirst("scope")?.Value))
            return StatusCode(403, new { message = $"当前账号无权执行 {req.OpCode}" });

        var user = CurrentUsername();
        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));

        if (req.OpCode == CarExecuteOp)
            return Ok(await ExecuteConfiguredCarMethodAsync(user, scope, req));

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

        var forwarded = await ForwardExecuteAsync(map.Kind, numericId.Value, map.Method, null, user);
        return Ok(Done(user, scope, req, forwarded.Result, forwarded.Message, ok: forwarded.Ok));
    }

    /// <summary>
    /// 地图监控：按管理端 monitor-config.carActionByType 勾选的方法下发。
    /// 账号需具备任意 <c>ops.car.*</c>（或 *）；方法名必须落在该车型已配置白名单内。
    /// </summary>
    private async Task<ExecuteResponse> ExecuteConfiguredCarMethodAsync(string user, string scope, ExecuteRequest req)
    {
        var method = (req.Method ?? "").Trim();
        if (!IsSafeMethodName(method))
            return Done(user, scope, req, "failed", "方法名无效", ok: false);

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

        var allowed = await LoadAllowedCarMethodsAsync(numericId.Value);
        if (allowed is null)
            return Done(user, scope, req, "failed", "无法读取管理端车辆动作配置（monitor-config）", ok: false);
        allowed.UnionWith(MapMonitorQuickMethods);
        if (allowed.Count == 0)
            return Done(user, scope, req, "unmapped", "当前车型未配置动作。请在管理端「配置中心 → 运营维护」按车型勾选。", ok: false);
        if (!allowed.Contains(method))
            return Done(user, scope, req, "failed", $"方法 {method} 不在该车型已配置的动作列表中", ok: false);

        var siteId = req.SiteId;
        if (siteId is null && req.Params is not null && req.Params.TryGetValue("siteId", out var rawSite)
            && int.TryParse(rawSite, out var parsedSite))
            siteId = parsedSite;

        if (siteId is int sid && GotoMethods.Contains(method))
        {
            var gotoResult = await ForwardGotoSiteAsync(numericId.Value, sid, user);
            if (gotoResult.Ok || !string.Equals(method, "WebGotoSite", StringComparison.OrdinalIgnoreCase))
                return Done(user, scope, req, gotoResult.Result,
                    CarAuditMessage(method, gotoResult.Ok ? $"前往站点 {sid}" : gotoResult.Message, gotoResult.Ok),
                    ok: gotoResult.Ok);
        }

        var forwarded = await ForwardExecuteAsync("car", numericId.Value, method, req.Params, user);
        return Done(user, scope, req, forwarded.Result,
            CarAuditMessage(method, forwarded.Message, forwarded.Ok),
            ok: forwarded.Ok);
    }

    private static string CarAuditMessage(string method, string? detail, bool ok)
    {
        if (!ok) return string.IsNullOrWhiteSpace(detail) ? $"执行 {method} 失败" : detail;
        return string.IsNullOrWhiteSpace(detail) ? $"执行 {method}" : $"{method}：{detail}";
    }

    /// <summary>只返回当前登录人的操作记录，不提供全员查询参数。</summary>
    [HttpGet("audits")]
    public IActionResult Audits200() => Ok(_audits.Recent(CurrentUsername()));

    /// <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>与 YARP 注入 X-Platform-User 同一套取值：登录名，不是用户 id。</summary>
    private string CurrentUsername() =>
        User.FindFirst("unique_name")?.Value
        ?? User.Identity?.Name
        ?? User.FindFirstValue(ClaimTypes.Name)
        ?? User.FindFirstValue(ClaimTypes.NameIdentifier)
        ?? "anonymous";

    /// <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] + "…";
    }

    private static bool CanRunOp(string[] userOps, string opCode, string? scope)
    {
        if (userOps.Contains("*") || userOps.Contains(opCode)) return true;
        if (opCode == CarExecuteOp && userOps.Any(o => o.StartsWith("ops.car.", StringComparison.Ordinal)))
            return true;
        // 管理面地图监控原先可直调反射；走网关只为记审计，不额外收权。
        return opCode == CarExecuteOp
            && string.Equals(scope, PageCatalog.ScopePlatform, StringComparison.OrdinalIgnoreCase);
    }

    private static bool IsSafeMethodName(string method) =>
        method.Length is > 0 and <= 64 && method.All(c => char.IsAsciiLetterOrDigit(c) || c == '_');

    private readonly record struct ForwardOutcome(bool Ok, string Result, string? Message);

    private async Task<HashSet<string>?> LoadAllowedCarMethodsAsync(int carId)
    {
        try
        {
            var bundle = await GetLiteJsonAsync($"/projection/reflection/bundle/car/{carId}");
            var config = await GetLiteJsonAsync("/projection/reflection/monitor-config");
            if (bundle is null || config is null) return null;

            var typeName = ReadString(bundle, "typeName") ?? "";
            var fullTypeName = ReadString(bundle, "fullTypeName") ?? typeName;
            var map = ReadCarActionByType(config.Value);
            return LookupCarMethods(map, fullTypeName, typeName);
        }
        catch (Exception ex)
        {
            _log.LogWarning(ex, "读取 monitor-config 失败 car={CarId}", carId);
            return null;
        }
    }

    private static Dictionary<string, string[]> ReadCarActionByType(JsonElement envelope)
    {
        var map = new Dictionary<string, string[]>(StringComparer.Ordinal);
        if (!TryData(envelope, out var data)) return map;
        if (!data.TryGetProperty("config", out var cfg) || cfg.ValueKind != JsonValueKind.Object)
            return map;
        if (!cfg.TryGetProperty("carActionByType", out var cat) || cat.ValueKind != JsonValueKind.Object)
            return map;
        foreach (var prop in cat.EnumerateObject())
        {
            if (prop.Value.ValueKind != JsonValueKind.Array) continue;
            map[prop.Name] = prop.Value.EnumerateArray()
                .Where(x => x.ValueKind == JsonValueKind.String)
                .Select(x => x.GetString() ?? "")
                .Where(x => x.Length > 0)
                .ToArray();
        }
        return map;
    }

    private static HashSet<string> LookupCarMethods(Dictionary<string, string[]> map, string fullType, string shortType)
    {
        var shortName = shortType;
        var dot = shortType.LastIndexOf('.');
        if (dot >= 0 && dot < shortType.Length - 1) shortName = shortType[(dot + 1)..];

        if (map.TryGetValue(fullType, out var exact) && exact.Length > 0)
            return new HashSet<string>(exact, StringComparer.Ordinal);
        if (map.TryGetValue(shortType, out var byType) && byType.Length > 0)
            return new HashSet<string>(byType, StringComparer.Ordinal);
        if (map.TryGetValue(shortName, out var byShort) && byShort.Length > 0)
            return new HashSet<string>(byShort, StringComparer.Ordinal);

        foreach (var kv in map)
        {
            if (kv.Value.Length == 0) continue;
            var k = kv.Key;
            if (k == fullType || k == shortType || k == shortName) return new HashSet<string>(kv.Value, StringComparer.Ordinal);
            if (!string.IsNullOrEmpty(fullType) && (fullType.EndsWith('.' + k, StringComparison.Ordinal) || k.EndsWith('.' + shortName, StringComparison.Ordinal)))
                return new HashSet<string>(kv.Value, StringComparer.Ordinal);
            if (!string.IsNullOrEmpty(shortName) && k.EndsWith('.' + shortName, StringComparison.Ordinal))
                return new HashSet<string>(kv.Value, StringComparer.Ordinal);
        }
        return new HashSet<string>(StringComparer.Ordinal);
    }

    private static bool TryData(JsonElement envelope, out JsonElement data)
    {
        if (envelope.TryGetProperty("data", out data) && data.ValueKind == JsonValueKind.Object)
            return true;
        data = envelope;
        return envelope.ValueKind == JsonValueKind.Object;
    }

    private static string? ReadString(JsonElement? envelope, string name)
    {
        if (envelope is null) return null;
        var root = envelope.Value;
        if (TryData(root, out var data) && data.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String)
            return v.GetString();
        if (root.TryGetProperty(name, out var direct) && direct.ValueKind == JsonValueKind.String)
            return direct.GetString();
        return null;
    }

    private async Task<JsonElement?> GetLiteJsonAsync(string path)
    {
        var call = await CallLiteAsync(HttpMethod.Get, path, actor: null);
        if (!call.Ok) return null;
        using var doc = JsonDocument.Parse(call.Body);
        return doc.RootElement.Clone();
    }

    private async Task<ForwardOutcome> ForwardGotoSiteAsync(int carId, int siteId, string actor)
    {
        var path = $"/projection/reflection/car/{carId}/goto-site?siteId={siteId}";
        return await SendLiteExecuteAsync(path, actor);
    }

    private async Task<ForwardOutcome> ForwardExecuteAsync(
        string kind, int id, string method, Dictionary<string, string>? query, string actor)
    {
        var path = $"/projection/reflection/execute/{kind}/{id}/{Uri.EscapeDataString(method)}";
        if (query is { Count: > 0 })
        {
            var qs = string.Join("&", query
                .Where(kv => !string.IsNullOrEmpty(kv.Key))
                .Select(kv => $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value ?? "")}"));
            if (qs.Length > 0) path += "?" + qs;
        }
        return await SendLiteExecuteAsync(path, actor);
    }

    private async Task<ForwardOutcome> SendLiteExecuteAsync(string path, string actor)
    {
        try
        {
            var call = await CallLiteAsync(HttpMethod.Post, path, actor);
            if (TryReadConfirmTicket(call.Body, out var token))
            {
                call = await CallLiteAsync(HttpMethod.Post, path, actor, new Dictionary<string, string>
                {
                    [ConfirmTokenHeader] = token
                });
            }
            var success = call.Ok && ParseSuccess(call.Body);
            return success
                ? new ForwardOutcome(true, "ok", null)
                : new ForwardOutcome(false, "failed", $"Simple3 返回 {call.Status}：{ExtractMessage(call.Body)}");
        }
        catch (Exception ex)
        {
            _log.LogWarning(ex, "ops 转发 Simple3 失败 path={Path}", path);
            return new ForwardOutcome(false, "failed", $"下发 Simple3 失败：{ex.GetType().Name}: {ex.Message}");
        }
    }

    private const string ConfirmTokenHeader = "X-Platform-Confirm-Token";

    private static bool TryReadConfirmTicket(string body, out string token)
    {
        token = "";
        try
        {
            using var doc = JsonDocument.Parse(body);
            var root = doc.RootElement;
            var code = root.TryGetProperty("code", out var c) && c.TryGetInt32(out var n) ? n : 0;
            if (code != 428) return false;
            if (!root.TryGetProperty("data", out var data) || data.ValueKind != JsonValueKind.Object)
                return false;
            token = data.TryGetProperty("confirmToken", out var t) && t.ValueKind == JsonValueKind.String
                ? t.GetString() ?? ""
                : "";
            return !string.IsNullOrWhiteSpace(token);
        }
        catch
        {
            return false;
        }
    }

    private async Task<(bool Ok, int Status, string Body)> CallLiteAsync(
        HttpMethod method, string path, string? actor, IReadOnlyDictionary<string, string>? extraHeaders = null)
    {
        var url = $"http://127.0.0.1:{_sl.ProjectionPort}{path}";
        using var client = _httpFactory.CreateClient();
        client.Timeout = TimeSpan.FromSeconds(8);
        using var msg = new HttpRequestMessage(method, url);
        if (!string.IsNullOrEmpty(_internalToken.Token))
            msg.Headers.TryAddWithoutValidation("X-Platform-Internal-Token", _internalToken.Token);
        if (method == HttpMethod.Post)
            msg.Headers.TryAddWithoutValidation("X-Platform-Confirmed", "1");
        if (!string.IsNullOrWhiteSpace(actor))
            msg.Headers.TryAddWithoutValidation("X-Platform-User", actor);
        if (extraHeaders != null)
        {
            foreach (var kv in extraHeaders)
            {
                if (string.IsNullOrWhiteSpace(kv.Key) || kv.Value is null) continue;
                msg.Headers.TryAddWithoutValidation(kv.Key, kv.Value);
            }
        }
        using var resp = await client.SendAsync(msg);
        var body = await resp.Content.ReadAsStringAsync();
        return (resp.IsSuccessStatusCode, (int)resp.StatusCode, body);
    }
}
