using System.Drawing;
using System.Globalization;
using DevExpress.Office;
using DevExpress.XtraRichEdit;
using DevExpress.XtraRichEdit.API.Native;
using Microsoft.Extensions.Options;

namespace ROLAC.API.Services.Disbursement;

/// <summary>
/// Renders a check onto pre-printed 8.5"x11" three-stub stock using the DevExpress Office
/// (RichEdit) API. Fields are placed as absolutely-positioned floating TextBoxes so they align to
/// the boxes printed on the stock; every coordinate comes from <see cref="CheckPrintLayoutOptions"/>
/// (bound from "CheckPrint:Layout" in appsettings.json) so alignment can be tuned without recompiling.
/// The page is one check stub on top followed by two identical receipt copies.
/// </summary>
public class CheckPrintService : ICheckPrintService
{
    private readonly CheckPrintLayoutOptions _layout;

    public CheckPrintService(IOptions<CheckPrintLayoutOptions> layout)
    {
        _layout = layout.Value;
    }

    public Task<Stream> RenderPdfAsync(CheckPrintModel model)
    {
        using var server = new RichEditDocumentServer();
        var doc = server.Document;
        doc.BeginUpdate();
        try
        {
            doc.Unit = DocumentUnit.Inch;
            var section = doc.Sections[0];
            section.Page.Width = 8.5f;
            section.Page.Height = 11f;
            section.Margins.Left = section.Margins.Right = 0.6f;
            section.Margins.Top = section.Margins.Bottom = 0.5f;

            // Floating TextBoxes must anchor to a paragraph; everything is positioned relative to
            // the page, so a single empty anchor paragraph at the document start is enough.
            var anchor = doc.Paragraphs[0];

            BuildCheckStub(doc, anchor, model);
            BuildReceiptStub(doc, anchor, model, _layout.Receipt1OriginY);
            BuildReceiptStub(doc, anchor, model, _layout.Receipt2OriginY);
        }
        finally
        {
            doc.EndUpdate();
        }

        var ms = new MemoryStream();
        server.ExportToPdf(ms);
        ms.Position = 0;
        return Task.FromResult<Stream>(ms);
    }

    // ── check page builders (absolute positioning) ─────────────────────────────

    private void BuildCheckStub(Document doc, Paragraph anchor, CheckPrintModel model)
    {
        var check = model.Check;
        var originY = _layout.CheckOriginY;

        PlaceField(doc, anchor, _layout.Payee, originY, 4.5f, check.PayeeName);
        PlaceField(doc, anchor, _layout.AmountNumeric, originY, 1.6f, FormatCurrency(check.Amount,13), rightAlign: false);
        PlaceField(doc, anchor, _layout.AmountWords, originY, 6.0f, model.AmountInWords);
        PlaceField(doc, anchor, _layout.CheckDate, originY, 1.6f, check.CheckDate.ToString("MM/dd/yyyy"));
        if (!string.IsNullOrWhiteSpace(check.Memo))
            PlaceField(doc, anchor, _layout.Memo, originY, 4.5f, check.Memo);
    }

    private void BuildReceiptStub(Document doc, Paragraph anchor, CheckPrintModel model, float originY)
    {
        var check = model.Check;

        PlaceField(doc, anchor, _layout.ReceiptPayee, originY, 4.5f, "Pay To The Order Of: " + check.PayeeName);
        PlaceField(doc, anchor, _layout.ReceiptAmount, originY, 1.6f, "Amount: " + FormatCurrency(check.Amount), rightAlign: true);
        PlaceField(doc, anchor, _layout.ReceiptDate, originY, 1.6f, check.CheckDate.ToString("MM/dd/yyyy"));
        if (!string.IsNullOrWhiteSpace(check.Memo))
            PlaceField(doc, anchor, _layout.ReceiptMemo, originY, 4.5f, check.Memo);

        BuildVoucherGrid(doc, anchor, model, originY);
    }

