using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using MiGu.Server.Configs;
using MiGu.Server.Ota;

namespace MiGu.Server.Controllers;

[ApiController]
[Authorize]
[Route("api/ota")]
public class OtaController : ControllerBase
{
    private readonly OtaStore _store;
    private readonly WatchDogClient _wd;
    private readonly OtaVehicleSource _vehicles;
    private readonly OtaJobRunner _jobs;
    private readonly OpsAuditStore _audits;
    private readonly OtaOptions _opt;

    public OtaController(
        OtaStore store,
        WatchDogClient wd,
        OtaVehicleSource vehicles,
        OtaJobRunner jobs,
        OpsAuditStore audits,
        IOptions<OtaOptions> opt)
    {
        _store = store;
        _wd = wd;
        _vehicles = vehicles;
        _jobs = jobs;
        _audits = audits;
        _opt = opt.Value;
    }

    private string UserName =>
        User.FindFirst("unique_name")?.Value
        ?? User.Identity?.Name
        ?? User.FindFirst(ClaimTypes.NameIdentifier)?.Value
        ?? "unknown";

    private string Scope => User.FindFirst("scope")?.Value ?? "";

    private bool CanWrite()
    {
        if (string.Equals(Scope, "Platform", StringComparison.OrdinalIgnoreCase)) return true;
        var ops = User.FindFirst("ops")?.Value ?? "";
        var set = ops.Split(' ', StringSplitOptions.RemoveEmptyEntries);
        return set.Contains("*") || set.Any(o => o.StartsWith("ops.ota", StringComparison.OrdinalIgnoreCase));
    }

    private bool DenyWrite(out ActionResult denied)
    {
        if (CanWrite()) { denied = null!; return false; }
        denied = StatusCode(StatusCodes.Status403Forbidden, new { message = "需要 Platform 或 ops.ota.* 权限" });
        return true;
    }

    private void Audit(string op, string target, string result, string? msg = null) =>
        _audits.Append(UserName, Scope, op, target, result, msg);

    [HttpGet("settings")]
    public ActionResult<OtaSettings> GetSettings() => _store.GetSettings();

    [HttpPut("settings")]
    public ActionResult<OtaSettings> PutSettings([FromBody] OtaSettings settings)
    {
        if (DenyWrite(out var denied)) return denied;
        var saved = _store.SaveSettings(settings);
        Audit("ops.ota.settings", "settings", "ok");
        return saved;
    }

    [HttpGet("target")]
    public ActionResult<object> GetTarget()
    {
        var t = _store.GetTarget();
        if (t == null) return Ok(new { target = (OtaTarget?)null });
        return Ok(new
        {
            target = t,
            summary = t.Components.ToDictionary(
                kv => kv.Key,
                kv => OtaHash.Short(kv.Value.Hash))
        });
    }

    [HttpGet("packages")]
    public ActionResult<List<OtaPackageInfo>> ListPackages() => _store.ListPackages();

    [HttpGet("packages/{id}")]
    public ActionResult<OtaPackageInfo> GetPackage(string id)
    {
        try { return _store.ScanPackage(id); }
        catch (DirectoryNotFoundException) { return NotFound(); }
        catch (ArgumentException ex) { return BadRequest(new { message = ex.Message }); }
    }

    [HttpPost("packages/{id}/activate")]
    public ActionResult<OtaTarget> Activate(string id, [FromQuery] string? name = null)
    {
        if (DenyWrite(out var denied)) return denied;
        try
        {
            var t = _store.ActivatePackage(id, name);
            Audit("ops.ota.activate", id, "ok");
            return t;
        }
        catch (Exception ex)
        {
            Audit("ops.ota.activate", id, "fail", ex.Message);
            return BadRequest(new { message = ex.Message });
        }
    }

    [HttpDelete("packages/{id}")]
    public IActionResult DeletePackage(string id)
    {
        if (DenyWrite(out var denied)) return denied;
        try
        {
            _store.DeletePackage(id);
            Audit("ops.ota.package.delete", id, "ok");
            return NoContent();
        }
        catch (Exception ex)
        {
            return BadRequest(new { message = ex.Message });
        }
    }

