// Program.cs — ASP.NET Core 8 backend: builds FAT12 on-the-fly (GET) and extracts on POST
using System.Buffers.Binary;
using System.Text;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using System.Security.Cryptography.X509Certificates;

var builder = WebApplication.CreateBuilder(args);

// Require client certificates (mTLS). Configure server cert via appsettings.json or env variables.
builder.WebHost.ConfigureKestrel(k =>
{
    k.ConfigureHttpsDefaults(o =>
    {
        o.ClientCertificateMode = Microsoft.AspNetCore.Server.Kestrel.Https.ClientCertificateMode.RequireCertificate;
    });
});

var app = builder.Build();

// Basic client-cert gate (replace with your CA/allow-list validation)
app.Use(async (ctx, next) =>
{
    if (ctx.Connection.ClientCertificate is null)
    {
        ctx.Response.StatusCode = 401;
        await ctx.Response.WriteAsync("client certificate required");
        return;
    }
    await next();
});

var root       = app.Environment.ContentRootPath;
var imagesDir  = Path.Combine(root, "Images");     // store ONLY floppy images here (per-tag)
var uploadsDir = Path.Combine(root, "Uploaded");   // optional archive of posted volumes
Directory.CreateDirectory(imagesDir);
Directory.CreateDirectory(uploadsDir);

// Load FF.CFG from disk (mounted file). Fallback to a sane default if missing.
string FfCfgText = LoadFfCfg(Path.Combine(root, "FF.CFG")) ??
  "host = acorn\r\n" +
  "interface = ibmpc\r\n" +
  "index-suppression = no\r\n" +
  "sound = on\r\n" +
  "sound-volume = 19\r\n" +
  "display-type = none\r\n";

// GET /api/volume?id=<hex64>  -> build tiny FAT12 in memory containing: [<image file>, FF.CFG]
app.MapGet("/api/volume", async ([FromQuery] string? id) =>
{
    if (!IsHex64(id)) return Results.BadRequest("id required (hex, up to 16 chars)");

    var imgPath = FindImage(imagesDir, id!);
    if (imgPath is null) return Results.NotFound($"No image for id {id}");

    byte[] payload = await File.ReadAllBytesAsync(imgPath);
    string payloadFileName = Path.GetFileName(imgPath);

    byte[] fat = BuildFat12TwoFiles(payload, payloadFileName, Encoding.ASCII.GetBytes(FfCfgText));

    return Results.File(new MemoryStream(fat), "application/octet-stream",
        fileDownloadName: $"{id!.ToLowerInvariant()}.fat.img", enableRangeProcessing: true);
});

// POST /api/volume?id=<hex64> -> receive modified FAT; extract image file; save back to Images/<id>.<ext>
app.MapPost("/api/volume", async ([FromQuery] string? id, HttpRequest req) =>
{
    if (!IsHex64(id)) return Results.BadRequest("id required (hex, up to 16 chars)");

    var ts = DateTime.UtcNow.ToString("yyyyMMdd-HHmmss");
    var uploadedPath = Path.Combine(uploadsDir, $"{id!.ToLowerInvariant()}-{ts}.fat.img");
    await using (var fs = File.Create(uploadedPath))
        await req.Body.CopyToAsync(fs);

    try
    {
        await using var s = File.OpenRead(uploadedPath);
        var (name, data) = ExtractFirstNonCfgRootFile(s);
        if (name is null || data is null)
            return Results.BadRequest("no floppy image found in volume");

        var ext = Path.GetExtension(name);
        if (string.IsNullOrEmpty(ext)) ext = ".img";
        var outPath = Path.Combine(imagesDir, $"{id.ToLowerInvariant()}{ext.ToLowerInvariant()}");
        await File.WriteAllBytesAsync(outPath, data);
        return Results.Ok(new { ok = true, saved = outPath });
    }
    catch (Exception ex)
    {
        return Results.BadRequest(new { error = "extract failed", detail = ex.Message });
    }
});

app.Run();

// ---------- Helpers ----------

static string? LoadFfCfg(string path)
{
    try
    {
        if (File.Exists(path)) return File.ReadAllText(path);
        return null;
    }
    catch { return null; }
}

static bool IsHex64(string? s)
{
    if (string.IsNullOrWhiteSpace(s) || s.Length > 16) return false;
    foreach (var c in s) if (!Uri.IsHexDigit(c)) return false;
    return true;
}

static string? FindImage(string dir, string id)
{
    var stem = id.ToLowerInvariant();
    var exts = new[] { ".adf", ".img", ".st", ".dsk", ".ima", ".adl" };
    foreach (var ext in exts)
    {
        var p = Path.Combine(dir, stem + ext);
        if (File.Exists(p)) return p;
    }
    var any = Directory.EnumerateFiles(dir)
                       .FirstOrDefault(f => Path.GetFileNameWithoutExtension(f)
                       .Equals(stem, StringComparison.OrdinalIgnoreCase));
    return any;
}