    /// <summary>
    /// Two-column voucher detail grid: up to 12 expense lines (6 per column), filled column-major.
    /// Beyond 12 lines, prints the first 12 plus an overflow hint so the receipt total stays honest.
    /// </summary>
    private void BuildVoucherGrid(Document doc, Paragraph anchor, CheckPrintModel model, float originY)
    {
        var grid = _layout.Grid;
        var blockWidth = grid.DateWidth + grid.DescWidth + grid.AmountWidth;
        const int rowsPerColumn = 6;
        const int maxLines = rowsPerColumn * 2;

        float ColumnX(int column) => grid.OriginX + column * (blockWidth + grid.ColumnGap);

        void PlaceRow(float x, float y, string date, string description, string amount, bool bold)
        {
            PlaceText(doc, anchor, x, y, grid.DateWidth, date, grid.FontSize, bold, rightAlign: false);
            PlaceText(doc, anchor, x + grid.DateWidth, y, grid.DescWidth, description, grid.FontSize, bold, rightAlign: false);
            PlaceText(doc, anchor, x + grid.DateWidth + grid.DescWidth, y, grid.AmountWidth, amount, grid.FontSize, bold, rightAlign: true);
        }

        if (grid.ShowGridHeaders)
        {
            var headerY = originY + grid.HeaderOffsetY;
            for (var column = 0; column < 2; column++)
                PlaceRow(ColumnX(column), headerY, "Date", "Description", "Amount", bold: true);
        }

        var printed = Math.Min(model.Lines.Count, maxLines);
        for (var i = 0; i < printed; i++)
        {
            var line = model.Lines[i];
            var column = i < rowsPerColumn ? 0 : 1;
            var row = i % rowsPerColumn;
            var y = originY + grid.OffsetY + row * grid.RowHeight;
            var date = line.Expense?.ExpenseDate.ToString("MM/dd/yyyy") ?? "";
            PlaceRow(ColumnX(column), y, date, line.Description, FormatCurrency(line.Amount), bold: false);
        }

        if (model.Lines.Count > maxLines)
        {
            var remaining = model.Lines.Count - maxLines;
            var remainingTotal = model.Lines.Skip(maxLines).Sum(line => line.Amount);
            var hint = $"…and {remaining} more line(s) ({FormatCurrency(remainingTotal)})";
            PlaceText(doc, anchor, grid.OriginX, originY + grid.OverflowOffsetY,
                blockWidth * 2 + grid.ColumnGap, hint, grid.FontSize, bold: false, rightAlign: false);
        }
    }

    /// <summary>Places one configured field's value at its stub-relative position.</summary>
    private void PlaceField(Document doc, Paragraph anchor, FieldPos field, float stubOriginY,
        float width, string text, bool rightAlign = false)
    {
        PlaceText(doc, anchor, field.X, stubOriginY + field.OffsetY, width, text,
            field.FontSize, field.Bold, rightAlign);
    }

    /// <summary>
    /// Inserts a borderless, fill-less floating TextBox positioned absolutely relative to the page
    /// (X from the left edge, Y from the top edge) and writes <paramref name="text"/> into it.
    /// </summary>
    private void PlaceText(Document doc, Paragraph anchor, float x, float y, float width,
        string text, float fontSize, bool bold, bool rightAlign)
    {
        var shape = doc.Shapes.InsertTextBox(anchor.Range.Start);
        shape.HorizontalAlignment = ShapeHorizontalAlignment.None;
        shape.VerticalAlignment = ShapeVerticalAlignment.None;
        shape.RelativeHorizontalPosition = ShapeRelativeHorizontalPosition.Page;
        shape.RelativeVerticalPosition = ShapeRelativeVerticalPosition.Page;
        // Pull the box up-and-left by the calibration inset so the text inside lands exactly on the
        // configured (x, y) rather than down-and-right of it.
        shape.Offset = new PointF(x - _layout.TextInsetX, y - _layout.TextInsetY);
        shape.Size = new SizeF(width, Math.Max(0.2f, fontSize / 72f + 0.08f));
        shape.Fill.SetNoFill();
        shape.Line.Fill.SetNoFill();

        var body = shape.ShapeFormat.TextBox.Document;
        var range = body.InsertText(body.Range.Start, text);

        var characters = body.BeginUpdateCharacters(range);
        characters.FontSize = fontSize;
        characters.Bold = bold;
        body.EndUpdateCharacters(characters);

        if (rightAlign)
        {
            var paragraphs = body.BeginUpdateParagraphs(range);
            paragraphs.Alignment = ParagraphAlignment.Right;
            body.EndUpdateParagraphs(paragraphs);
        }
    }

    public Task<Stream> RenderReceiptPdfAsync(CheckPrintModel model)
    {
        using var server = new RichEditDocumentServer();
        var document = server.Document;
        document.BeginUpdate();
        try
        {
            document.Unit = DocumentUnit.Inch;
            var section = document.Sections[0];
            section.Page.Width = 8.5f;
            section.Page.Height = 11f;
            section.Margins.Left = section.Margins.Right = 0.8f;
            section.Margins.Top = section.Margins.Bottom = 0.8f;

            document.AppendHtmlText(BuildReceiptHtml(model));
        }
        finally
        {
            document.EndUpdate();
        }

        var stream = new MemoryStream();
        server.ExportToPdf(stream);
        stream.Position = 0;
        return Task.FromResult<Stream>(stream);
    }