    [HttpPost("packages/pull")]
    public async Task<ActionResult<object>> Pull([FromBody] PullPackageRequest req, CancellationToken ct)
    {
        if (DenyWrite(out var denied)) return denied;
        var cars = await _vehicles.ListCarsAsync(ct);
        var car = cars.FirstOrDefault(c => string.Equals(c.Id, req.CarId, StringComparison.OrdinalIgnoreCase));
        if (car == null || string.IsNullOrEmpty(car.Ip))
            return BadRequest(new { message = "车辆不存在或无 IP" });

        var pkgId = _store.BeginPullPackage(car.Ip);
        try
        {
            var baseUrl = ResolvePublicBase();
            var receiveBase = $"{baseUrl.TrimEnd('/')}/api/ota/receive";
            var time = DateTime.Now.ToString("yyyyMMddHHmmss");
            await _wd.TriggerPullAsync(car.Ip, receiveBase, time, ct);
            // 等待文件落盘
            await Task.Delay(1500, ct);
            for (var i = 0; i < 40; i++)
            {
                var info = _store.ScanPackage(pkgId);
                if (info.Components.Count > 0) break;
                await Task.Delay(500, ct);
            }
            _store.ClearActivePull(car.Ip);
            var result = _store.ScanPackage(pkgId);
            if (result.Components.Count == 0)
            {
                Audit("ops.ota.package.pull", req.CarId, "empty", pkgId);
                return BadRequest(new
                {
                    message =
                        $"未收到任何组件文件（包 {pkgId} 为 0 B）。" +
                        $"WatchDog 会固定 POST 到 http://{{serverIP}}:{_opt.ReceivePort}/upload-mdcs/{{组件}}，" +
                        "请把该车 watch_dog.json 的 serverIP 设为本机局域网 IP，并确认本机已监听该端口；" +
                        "同时确认车上已配置 Medulla/Detour/Clumsy 路径。",
                    packageId = pkgId,
                    receivePort = _opt.ReceivePort
                });
            }
            Audit("ops.ota.package.pull", req.CarId, "ok", pkgId);
            return Ok(result);
        }
        catch (Exception ex)
        {
            _store.ClearActivePull(car.Ip);
            Audit("ops.ota.package.pull", req.CarId, "fail", ex.Message);
            return BadRequest(new { message = ex.Message, packageId = pkgId });
        }
    }

