using System.Reflection;
using Eleon.Modding;
using EmpyrionBridge;

var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
builder.Logging.AddSimpleConsole(o =>
{
    o.SingleLine = true;
    o.TimestampFormat = "HH:mm:ss ";
});
builder.Services.AddSingleton<BridgePool>();
builder.WebHost.UseUrls(Environment.GetEnvironmentVariable("BRIDGE_URLS") ?? "http://0.0.0.0:8090");

// Eleon's protobuf-net contracts (PlayerInfo, FactionInfo, etc.) expose
// their data as PUBLIC FIELDS, not properties. System.Text.Json ignores
// public fields by default, which made `Results.Json(playerInfo)` emit
// `{}` even though the deserialized object had real data inside. Turning
// IncludeFields on (globally for every endpoint that doesn't project
// explicitly) fixes the player-list, player-info, dedi-stats, and any
// future endpoint that returns a raw mod-API DTO.
//
// AllowNamedFloatingPointLiterals lets us serialize `NaN` / `Infinity`
// as the JSON string forms. Empyrion's mod API hands those back for
// uninitialized float fields (heading, posY, etc.) and STJ will throw
// at serialization time without this — breaking the entire response.
builder.Services.ConfigureHttpJsonOptions(o =>
{
    o.SerializerOptions.IncludeFields = true;
    o.SerializerOptions.NumberHandling =
        System.Text.Json.Serialization.JsonNumberHandling.AllowNamedFloatingPointLiterals;
});

var app = builder.Build();
var pool = app.Services.GetRequiredService<BridgePool>();
var requestTimeout = TimeSpan.FromSeconds(double.TryParse(Environment.GetEnvironmentVariable("BRIDGE_REQUEST_TIMEOUT_S"), out var t) ? t : 8);
var apiToken = (Environment.GetEnvironmentVariable("BRIDGE_API_TOKEN") ?? "").Trim();
if (string.IsNullOrEmpty(apiToken))
{
    app.Logger.LogWarning("BRIDGE_API_TOKEN is not set — auth is OFF. Anyone reaching the bridge can drive it. Suitable for loopback-only deployments.");
}

// Bearer-token middleware. Loopback (127.0.0.1, ::1) and unix sockets are
// always allowed without a token, matching the panel's existing pattern.
// Health endpoint always allowed.
app.Use(async (ctx, next) =>
{
    if (string.IsNullOrEmpty(apiToken)) { await next(); return; }
    if (ctx.Request.Path.StartsWithSegments("/healthz")) { await next(); return; }
    var ip = ctx.Connection.RemoteIpAddress;
    if (ip is not null && (System.Net.IPAddress.IsLoopback(ip))) { await next(); return; }
    var auth = ctx.Request.Headers["Authorization"].FirstOrDefault() ?? "";
    if (auth == $"Bearer {apiToken}") { await next(); return; }
    ctx.Response.StatusCode = 401;
    await ctx.Response.WriteAsJsonAsync(new { error = "unauthorized" });
});

string? Q(HttpRequest r, string k) => r.Query[k].FirstOrDefault();

(string host, int port)? Endpoint(HttpRequest r)
{
    var h = Q(r, "host");
    var p = Q(r, "port");
    if (string.IsNullOrEmpty(h) || !int.TryParse(p, out var pi)) return null;
    return (h, pi);
}

app.MapGet("/healthz", () => Results.Json(new
{
    ok = true,
    version = typeof(Program).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion,
    pid = Environment.ProcessId,
    authMode = string.IsNullOrEmpty(apiToken) ? "none" : "bearer-token",
}));

