using System.Globalization;
using System.Text;

namespace CashLineERP.Application.Reporting;

internal static class FinancialStatementExportBuilder
{
    public static FinancialStatementExportContent Build(FinancialStatementReportView report, string formatCode)
    {
        var normalizedFormatCode = FinancialStatementCatalog.NormalizeFormatCode(formatCode);
        var fileName = FinancialStatementCatalog.BuildFileName(report.StatementCode, normalizedFormatCode, report.AsOfDate);

        return normalizedFormatCode switch
        {
            "CSV" => new FinancialStatementExportContent(
                Encoding.UTF8.GetBytes(BuildCsv(report)),
                "text/csv",
                fileName),
            "EXCEL" => new FinancialStatementExportContent(
                Encoding.UTF8.GetBytes(BuildSpreadsheetMl(report)),
                "application/vnd.ms-excel",
                fileName),
            "PDF" => new FinancialStatementExportContent(
                BuildPdf(report),
                "application/pdf",
                fileName),
            _ => throw new InvalidOperationException($"Unsupported financial statement export format '{formatCode}'.")
        };
    }

    private static string BuildCsv(FinancialStatementReportView report)
    {
        var builder = new StringBuilder();
        builder.AppendLine("SectionCode,SectionTitle,LineCode,LineLabel,GeneralLedgerAccountCode,GeneralLedgerAccountName,OpeningAmount,DebitAmount,CreditAmount,NetAmount,ClosingAmount,ActivityDate,BatchNumber,JournalNumber,DocumentType,ReferenceNumber,DescriptionText");
        foreach (var row in report.Rows)
        {
            builder.AppendLine(string.Join(',',
                Escape(row.SectionCode),
                Escape(row.SectionTitle),
                Escape(row.LineCode),
                Escape(row.LineLabel),
                Escape(row.GeneralLedgerAccountCode),
                Escape(row.GeneralLedgerAccountName),
                Escape(row.OpeningAmount),
                Escape(row.DebitAmount),
                Escape(row.CreditAmount),
                Escape(row.NetAmount),
                Escape(row.ClosingAmount),
                Escape(row.ActivityDate?.ToString("yyyy-MM-dd")),
                Escape(row.BatchNumber),
                Escape(row.JournalNumber),
                Escape(row.DocumentType),
                Escape(row.ReferenceNumber),
                Escape(row.DescriptionText)));
        }

        return builder.ToString();
    }