// Build tiny FAT12 with two root files: payloadName + FF.CFG
static byte[] BuildFat12TwoFiles(byte[] payload, string payloadName, byte[] ffCfg)
{
    const int BPS = 512;
    const int SPC = 1;
    const int RSVD = 1;
    const int FATS = 2;
    const int ROOT_ENTS = 32;
    const byte MEDIA = 0xF8;

    int clus = BPS * SPC;
    int clPayload = Math.Max(1, (payload.Length + clus - 1) / clus);
    int clCfg     = Math.Max(1, (ffCfg.Length   + clus - 1) / clus);
    int clTotal   = clPayload + clCfg;

    int fatEntries = clTotal + 2;
    int fatBytes   = (fatEntries * 3 + 1) / 2;
    int spFAT      = (fatBytes + BPS - 1) / BPS;

    int rootSecs   = ((ROOT_ENTS * 32) + (BPS - 1)) / BPS;
    int dataSecs   = clTotal * SPC;
    int totalSecs  = RSVD + FATS * spFAT + rootSecs + dataSecs;
    if (totalSecs >= 0x10000) throw new InvalidOperationException("volume too large");

    using var ms = new MemoryStream(totalSecs * BPS);
    var bw = new BinaryWriter(ms, Encoding.ASCII, leaveOpen:true);

    byte[] bs = new byte[512];
    bs[0]=0xEB; bs[1]=0x3C; bs[2]=0x90;
    Encoding.ASCII.GetBytes("MSDOS5.0").CopyTo(bs,3);
    WriteLE16(bs,11,(ushort)BPS);
    bs[13]=(byte)SPC;
    WriteLE16(bs,14,(ushort)RSVD);
    bs[16]=(byte)FATS;
    WriteLE16(bs,17,(ushort)ROOT_ENTS);
    WriteLE16(bs,19,(ushort)totalSecs);
    bs[21]=MEDIA;
    WriteLE16(bs,22,(ushort)spFAT);
    WriteLE16(bs,24,32); WriteLE16(bs,26,64);
    WriteLE32(bs,28,0);
    bs[36]=0x80; bs[38]=0x29; WriteLE32(bs,39,0x1234ABCD);
    Encoding.ASCII.GetBytes("GOTEK  VOL ").CopyTo(bs,43);
    Encoding.ASCII.GetBytes("FAT12   ").CopyTo(bs,54);
    bs[510]=0x55; bs[511]=0xAA;
    bw.Write(bs);

    int fatAligned = spFAT * BPS;
    byte[] fat = new byte[fatAligned];
    fat[0]=MEDIA; fat[1]=0xFF; fat[2]=0xFF;

    ushort firstPayload=2;
    ushort firstCfg=(ushort)(firstPayload+clPayload);

    for(int i=0;i<clPayload;i++){
        ushort cl=(ushort)(firstPayload+i);
        ushort val=(ushort)(i==clPayload-1?0x0FFF:cl+1);
        SetFAT12(fat,cl,val);
    }
    for(int i=0;i<clCfg;i++){
        ushort cl=(ushort)(firstCfg+i);
        ushort val=(ushort)(i==clCfg-1?0x0FFF:cl+1);
        SetFAT12(fat,cl,val);
    }
    for(int f=0;f<FATS;f++) bw.Write(fat);

    byte[] root = new byte[rootSecs*BPS];
    var n1 = To83(payloadName);
    Array.Copy(n1,0,root,0,11);
    root[11]=0x20; WriteLE16(root,26,firstPayload); WriteLE32(root,28,(uint)payload.Length);
    var n2 = To83("FF.CFG");
    Array.Copy(n2,0,root,32,11);
    root[32+11]=0x20; WriteLE16(root,32+26,firstCfg); WriteLE32(root,32+28,(uint)ffCfg.Length);
    bw.Write(root);

    WriteData(bw,payload,clPayload,BPS);
    WriteData(bw,ffCfg,clCfg,BPS);

    bw.Flush();
    return ms.ToArray();

    static void WriteLE16(byte[] b,int o,ushort v){ b[o]=(byte)v; b[o+1]=(byte)(v>>8); }
    static void WriteLE32(byte[] b,int o,uint v){ b[o]=(byte)v; b[o+1]=(byte)(v>>8); b[o+2]=(byte)(v>>16); b[o+3]=(byte)(v>>24); }
    static void SetFAT12(byte[] fat, ushort cluster, ushort value){
        int idx = (cluster*3)/2;
        if((cluster&1)!=0){
            ushort curr = (ushort)(fat[idx] | (fat[idx+1]<<8));
            curr = (ushort)((curr & 0x00FF) | ((value & 0x0FFF) << 8));
            fat[idx]=(byte)(curr&0xFF); fat[idx+1]=(byte)(curr>>8);
        } else {
            ushort curr = (ushort)(fat[idx] | (fat[idx+1]<<8));
            curr = (ushort)((curr & 0xF000) | (value & 0x0FFF));
            fat[idx]=(byte)(curr&0xFF); fat[idx+1]=(byte)(curr>>8);
        }
    }
    static byte[] To83(string name){
        name=name.Replace('\\','/'); if(name.Contains('/')) name=name[(name.LastIndexOf('/')+1)..];
        var dot=name.LastIndexOf('.');
        var basePart=dot>0?name[..dot]:name;
        var ext=dot>0?name[(dot+1)..]:"";
        basePart=basePart.ToUpperInvariant();
        ext=ext.ToUpperInvariant();
        if(string.IsNullOrEmpty(basePart)) basePart="FILE";
        Span<byte> out11 = stackalloc byte[11]; out11.Fill((byte)' ');
        Encoding.ASCII.GetBytes(basePart.Length>8?basePart[..8]:basePart).CopyTo(out11);
        Encoding.ASCII.GetBytes(ext.Length>3?ext[..3]:ext).CopyTo(out11[8..]);
        return out11.ToArray();
    }
    static void WriteData(BinaryWriter bw, byte[] data, int clusters, int bps){
        int written=0; byte[] pad=new byte[bps];
        for(int i=0;i<clusters;i++){
            int take=Math.Min(bps, data.Length-written);
            if(take>0) bw.Write(data, written, take);
            if(take<bps) bw.Write(pad, 0, bps-take);
            written += take;
        }
    }
}

