using System.Globalization;
using System.IO.Compression;
using System.Text;
using QRCoder;

/// <summary>
/// Builds printable cover sheet PDFs using raw PDF 1.4 objects and QRCoder for QR images.
/// Each page includes a header, QR code, human-readable metadata, and a footer instruction line.
/// </summary>
public static class CoverSheetPdfGenerator
{
    private const float PageWidth = 612f;
    private const float PageHeight = 792f;
    private const float QrDisplaySize = 200f;
    private const float LeftMargin = 72f;

    public static byte[] Generate(CoverSheet sheet) =>
        GenerateBatch(new[] { sheet });

    public static byte[] GenerateBatch(IReadOnlyList<CoverSheet> sheets)
    {
        if (sheets.Count == 0)
            throw new ArgumentException("At least one cover sheet is required.", nameof(sheets));

        var pages = sheets.Select(BuildPage).ToList();
        return BuildPdf(pages);
    }

    private sealed record PageData(string ContentStream, byte[] ImageRgb, int ImageWidth, int ImageHeight);

    private static PageData BuildPage(CoverSheet sheet)
    {
        var (rgb, width, height) = GenerateQrRgb(sheet.Code);
        var content = BuildContentStream(sheet);
        return new PageData(content, rgb, width, height);
    }

    private static (byte[] Rgb, int Width, int Height) GenerateQrRgb(string code)
    {
        using var generator = new QRCodeGenerator();
        using var data = generator.CreateQrCode(code, QRCodeGenerator.ECCLevel.M);

        var modules = data.ModuleMatrix.Count;
        const int scale = 8;
        const int quiet = 2;
        var size = (modules + quiet * 2) * scale;
        var rgb = new byte[size * size * 3];

        for (var y = 0; y < size; y++)
        {
            for (var x = 0; x < size; x++)
            {
                var mx = x / scale - quiet;
                var my = y / scale - quiet;
                var dark = mx >= 0 && my >= 0 && mx < modules && my < modules && data.ModuleMatrix[my][mx];
                var color = dark ? (byte)0 : (byte)255;
                var i = (y * size + x) * 3;
                rgb[i] = color;
                rgb[i + 1] = color;
                rgb[i + 2] = color;
            }
        }

        return (rgb, size, size);
    }

    private static string BuildContentStream(CoverSheet sheet)
    {
        var sb = new StringBuilder();
        var qrX = (PageWidth - QrDisplaySize) / 2f;
        const float qrY = 470f;

        sb.AppendLine(CultureInfo.InvariantCulture, $"q {QrDisplaySize} 0 0 {QrDisplaySize} {qrX} {qrY} cm /Im1 Do Q");

        WriteTextLine(sb, 18f, LeftMargin, 740f, "VigilCare Records - Cover Sheet");

        var y = 430f;
        y = WriteTextLine(sb, 12f, LeftMargin, y, $"Code: {sheet.Code}");
        y = WriteTextLine(sb, 12f, LeftMargin, y, $"Batch Type: {sheet.BatchType.ToDbString()}");
        y = WriteTextLine(sb, 12f, LeftMargin, y, $"Track: {sheet.Track.ToDbString()}");

        if (sheet.Patient is not null)
        {
            y = WriteTextLine(sb, 12f, LeftMargin, y,
                $"Patient: {sheet.Patient.FullName} (MRN {sheet.Patient.Mrn})");
        }

        if (sheet.AssignToUser is not null)
        {
            y = WriteTextLine(sb, 12f, LeftMargin, y,
                $"Assigned To: {sheet.AssignToUser.FullName}");
        }

        var generated = sheet.CreatedAt.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
        WriteTextLine(sb, 12f, LeftMargin, y, $"Generated: {generated}");

        WriteTextLine(sb, 10f, LeftMargin, 48f,
            "Attach to front of chart section. Scanner reads QR code automatically.");

        return sb.ToString();
    }

    private static float WriteTextLine(StringBuilder sb, float fontSize, float x, float y, string text)
    {
        sb.AppendLine("BT");
        sb.AppendLine(CultureInfo.InvariantCulture, $"/F1 {fontSize} Tf");
        sb.AppendLine(CultureInfo.InvariantCulture, $"{x} {y} Td");
        sb.AppendLine(CultureInfo.InvariantCulture, $"({EscapePdfString(text)}) Tj");
        sb.AppendLine("ET");
        return y - fontSize - 8f;
    }