    private static string BuildReceiptHtml(CheckPrintModel model)
    {
        var issuer = model.Issuer;
        var check = model.Check;

        var issuerAddress = Encode(JoinAddress(issuer.Address, issuer.City, issuer.State, issuer.ZipCode));
        var payeeAddress = Encode(JoinAddress(check.PayeeAddress, check.PayeeCity, check.PayeeState, check.PayeeZip));

        var detailRows = new System.Text.StringBuilder();
        foreach (var line in model.Lines)
        {
            var date = line.Expense?.ExpenseDate.ToString("MM/dd/yyyy") ?? "";
            detailRows.Append(
                "<tr>" +
                $"<td>{Encode(date)}</td>" +
                $"<td>{Encode(line.Description)}</td>" +
                $"<td align=\"right\">{Encode(FormatCurrency(line.Amount))}</td>" +
                "</tr>");
        }

        var signedOn = check.ReceiptSignedAt.HasValue
            ? check.ReceiptSignedAt.Value.ToLocalTime().ToString("MM/dd/yyyy HH:mm")
            : "";

        var signatureBlock = "";
        if (model.SignatureImage is { Length: > 0 })
        {
            var base64 = Convert.ToBase64String(model.SignatureImage);
            signatureBlock =
                $"<p><img src=\"data:{model.SignatureContentType};base64,{base64}\" width=\"260\" /></p>";
        }

        return
            "<div style=\"font-family:Arial;font-size:11pt;color:#111;\">" +
            "<h2 style=\"text-align:center;margin:0;\">Disbursement Receipt / 簽收收據</h2>" +
            $"<p style=\"text-align:center;margin:4px 0 16px 0;\"><b>{Encode(issuer.Name)}</b><br/>{issuerAddress}</p>" +

            "<h3 style=\"margin:8px 0;\">Check Information / 支票資訊</h3>" +
            "<table border=\"1\" cellspacing=\"0\" cellpadding=\"5\" width=\"100%\" style=\"border-collapse:collapse;\">" +
            $"<tr><td width=\"22%\"><b>Check No. / 支票號碼</b></td><td width=\"28%\">{Encode(check.CheckNumber)}</td>" +
            $"<td width=\"22%\"><b>Date / 日期</b></td><td width=\"28%\">{check.CheckDate:MM/dd/yyyy}</td></tr>" +
            $"<tr><td><b>Payee / 收款人</b></td><td colspan=\"3\">{Encode(check.PayeeName)} {payeeAddress}</td></tr>" +
            $"<tr><td><b>Amount / 金額</b></td><td colspan=\"3\">{Encode(FormatCurrency(check.Amount))} &mdash; {Encode(model.AmountInWords)}</td></tr>" +
            $"<tr><td><b>Memo / 摘要</b></td><td colspan=\"3\">{Encode(check.Memo ?? "")}</td></tr>" +
            "</table>" +

            "<h3 style=\"margin:16px 0 8px 0;\">Disbursement Detail / 撥款明細</h3>" +
            "<table border=\"1\" cellspacing=\"0\" cellpadding=\"5\" width=\"100%\" style=\"border-collapse:collapse;\">" +
            "<tr><th align=\"left\">Date / 日期</th><th align=\"left\">Description / 說明</th><th align=\"right\">Amount / 金額</th></tr>" +
            detailRows +
            $"<tr><td></td><td align=\"right\"><b>TOTAL / 合計</b></td><td align=\"right\"><b>{Encode(FormatCurrency(check.Amount))}</b></td></tr>" +
            "</table>" +

            "<h3 style=\"margin:16px 0 8px 0;\">Acknowledgement of Receipt / 收款簽收</h3>" +
            "<p>I acknowledge receipt of the above payment in full. / 本人確認已如數收到上述款項。</p>" +
            $"<p><b>Received by / 簽收人:</b> {Encode(check.ReceiptSignedName ?? "")}<br/>" +
            $"<b>Date / 簽收日期:</b> {Encode(signedOn)}</p>" +
            signatureBlock +
            "</div>";
    }

    private static string Encode(string? text) => System.Net.WebUtility.HtmlEncode(text ?? "");

    // ── low-level helpers ──────────────────────────────────────────────────────
    private static string FormatCurrency(decimal amount, int paddingAstric = 0)
    {
        var c = (CultureInfo)CultureInfo.GetCultureInfo("en-US").Clone();
        c.NumberFormat.CurrencySymbol = "";
        string formatedAmount = amount.ToString("#,##0.00", CultureInfo.GetCultureInfo("en-US"));
        return paddingAstric > 0 ? formatedAmount.PadLeft(paddingAstric, '*') : formatedAmount;
    }

    private static string JoinAddress(string? addr, string? city, string? state, string? zip)
    {
        var cityLine = string.Join(", ",
            new[] { city, string.Join(" ", new[] { state, zip }.Where(s => !string.IsNullOrWhiteSpace(s))) }
            .Where(s => !string.IsNullOrWhiteSpace(s)));
        return string.Join("  ", new[] { addr, cityLine }.Where(s => !string.IsNullOrWhiteSpace(s)));
    }
}
