package renderer

import (
	"bytes"
	"math"
	"strings"

	"github.com/jung-kurt/gofpdf"
	"github.com/tech/sendico/billing/documents/internal/content"
)

const (
	pageMarginLeft   = 20.0
	pageMarginRight  = 20.0
	pageMarginTop    = 20.0
	pageMarginBottom = 22.0
)

// Renderer builds a PDF document from tagged blocks.
type Renderer struct {
	Issuer        Issuer
	OwnerPassword string
}

// Render generates the PDF bytes for the provided blocks and footer hash.
func (r Renderer) Render(blocks []Block, footerHash string) ([]byte, error) {
	pdf := gofpdf.New("P", "mm", "A4", "")
	pdf.SetMargins(pageMarginLeft, pageMarginTop, pageMarginRight)
	pdf.SetAutoPageBreak(true, pageMarginBottom)
	pdf.SetCompression(false)
	pdf.SetAuthor(r.Issuer.LegalName, false)
	pdf.SetTitle(content.PDFTitleActOfAcceptance, false)

	owner := strings.TrimSpace(r.OwnerPassword)
	if owner != "" {
		pdf.SetProtection(gofpdf.CnProtectPrint, "", owner)
	}

	pdf.SetFooterFunc(func() {
		pdf.SetY(-15)
		pdf.SetFont("Helvetica", "", 8)

		footer := content.DocumentIntegrityHashPrefix + footerHash
		pdf.CellFormat(0, 5, footer, "", 0, "L", false, 0, "")
	})

	pdf.AddPage()

	if _, err := drawHeader(pdf, r.Issuer, pageMarginLeft, pageMarginTop); err != nil {
		return nil, err
	}

	pdf.Ln(6)

	for _, block := range blocks {
		renderBlock(pdf, block)

		if pdf.Error() != nil {
			return nil, pdf.Error()
		}
	}

	buf := &bytes.Buffer{}
	if err := pdf.Output(buf); err != nil {
		return nil, err
	}

	return buf.Bytes(), nil
}

func renderBlock(pdf *gofpdf.Fpdf, block Block) {
	switch block.Tag {
	case TagSpacer:
		pdf.Ln(6)
	case TagTitle:
		pdf.SetFont("Helvetica", "B", 14)

		for _, line := range block.Lines {
			if strings.TrimSpace(line) == "" {
				pdf.Ln(4)

				continue
			}

			pdf.CellFormat(0, 7, line, "", 1, "C", false, 0, "")
		}

		pdf.Ln(2)
	case TagSubtitle:
		pdf.SetFont("Helvetica", "", 11)

		for _, line := range block.Lines {
			if strings.TrimSpace(line) == "" {
				pdf.Ln(3)

				continue
			}

			pdf.CellFormat(0, 6, line, "", 1, "C", false, 0, "")
		}

		pdf.Ln(2)
	case TagMeta:
		pdf.SetFont("Helvetica", "", 9)

		for _, line := range block.Lines {
			if strings.TrimSpace(line) == "" {
				pdf.Ln(2)

				continue
			}

			pdf.CellFormat(0, 4.5, line, "", 1, "R", false, 0, "")
		}

		pdf.Ln(2)
	case TagSection:
		pdf.Ln(2)
		pdf.SetFont("Helvetica", "B", 11)

		for _, line := range block.Lines {
			if strings.TrimSpace(line) == "" {
				pdf.Ln(3)

				continue
			}

			pdf.CellFormat(0, 6, line, "", 1, "L", false, 0, "")
		}

		pdf.Ln(1)
	case TagText:
		pdf.SetFont("Helvetica", "", 10)

		text := strings.Join(block.Lines, "\n")
		pdf.MultiCell(0, 5, text, "", "L", false)
		pdf.Ln(1)
	case TagKV:
		renderKeyValue(pdf, block)
	case TagTable:
		renderTable(pdf, block)
	case TagSign:
		pdf.SetFont("Helvetica", "", 10)

		text := strings.Join(block.Lines, "\n")
		pdf.MultiCell(0, 6, text, "", "L", false)
		pdf.Ln(2)
	default:
		// Unknown tag: treat as plain text for resilience.
		pdf.SetFont("Helvetica", "", 10)

		text := strings.Join(block.Lines, "\n")
		pdf.MultiCell(0, 5, text, "", "L", false)
		pdf.Ln(1)
	}
}

func renderKeyValue(pdf *gofpdf.Fpdf, block Block) {
	pdf.SetFont("Helvetica", "", 10)

	usable := usableWidth(pdf)
	keyWidth := math.Round(usable * 0.35)
	valueWidth := usable - keyWidth
	lineHeight := 5.0

	for _, row := range block.Rows {
		if len(row) == 0 {
			continue
		}

		key := row[0]

		value := ""
		if len(row) > 1 {
			value = row[1]
		}

		x := pdf.GetX()
		y := pdf.GetY()

		pdf.SetXY(x, y)
		pdf.SetFont("Helvetica", "B", 10)
		pdf.MultiCell(keyWidth, lineHeight, key, "", "L", false)
		leftY := pdf.GetY()

		pdf.SetXY(x+keyWidth, y)
		pdf.SetFont("Helvetica", "", 10)
		pdf.MultiCell(valueWidth, lineHeight, value, "", "L", false)
		rightY := pdf.GetY()

		pdf.SetY(maxFloat(leftY, rightY))
	}

	pdf.Ln(1)
}

func renderTable(pdf *gofpdf.Fpdf, block Block) {
	if len(block.Rows) == 0 {
		return
	}

	usable := usableWidth(pdf)
	col1 := math.Round(usable * 0.7)
	col2 := usable - col1
	lineHeight := 6.0

	header := block.Rows[0]
	pdf.SetFont("Helvetica", "B", 10)

	if len(header) > 0 {
		pdf.CellFormat(col1, lineHeight, header[0], "1", 0, "L", false, 0, "")
	}

	if len(header) > 1 {
		pdf.CellFormat(col2, lineHeight, header[1], "1", 1, "R", false, 0, "")
	} else {
		pdf.CellFormat(col2, lineHeight, "", "1", 1, "R", false, 0, "")
	}

	pdf.SetFont("Helvetica", "", 10)

	for _, row := range block.Rows[1:] {
		colA := ""
		colB := ""

		if len(row) > 0 {
			colA = row[0]
		}

		if len(row) > 1 {
			colB = row[1]
		}

		x := pdf.GetX()
		y := pdf.GetY()
		pdf.MultiCell(col1, lineHeight, colA, "1", "L", false)
		leftY := pdf.GetY()
		pdf.SetXY(x+col1, y)
		pdf.MultiCell(col2, lineHeight, colB, "1", "R", false)
		rightY := pdf.GetY()
		pdf.SetY(maxFloat(leftY, rightY))
	}

	pdf.Ln(2)
}

func usableWidth(pdf *gofpdf.Fpdf) float64 {
	pageW, _ := pdf.GetPageSize()
	left, _, right, _ := pdf.GetMargins()

	return pageW - left - right
}

func maxFloat(a, b float64) float64 {
	if a > b {
		return a
	}

	return b
}