    private static string BuildSpreadsheetMl(FinancialStatementReportView report)
    {
        var rows = new StringBuilder();
        rows.AppendLine(Row("Statement", report.StatementTitle));
        rows.AppendLine(Row("As Of", report.AsOfDate.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)));
        if (report.DateFrom.HasValue || report.DateTo.HasValue)
        {
            rows.AppendLine(Row("Date From", report.DateFrom?.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) ?? string.Empty));
            rows.AppendLine(Row("Date To", report.DateTo?.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) ?? string.Empty));
        }

        rows.AppendLine("<Row>");
        foreach (var heading in new[]
                 {
                     "Section",
                     "Line",
                     "GL Account",
                     "GL Account Name",
                     "Opening",
                     "Debit",
                     "Credit",
                     "Net",
                     "Closing",
                     "Activity Date",
                     "Batch",
                     "Journal",
                     "Document Type",
                     "Reference",
                     "Description"
                 })
        {
            rows.AppendLine(Cell(heading));
        }

        rows.AppendLine("</Row>");

        foreach (var row in report.Rows)
        {
            rows.AppendLine("<Row>");
            rows.AppendLine(Cell(row.SectionTitle));
            rows.AppendLine(Cell(row.LineLabel));
            rows.AppendLine(Cell(row.GeneralLedgerAccountCode));
            rows.AppendLine(Cell(row.GeneralLedgerAccountName));
            rows.AppendLine(Cell(row.OpeningAmount));
            rows.AppendLine(Cell(row.DebitAmount));
            rows.AppendLine(Cell(row.CreditAmount));
            rows.AppendLine(Cell(row.NetAmount));
            rows.AppendLine(Cell(row.ClosingAmount));
            rows.AppendLine(Cell(row.ActivityDate?.ToString("yyyy-MM-dd")));
            rows.AppendLine(Cell(row.BatchNumber));
            rows.AppendLine(Cell(row.JournalNumber));
            rows.AppendLine(Cell(row.DocumentType));
            rows.AppendLine(Cell(row.ReferenceNumber));
            rows.AppendLine(Cell(row.DescriptionText));
            rows.AppendLine("</Row>");
        }

        return $"""
<?xml version="1.0"?>
<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
 xmlns:o="urn:schemas-microsoft-com:office:office"
 xmlns:x="urn:schemas-microsoft-com:office:excel"
 xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">
  <Worksheet ss:Name="{EscapeXml(report.StatementTitle)}">
    <Table>
      {rows}
    </Table>
  </Worksheet>
</Workbook>
""";
    }

    private static byte[] BuildPdf(FinancialStatementReportView report)
    {
        var textLines = new List<string>
        {
            report.StatementTitle,
            $"As Of: {report.AsOfDate:yyyy-MM-dd}",
            string.Empty
        };

        if (report.DateFrom.HasValue || report.DateTo.HasValue)
        {
            textLines.Add($"Date Range: {report.DateFrom:yyyy-MM-dd} to {report.DateTo:yyyy-MM-dd}");
            textLines.Add(string.Empty);
        }

        foreach (var metric in report.Metrics)
        {
            textLines.Add($"{metric.MetricLabel}: {metric.Amount:N2}");
        }

        if (report.Metrics.Count > 0)
        {
            textLines.Add(string.Empty);
        }

        foreach (var row in report.Rows.Take(80))
        {
            var amount = row.ClosingAmount ?? row.NetAmount ?? row.DebitAmount ?? row.CreditAmount ?? 0m;
            textLines.Add($"{row.SectionTitle} | {row.LineLabel} | {amount:N2}");
        }

        var escapedLines = textLines
            .Select(line => line
                .Replace("\\", "\\\\", StringComparison.Ordinal)
                .Replace("(", "\\(", StringComparison.Ordinal)
                .Replace(")", "\\)", StringComparison.Ordinal))
            .ToArray();

        var contentBuilder = new StringBuilder();
        contentBuilder.AppendLine("BT");
        contentBuilder.AppendLine("/F1 10 Tf");
        contentBuilder.AppendLine("50 780 Td");
        contentBuilder.AppendLine("14 TL");
        for (var index = 0; index < escapedLines.Length; index++)
        {
            contentBuilder.Append('(').Append(escapedLines[index]).AppendLine(") Tj");
            if (index < escapedLines.Length - 1)
            {
                contentBuilder.AppendLine("T*");
            }
        }

        contentBuilder.AppendLine("ET");
        var contentStream = contentBuilder.ToString();
        var objects = new[]
        {
            "<< /Type /Catalog /Pages 2 0 R >>",
            "<< /Type /Pages /Count 1 /Kids [3 0 R] >>",
            "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>",
            $"<< /Length {Encoding.ASCII.GetByteCount(contentStream)} >>\nstream\n{contentStream}endstream",
            "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>"
        };

        var builder = new StringBuilder();
        builder.Append("%PDF-1.4\n");
        var offsets = new List<int> { 0 };
        for (var index = 0; index < objects.Length; index++)
        {
            offsets.Add(Encoding.ASCII.GetByteCount(builder.ToString()));
            builder.Append(index + 1)
                .Append(" 0 obj\n")
                .Append(objects[index])
                .Append("\nendobj\n");
        }

        var xrefOffset = Encoding.ASCII.GetByteCount(builder.ToString());
        builder.Append("xref\n")
            .Append("0 ")
            .Append(objects.Length + 1)
            .Append('\n')
            .Append("0000000000 65535 f \n");

        foreach (var offset in offsets.Skip(1))
        {
            builder.Append(offset.ToString("D10", CultureInfo.InvariantCulture))
                .Append(" 00000 n \n");
        }

        builder.Append("trailer\n")
            .Append("<< /Size ")
            .Append(objects.Length + 1)
            .Append(" /Root 1 0 R >>\n")
            .Append("startxref\n")
            .Append(xrefOffset.ToString(CultureInfo.InvariantCulture))
            .Append("\n%%EOF");

        return Encoding.ASCII.GetBytes(builder.ToString());
    }

    private static string Row(string label, string value) => $"<Row>{Cell(label)}{Cell(value)}</Row>";

    private static string Cell(object? value) =>
        $"<Cell><Data ss:Type=\"String\">{EscapeXml(value?.ToString() ?? string.Empty)}</Data></Cell>";

    private static string Escape(object? value)
    {
        var text = value?.ToString() ?? string.Empty;
        if (!text.Contains(',') && !text.Contains('"') && !text.Contains('\n'))
        {
            return text;
        }

        return $"\"{text.Replace("\"", "\"\"")}\"";
    }

    private static string EscapeXml(string text) =>
        text
            .Replace("&", "&amp;", StringComparison.Ordinal)
            .Replace("<", "&lt;", StringComparison.Ordinal)
            .Replace(">", "&gt;", StringComparison.Ordinal)
            .Replace("\"", "&quot;", StringComparison.Ordinal)
            .Replace("'", "&apos;", StringComparison.Ordinal);
}