    [HttpPost("packages/upload")]
    [RequestSizeLimit(512_000_000)]
    public async Task<ActionResult<OtaPackageInfo>> Upload(IFormFile? file, CancellationToken ct)
    {
        if (DenyWrite(out var denied)) return denied;
        if (file == null || file.Length == 0)
            return BadRequest(new { message = "请上传文件（zip 或单文件）" });

        var pkgId = _store.BeginPullPackage("upload");
        var dir = _store.PackageDir(pkgId);
        try
        {
            var name = Path.GetFileName(file.FileName);
            if (string.IsNullOrWhiteSpace(name))
                return BadRequest(new { message = "文件名无效" });
            if (name.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
            {
                var zipPath = Path.Combine(dir, name);
                await using (var fs = System.IO.File.Create(zipPath))
                    await file.CopyToAsync(fs, ct);
                System.IO.Compression.ZipFile.ExtractToDirectory(zipPath, dir, true);
                System.IO.File.Delete(zipPath);
                NormalizeUploadLayout(dir);
            }
            else
            {
                // 单文件：按扩展名猜放到 M/D/C
                var dest = GuessDest(dir, name);
                Directory.CreateDirectory(Path.GetDirectoryName(dest)!);
                await using var fs = System.IO.File.Create(dest);
                await file.CopyToAsync(fs, ct);
            }
            _store.ClearActivePull("upload");
            var info = _store.ScanPackage(pkgId);
            Audit("ops.ota.package.upload", pkgId, "ok");
            return info;
        }
        catch (Exception ex)
        {
            _store.ClearActivePull("upload");
            return BadRequest(new { message = ex.Message });
        }
    }

    [HttpGet("vehicles")]
    public async Task<ActionResult<List<OtaVehicleRow>>> Vehicles([FromQuery] bool? latency, CancellationToken ct)
    {
        var settings = _store.GetSettings();
        var doLatency = latency ?? settings.LatencyEnabled;
        var target = _store.GetTarget();
        var cars = await _vehicles.ListCarsAsync(ct);

        await Parallel.ForEachAsync(cars, new ParallelOptions { MaxDegreeOfParallelism = 8, CancellationToken = ct }, async (car, token) =>
        {
            if (string.IsNullOrEmpty(car.Ip))
            {
                car.Reachable = false;
                return;
            }
            if (doLatency)
                car.RttMs = await _wd.MeasureRttMsAsync(car.Ip, token);

            var (ok, m, d, c, _) = await _wd.GetMdcInfoAsync(car.Ip, token);
            car.Reachable = ok;
            car.Medulla = m;
            car.Detour = d;
            car.Clumsy = c;
            if (target != null)
                car.Match = BuildMatch(target, m, d, c);
        });

        return cars;
    }

    [HttpGet("latency")]
    public async Task<ActionResult<object>> Latency(CancellationToken ct)
    {
        var settings = _store.GetSettings();
        if (!settings.LatencyEnabled)
            return Ok(new { enabled = false, items = Array.Empty<object>() });
        var cars = await _vehicles.ListCarsAsync(ct);
        var items = new List<object>();
        foreach (var car in cars)
        {
            if (string.IsNullOrEmpty(car.Ip)) continue;
            var rtt = await _wd.MeasureRttMsAsync(car.Ip, ct);
            items.Add(new { car.Id, car.Ip, rttMs = rtt, over = rtt == null || rtt > settings.RttThresholdMs });
        }
        return Ok(new { enabled = true, thresholdMs = settings.RttThresholdMs, items });
    }

    [HttpGet("jobs")]
    public ActionResult<List<OtaJob>> ListJobs([FromQuery] int take = 100) => _store.ListJobs(take);

    [HttpGet("jobs/{id}")]
    public ActionResult<OtaJob> GetJob(string id)
    {
        try
        {
            var j = _store.GetJob(id);
            return j == null ? NotFound() : j;
        }
        catch (ArgumentException ex)
        {
            return BadRequest(new { message = ex.Message });
        }
    }

    [HttpPost("jobs")]
    public ActionResult<OtaJob> CreateJob([FromBody] CreateSyncJobRequest req)
    {
        if (DenyWrite(out var denied)) return denied;
        try
        {
            var job = _jobs.EnqueueSync(req, UserName);
            Audit("ops.ota.job.create", job.Id, "ok", $"cars={req.CarIds.Count}");
            return job;
        }
        catch (Exception ex)
        {
            return BadRequest(new { message = ex.Message });
        }
    }

    [HttpPost("jobs/{id}/cancel")]
    public IActionResult Cancel(string id)
    {
        if (DenyWrite(out var denied)) return denied;
        var ok = _jobs.Cancel(id);
        Audit("ops.ota.job.cancel", id, ok ? "ok" : "noop");
        return ok ? Ok(new { ok = true }) : BadRequest(new { message = "无法取消" });
    }

    [HttpPost("jobs/{id}/retry")]
    public ActionResult<OtaJob> Retry(string id)
    {
        if (DenyWrite(out var denied)) return denied;
        try
        {
            var job = _jobs.RetryFailed(id, UserName);
            if (job == null) return NotFound();
            Audit("ops.ota.job.retry", id, "ok", job.Id);
            return job;
        }
        catch (Exception ex)
        {
            return BadRequest(new { message = ex.Message });
        }
    }

    [HttpGet("config/{carId}/{app}")]
    public async Task<ActionResult<object>> GetConfig(string carId, string app, CancellationToken ct)
    {
        var cars = await _vehicles.ListCarsAsync(ct);
        var car = cars.FirstOrDefault(c => string.Equals(c.Id, carId, StringComparison.OrdinalIgnoreCase));
        if (car?.Ip == null) return BadRequest(new { message = "车辆无 IP" });
        try
        {
            var json = await _wd.GetJsonAsync(car.Ip, app, ct);
            return Ok(new { carId, app, json });
        }
        catch (Exception ex)
        {
            return BadRequest(new { message = ex.Message });
        }
    }

    [HttpPost("config/push")]
    public ActionResult<OtaJob> ConfigPush([FromBody] CreateConfigPushRequest req)
    {
        if (DenyWrite(out var denied)) return denied;
        try
        {
            var job = _jobs.EnqueueConfigPush(req, UserName);
            Audit("ops.ota.config.push", job.Id, "ok", req.App);
            return job;
        }
        catch (Exception ex)
        {
            return BadRequest(new { message = ex.Message });
        }
    }

    [HttpPost("custom-file")]
    [RequestSizeLimit(512_000_000)]
    public async Task<ActionResult<OtaJob>> CustomFile(
        [FromForm] string carIds,
        [FromForm] string remotePath,
        [FromForm] string? restartOps,
        [FromForm] int? restartOp,
        [FromForm] List<IFormFile>? files,
        IFormFile? file,
        CancellationToken ct)
    {
        if (DenyWrite(out var denied)) return denied;
        var uploadFiles = new List<IFormFile>();
        if (files is { Count: > 0 }) uploadFiles.AddRange(files.Where(f => f.Length > 0));
        if (file is { Length: > 0 }) uploadFiles.Add(file);
        if (uploadFiles.Count == 0)
            return BadRequest(new { message = "请至少选择一个文件" });

        var ids = ParseCarIds(carIds);
        var ops = new List<int>();
        if (!string.IsNullOrWhiteSpace(restartOps))
        {
            try
            {
                ops = System.Text.Json.JsonSerializer.Deserialize<List<int>>(restartOps) ?? new();
            }
            catch { /* ignore */ }
        }
        if (ops.Count == 0 && restartOp.HasValue) ops.Add(restartOp.Value);
        if (ops.Count == 0) ops.Add(-1);

        var tmpDir = Path.Combine(_store.Root, "uploads", $"{DateTime.UtcNow:yyyyMMddHHmmssfff}-{Guid.NewGuid():N}");
        Directory.CreateDirectory(tmpDir);
        var items = new List<OtaCustomFileItem>();
        foreach (var f in uploadFiles)
        {
            var safe = Path.GetFileName(f.FileName);
            var local = Path.Combine(tmpDir, $"{items.Count}_{safe}");
            await using (var fs = System.IO.File.Create(local))
                await f.CopyToAsync(fs, ct);
            items.Add(new OtaCustomFileItem { LocalPath = local, FileName = safe });
        }

        try
        {
            var job = _jobs.EnqueueCustomFile(new CreateCustomFileJobRequest
            {
                CarIds = ids,
                RemotePath = remotePath,
                RestartOps = ops
            }, items, UserName);
            Audit("ops.ota.customFile", job.Id, "ok", string.Join(",", items.Select(i => i.FileName)));
            return job;
        }
        catch (Exception ex)
        {
            return BadRequest(new { message = ex.Message });
        }
    }

    private string ResolvePublicBase()
    {
        if (!string.IsNullOrWhiteSpace(_opt.PublicBaseUrl))
            return _opt.PublicBaseUrl.TrimEnd('/');
        return $"{Request.Scheme}://{Request.Host}";
    }

    private static List<string> ParseCarIds(string carIds)
    {
        var trimmed = carIds.Trim();
        if (trimmed.StartsWith("[", StringComparison.Ordinal))
        {
            try
            {
                var ids = System.Text.Json.JsonSerializer.Deserialize<List<string>>(trimmed);
                if (ids != null)
                    return ids.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).ToList();
            }
            catch
            {
                // Fall back to comma-separated form data below.
            }
        }

        return trimmed
            .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
            .Where(x => !string.IsNullOrWhiteSpace(x))
            .ToList();
    }