    private static byte[] BuildPdf(IReadOnlyList<PageData> pages)
    {
        using var ms = new MemoryStream();
        ms.Write("%PDF-1.4\n"u8);

        var offsets = new List<long>();
        const int catalogId = 1;
        const int pagesId = 2;
        const int fontId = 3;
        var pageIds = Enumerable.Range(0, pages.Count).Select(i => 4 + i * 3).ToArray();
        var contentIds = pageIds.Select(id => id + 1).ToArray();
        var imageIds = pageIds.Select(id => id + 2).ToArray();

        offsets.Add(ms.Position);
        WriteAscii(ms, $"{catalogId} 0 obj\n<< /Type /Catalog /Pages {pagesId} 0 R >>\nendobj\n");

        offsets.Add(ms.Position);
        var kids = string.Join(" ", pageIds.Select(id => $"{id} 0 R"));
        WriteAscii(ms, $"{pagesId} 0 obj\n<< /Type /Pages /Kids [{kids}] /Count {pages.Count} >>\nendobj\n");

        offsets.Add(ms.Position);
        WriteAscii(ms, $"{fontId} 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj\n");

        for (var i = 0; i < pages.Count; i++)
        {
            var page = pages[i];
            var pageId = pageIds[i];
            var contentId = contentIds[i];
            var imageId = imageIds[i];
            var contentBytes = Encoding.ASCII.GetBytes(page.ContentStream);
            var imageBytes = FlateEncode(page.ImageRgb);

            offsets.Add(ms.Position);
            WriteAscii(ms,
                $"{pageId} 0 obj\n" +
                $"<< /Type /Page /Parent {pagesId} 0 R " +
                $"/MediaBox [0 0 {PageWidth} {PageHeight}] " +
                $"/Contents {contentId} 0 R " +
                $"/Resources << /Font << /F1 {fontId} 0 R >> /XObject << /Im1 {imageId} 0 R >> >> >>\n" +
                "endobj\n");

            offsets.Add(ms.Position);
            WriteAscii(ms,
                $"{contentId} 0 obj\n<< /Length {contentBytes.Length} >>\nstream\n");
            ms.Write(contentBytes);
            WriteAscii(ms, "\nendstream\nendobj\n");

            offsets.Add(ms.Position);
            WriteAscii(ms,
                $"{imageId} 0 obj\n" +
                $"<< /Type /XObject /Subtype /Image " +
                $"/Width {page.ImageWidth} /Height {page.ImageHeight} " +
                "/ColorSpace /DeviceRGB /BitsPerComponent 8 " +
                $"/Filter /FlateDecode /Length {imageBytes.Length} >>\nstream\n");
            ms.Write(imageBytes);
            WriteAscii(ms, "\nendstream\nendobj\n");
        }

        var xrefOffset = ms.Position;
        WriteAscii(ms, "xref\n");
        WriteAscii(ms, $"0 {offsets.Count + 1}\n");
        WriteAscii(ms, "0000000000 65535 f \n");
        foreach (var offset in offsets)
            WriteAscii(ms, $"{offset:D10} 00000 n \n");

        WriteAscii(ms,
            "trailer\n" +
            $"<< /Size {offsets.Count + 1} /Root {catalogId} 0 R >>\n" +
            "startxref\n" +
            $"{xrefOffset}\n" +
            "%%EOF\n");

        return ms.ToArray();
    }

    private static void WriteAscii(Stream stream, string text) =>
        stream.Write(Encoding.ASCII.GetBytes(text));

    private static byte[] FlateEncode(byte[] data)
    {
        using var output = new MemoryStream();
        using (var deflate = new ZLibStream(output, CompressionLevel.Optimal, leaveOpen: true))
            deflate.Write(data, 0, data.Length);

        return output.ToArray();
    }

    private static string EscapePdfString(string value) =>
        value.Replace("\\", "\\\\", StringComparison.Ordinal)
            .Replace("(", "\\(", StringComparison.Ordinal)
            .Replace(")", "\\)", StringComparison.Ordinal);
}