app.MapGet("/api/v1/factions", async (HttpRequest r) =>
{
    var ep = Endpoint(r);
    if (ep is null) return Results.BadRequest(new { error = "missing host or port" });
    var conn = await pool.GetAsync(ep.Value.host, ep.Value.port);
    var resp = await conn.RequestAsync<FactionInfoList>(CmdId.Request_Get_Factions, CmdId.Event_Get_Factions, new Id(1), requestTimeout);
    return Results.Json(new
    {
        host = ep.Value.host,
        port = ep.Value.port,
        factions = resp?.factions?.Select(f => new
        {
            origin = f.origin,
            factionId = f.factionId,
            name = f.name,
            abbrev = f.abbrev,
        }) ?? Enumerable.Empty<object>(),
    });
});

app.MapGet("/api/v1/players", async (HttpRequest r) =>
{
    var ep = Endpoint(r);
    if (ep is null) return Results.BadRequest(new { error = "missing host or port" });
    var conn = await pool.GetAsync(ep.Value.host, ep.Value.port);
    var resp = await conn.RequestAsync<IdList>(CmdId.Request_Player_List, CmdId.Event_Player_List, null, requestTimeout);
    return Results.Json(new
    {
        host = ep.Value.host,
        port = ep.Value.port,
        playerIds = resp?.list ?? new List<int>(),
        count = resp?.list?.Count ?? 0,
    });
});

app.MapGet("/api/v1/players/{id:int}", async (int id, HttpRequest r) =>
{
    var ep = Endpoint(r);
    if (ep is null) return Results.BadRequest(new { error = "missing host or port" });
    var conn = await pool.GetAsync(ep.Value.host, ep.Value.port);
    var resp = await conn.RequestAsync<PlayerInfo>(CmdId.Request_Player_Info, CmdId.Event_Player_Info, new Id(id), requestTimeout);
    if (resp is null) return Results.NotFound(new { error = "no info for player", id });
    return Results.Json(resp);
});

app.MapGet("/api/v1/playfields", async (HttpRequest r) =>
{
    var ep = Endpoint(r);
    if (ep is null) return Results.BadRequest(new { error = "missing host or port" });
    var conn = await pool.GetAsync(ep.Value.host, ep.Value.port);
    var resp = await conn.RequestAsync<PlayfieldList>(CmdId.Request_Playfield_List, CmdId.Event_Playfield_List, null, requestTimeout);
    return Results.Json(new
    {
        host = ep.Value.host,
        port = ep.Value.port,
        playfields = resp?.playfields ?? new List<string>(),
    });
});

app.MapGet("/api/v1/playfields/{name}/entities", async (string name, HttpRequest r) =>
{
    var ep = Endpoint(r);
    if (ep is null) return Results.BadRequest(new { error = "missing host or port" });
    var conn = await pool.GetAsync(ep.Value.host, ep.Value.port);
    var resp = await conn.RequestAsync<PlayfieldEntityList>(CmdId.Request_Playfield_Entity_List, CmdId.Event_Playfield_Entity_List, new PString(name), requestTimeout);
    return Results.Json(new
    {
        host = ep.Value.host,
        port = ep.Value.port,
        playfield = name,
        entities = resp?.entities ?? new List<EntityInfo>(),
    });
});

app.MapPost("/api/v1/console", async (HttpRequest r) =>
{
    var ep = Endpoint(r);
    if (ep is null) return Results.BadRequest(new { error = "missing host or port" });
    using var sr = new StreamReader(r.Body);
    var body = await sr.ReadToEndAsync();
    var line = body.Trim();
    if (string.IsNullOrEmpty(line)) return Results.BadRequest(new { error = "empty body" });
    var conn = await pool.GetAsync(ep.Value.host, ep.Value.port);
    conn.SendFireAndForget(CmdId.Request_ConsoleCommand, new PString(line));
    return Results.Json(new { sent = line });
});

EndpointMaps.MapAll(app, pool, requestTimeout);

app.Logger.LogInformation("panel-empyrion-bridge listening; request_timeout={Timeout}s authMode={Mode}",
    requestTimeout.TotalSeconds, string.IsNullOrEmpty(apiToken) ? "none" : "bearer");
app.Run();

public partial class Program { }