    private static Dictionary<string, string> BuildMatch(OtaTarget target, OtaAppVersions? m, OtaAppVersions? d, OtaAppVersions? c)
    {
        var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        void One(string key, string? vehicleHash)
        {
            if (!target.Components.TryGetValue(key, out var art))
            {
                map[key] = "missing-target";
                return;
            }
            if (string.IsNullOrEmpty(vehicleHash)) { map[key] = "unknown"; return; }
            map[key] = string.Equals(vehicleHash, art.Hash, StringComparison.Ordinal) ? "match" : "mismatch";
        }
        One("M.exe", m?.Exe?.Version);
        One("M.dll", m?.Dll?.Version);
        One("M.pdb", m?.Pdb?.Version);
        One("D.exe", d?.Exe?.Version);
        One("C.exe", c?.Exe?.Version);
        One("C.dll", c?.Dll?.Version);
        One("C.pdb", c?.Pdb?.Version);
        return map;
    }

    private static void NormalizeUploadLayout(string dir)
    {
        // 若 zip 根下直接是 M/D/C 或 Medulla.exe，尽量归位
        var medulla = Directory.GetFiles(dir, "Medulla.exe", SearchOption.AllDirectories).FirstOrDefault();
        if (medulla != null)
        {
            var dest = Path.Combine(dir, "M", "Medulla.exe");
            if (!string.Equals(medulla, dest, StringComparison.OrdinalIgnoreCase))
            {
                Directory.CreateDirectory(Path.GetDirectoryName(dest)!);
                System.IO.File.Copy(medulla, dest, true);
            }
        }
        var detour = Directory.GetFiles(dir, "Detour.exe", SearchOption.AllDirectories).FirstOrDefault();
        if (detour != null)
        {
            var dest = Path.Combine(dir, "D", "Detour.exe");
            if (!string.Equals(detour, dest, StringComparison.OrdinalIgnoreCase))
            {
                Directory.CreateDirectory(Path.GetDirectoryName(dest)!);
                System.IO.File.Copy(detour, dest, true);
            }
        }
    }

    private static string GuessDest(string dir, string fileName)
    {
        var n = fileName.ToLowerInvariant();
        if (n.Contains("medulla") && n.EndsWith(".exe")) return Path.Combine(dir, "M", "Medulla.exe");
        if (n.Contains("cartactivator") && n.EndsWith(".dll")) return Path.Combine(dir, "M", "plugins", "CartActivator.dll");
        if (n.Contains("detour")) return Path.Combine(dir, "D", "Detour.exe");
        if (n.Contains("clumsy") && n.EndsWith(".exe")) return Path.Combine(dir, "C", "ClumsyConsole.exe");
        return Path.Combine(dir, "M", Path.GetFileName(fileName));
    }
}