// Minimal FAT12 extractor (first non-FF.CFG file from root)
static (string? name, byte[]? data) ExtractFirstNonCfgRootFile(Stream s)
{
    Span<byte> bpb = stackalloc byte[512];
    if (s.Read(bpb) != 512) throw new InvalidOperationException("short BPB");
    ushort bps = BinaryPrimitives.ReadUInt16LittleEndian(bpb.Slice(11, 2));
    byte spc   = bpb[13];
    ushort rsv = BinaryPrimitives.ReadUInt16LittleEndian(bpb.Slice(14, 2));
    byte fats  = bpb[16];
    ushort rootEnts = BinaryPrimitives.ReadUInt16LittleEndian(bpb.Slice(17, 2));
    ushort spFat = BinaryPrimitives.ReadUInt16LittleEndian(bpb.Slice(22, 2));
    if (bps != 512) throw new InvalidOperationException("bps!=512 unsupported");

    uint rootSecs     = (uint)((rootEnts * 32 + (bps - 1)) / bps);
    uint firstFatSec  = rsv;
    uint rootDirSec   = rsv + fats * spFat;
    uint firstDataSec = rootDirSec + rootSecs;

    s.Position = rootDirSec * 512;
    byte[] root = new byte[rootSecs * 512];
    if (s.Read(root, 0, root.Length) != root.Length) throw new InvalidOperationException("short root");

    for (int i = 0; i < root.Length; i += 32)
    {
        byte first = root[i];
        if (first == 0x00) break;
        if (first == 0xE5) continue;
        byte attr = root[i + 11];
        if ((attr & 0x08) != 0) continue; // vol label
        if ((attr & 0x10) != 0) continue; // dir

        string name = Encoding.ASCII.GetString(root, i, 8).TrimEnd(' ');
        string ext  = Encoding.ASCII.GetString(root, i + 8, 3).TrimEnd(' ');
        string full = ext.Length > 0 ? $"{name}.{ext}" : name;
        if (string.Equals(full, "FF.CFG", StringComparison.OrdinalIgnoreCase)) continue;

        ushort firstClus = BinaryPrimitives.ReadUInt16LittleEndian(root.AsSpan(i + 26, 2));
        uint size        = BinaryPrimitives.ReadUInt32LittleEndian(root.AsSpan(i + 28, 4));

        s.Position = firstFatSec * 512;
        byte[] fat = new byte[spFat * 512];
        if (s.Read(fat, 0, fat.Length) != fat.Length) throw new InvalidOperationException("short FAT");

        // FAT12 cluster walk
        List<uint> clusters = new();
        uint cl = firstClus;
        while (cl >= 2 && cl < 0xFF8)
        {
            clusters.Add(cl);
            uint idx = (uint)((cl * 3) / 2);
            ushort entry = BitConverter.ToUInt16(fat, (int)idx);
            uint next = (cl % 2 == 0) ? (uint)(entry & 0x0FFF) : (uint)(entry >> 4);
            cl = next;
            if (clusters.Count > 65536) break;
        }

        using var ms = new MemoryStream((int)size);
        foreach (var c in clusters)
        {
            ulong sector = firstDataSec + (ulong)((c - 2) * spc);
            s.Position = (long)sector * 512;
            byte[] tmp = new byte[spc * 512];
            int got = s.Read(tmp, 0, tmp.Length);
            if (got <= 0) break;
            int copy = (int)Math.Min((long)tmp.Length, (long)size - ms.Length);
            ms.Write(tmp, 0, copy);
            if (ms.Length >= size) break;
        }
        return (full, ms.ToArray());
    }
    return (null, null);
}
