package main

import (
	"fmt"
	"strconv"
	"strings"

	"codeberg.org/go-pdf/fpdf"
)

type ConfigPDF struct {
	Orientacion string
	Unidad      string
	Tamaño      string
	FontDir     string
	MarginLeft  float64
	MarginRight float64
	FooterH     float64
	LineH       float64
	FontName    string
	FontSize    float64
	CellPadding float64
}

type Empresa struct {
	Nombre    string
	Subtitulo string
	Footer    string
}

type Reporte struct {
	pdf    *fpdf.Fpdf
	cfg    ConfigPDF
	comp   Empresa
	pageW  float64
	pageH  float64
	usable float64
}

func sanitizarDir(s string) string {
	s = strings.ReplaceAll(s, "/", "-")
	s = strings.ReplaceAll(s, ":", "-")
	return strings.TrimSpace(s)
}

func nuevoReporte(cfg ConfigPDF, comp Empresa) Reporte {
	pdf := fpdf.New(cfg.Orientacion, cfg.Unidad, cfg.Tamaño, cfg.FontDir)

	pdf.AddUTF8Font("LiberationSerif", "", "assets/LiberationSerif-Regular.ttf")
	pdf.AddUTF8Font("LiberationSerif", "B", "assets/LiberationSerif-Bold.ttf")
	pdf.AddUTF8Font("LiberationSerif", "I", "assets/LiberationSerif-Italic.ttf")
	pdf.AddUTF8Font("LiberationSerif", "BI", "assets/LiberationSerif-BoldItalic.ttf")

	pdf.SetMargins(cfg.MarginLeft, 10, cfg.MarginRight)
	pdf.SetAutoPageBreak(true, cfg.FooterH)
	w, h := pdf.GetPageSize()

	r := Reporte{
		pdf:    pdf,
		cfg:    cfg,
		comp:   comp,
		pageW:  w,
		pageH:  h,
		usable: w - cfg.MarginLeft - cfg.MarginRight,
	}

	pdf.SetHeaderFunc(func() {
		pdf.SetFont(cfg.FontName, "B", 13)
		pdf.CellFormat(0, 8, comp.Nombre, "", 1, "C", false, 0, "")
		if comp.Subtitulo != "" {
			pdf.Ln(3)
			pdf.SetFont(cfg.FontName, "B", 12)
			pdf.CellFormat(0, 7, comp.Subtitulo, "", 1, "C", false, 0, "")
		}
		pdf.Ln(2)
		pdf.SetLineWidth(0.5)
		pdf.SetDrawColor(0, 0, 0)
		y := pdf.GetY()
		pdf.Line(cfg.MarginLeft, y, w-cfg.MarginRight, y)
		pdf.Ln(4)
	})

	pdf.AliasNbPages("")
	pdf.SetFooterFunc(func() {
		pdf.SetY(-15)
		pdf.SetFont(cfg.FontName, "", 9)
		if comp.Footer != "" {
			pdf.CellFormat(0, 5, comp.Footer, "", 1, "C", false, 0, "")
		}
		pdf.CellFormat(0, 5, "Página "+strconv.Itoa(pdf.PageNo())+"/{nb}", "", 0, "C", false, 0, "")
	})

	return r
}

func tituloGeneral(r Reporte, title string) {
	r.pdf.SetFont(r.cfg.FontName, "B", 13)
	r.pdf.CellFormat(0, 8, title, "", 1, "C", false, 0, "")
	r.pdf.Ln(3)
	r.pdf.SetLineWidth(0.4)
	y := r.pdf.GetY()
	r.pdf.Line(r.cfg.MarginLeft, y, r.pageW-r.cfg.MarginRight, y)
	r.pdf.Ln(6)
	r.pdf.SetFont(r.cfg.FontName, "", r.cfg.FontSize)
}

func filtrarColsOcultas(cols []string) []string {
	result := make([]string, 0, len(cols))
	for _, c := range cols {
		if !strings.HasPrefix(c, "_") {
			result = append(result, c)
		}
	}
	return result
}

func renderizarRegistro(r Reporte, etiqueta string, cols []string, row map[string]string) {
	minEspacio := 7 + 2 + r.cfg.LineH*4
	if r.pdf.GetY()+minEspacio > r.pageH-r.cfg.FooterH {
		r.pdf.AddPage()
	}

	r.pdf.SetFont(r.cfg.FontName, "B", r.cfg.FontSize+1)
	r.pdf.CellFormat(0, 7, etiqueta, "", 1, "L", false, 0, "")
	r.pdf.SetLineWidth(0.1)
	r.pdf.SetDrawColor(160, 160, 160)
	y := r.pdf.GetY()
	r.pdf.Line(r.cfg.MarginLeft, y, r.pageW-r.cfg.MarginRight, y)
	r.pdf.Ln(2)

	for _, col := range filtrarColsOcultas(cols) {
		val := row[col]
		if val == "" {
			continue
		}
		if strings.ContainsRune(val, '\t') {
			r.pdf.SetFont(r.cfg.FontName, "B", r.cfg.FontSize)
			r.pdf.CellFormat(0, 5, col+":", "", 1, "L", false, 0, "")
			renderizarTablaInline(r, val)
		} else if len(val) > 65 || strings.ContainsAny(val, "\n\r") {
			r.pdf.SetFont(r.cfg.FontName, "B", r.cfg.FontSize)
			r.pdf.CellFormat(0, 5, col+":", "", 1, "L", false, 0, "")
			r.pdf.SetFont(r.cfg.FontName, "", r.cfg.FontSize)
			r.pdf.MultiCell(0, 5, val, "", "L", false)
		} else {
			r.pdf.SetFont(r.cfg.FontName, "B", r.cfg.FontSize)
			r.pdf.CellFormat(55, 5, col+":", "", 0, "L", false, 0, "")
			r.pdf.SetFont(r.cfg.FontName, "", r.cfg.FontSize)
			r.pdf.MultiCell(0, 5, val, "", "L", false)
		}
	}
	r.pdf.Ln(3)
}

// registrosHCySecciones agrupa los subregistros por Sección y los muestra
// con un encabezado por sección y dos columnas: Pregunta | Respuesta.
func registrosHCySecciones(r Reporte, subRegistros []registroExpediente) {
	type seccionEntry struct {
		nombre string
		filas  []map[string]string
	}

	var secciones []seccionEntry
	seccionIndex := map[string]int{}

	for _, sub := range subRegistros {
		nombre := sub.row["Sección"]
		if nombre == "" {
			nombre = "—"
		}
		if _, ok := seccionIndex[nombre]; !ok {
			seccionIndex[nombre] = len(secciones)
			secciones = append(secciones, seccionEntry{nombre: nombre})
		}
		idx := seccionIndex[nombre]
		secciones[idx].filas = append(secciones[idx].filas, sub.row)
	}

	cols := []string{"Pregunta", "Respuesta"}

	for _, sec := range secciones {
		r.pdf.Ln(2)
		r.pdf.SetFont(r.cfg.FontName, "B", r.cfg.FontSize)
		r.pdf.CellFormat(0, 5, sec.nombre, "", 1, "L", false, 0, "")
		r.pdf.SetDrawColor(180, 180, 180)
		y := r.pdf.GetY()
		r.pdf.Line(r.cfg.MarginLeft, y, r.pageW-r.cfg.MarginRight, y)
		r.pdf.Ln(1)

		crearTablaPdf(r, cols, sec.filas)
	}
	r.pdf.Ln(2)
}

func renderizarTablaInline(r Reporte, val string) {
	var data [][]string
	for _, linea := range strings.Split(val, "\n") {
		if linea == "" {
			continue
		}
		data = append(data, strings.Split(linea, "\t"))
	}
	if len(data) > 1 {
		drawTable(r, data)
	}
}

// crearTablaPdf agrega una tabla con encabezados al reporte.
func crearTablaPdf(r Reporte, cols []string, rows []map[string]string) {
	if len(rows) == 0 {
		r.pdf.SetFont(r.cfg.FontName, "I", r.cfg.FontSize)
		r.pdf.CellFormat(0, 6, "Sin registros.", "", 1, "L", false, 0, "")
		return
	}

	cols = filtrarColsOcultas(cols)
	data := make([][]string, 0, len(rows)+1)
	data = append(data, cols)
	for _, row := range rows {
		rowData := make([]string, len(cols))
		for i, col := range cols {
			rowData[i] = row[col]
		}
		data = append(data, rowData)
	}
	drawTable(r, data)
}

// crearParrafosPdf agrega secciones con campos a partir de registros.
// Cada registro es una sección separada por una línea.
// Valores de más de 50 caracteres o con saltos de línea se muestran debajo del nombre del campo.
func crearParrafosPdf(r Reporte, etiqueta string, cols []string, rows []map[string]string) {
	if len(rows) == 0 {
		r.pdf.SetFont(r.cfg.FontName, "I", r.cfg.FontSize)
		r.pdf.CellFormat(0, 6, "Sin registros.", "", 1, "L", false, 0, "")
		return
	}
	cols = filtrarColsOcultas(cols)
	for idx, row := range rows {
		titulo := etiqueta
		if len(rows) > 1 {
			titulo = fmt.Sprintf("%s #%d", etiqueta, idx+1)
		}

		r.pdf.SetFont(r.cfg.FontName, "B", r.cfg.FontSize+1)
		r.pdf.CellFormat(0, 7, titulo, "", 1, "L", false, 0, "")
		r.pdf.SetLineWidth(0.1)
		r.pdf.SetDrawColor(160, 160, 160)
		y := r.pdf.GetY()
		r.pdf.Line(r.cfg.MarginLeft, y, r.pageW-r.cfg.MarginRight, y)
		r.pdf.Ln(2)

		for _, col := range cols {
			val := row[col]
			if val == "" {
				continue
			}
			if len(val) > 50 || strings.ContainsAny(val, "\n\r") {
				r.pdf.SetFont(r.cfg.FontName, "B", r.cfg.FontSize)
				r.pdf.CellFormat(0, 5, col+":", "", 1, "L", false, 0, "")
				r.pdf.SetFont(r.cfg.FontName, "", r.cfg.FontSize)
				r.pdf.MultiCell(0, 5, val, "", "L", false)
			} else {
				r.pdf.SetFont(r.cfg.FontName, "B", r.cfg.FontSize)
				r.pdf.CellFormat(55, 5, col+":", "", 0, "L", false, 0, "")
				r.pdf.SetFont(r.cfg.FontName, "", r.cfg.FontSize)
				r.pdf.MultiCell(0, 5, val, "", "L", false)
			}
		}
		r.pdf.Ln(5)
	}
}

func guardarPDF(r Reporte, ruta string) {
	if err := r.pdf.OutputFileAndClose(ruta); err != nil {
		logFatal(err)
	}
	logInfo("Generado: %s", ruta)
}

// --- Internos para dibujar tabla ---

func drawTable(r Reporte, data [][]string) {
	if len(data) == 0 {
		return
	}
	colW := r.usable / float64(len(data[0]))
	r.pdf.Ln(2)
	minEspacio := r.cfg.LineH * 3
	if r.pdf.GetY()+minEspacio > r.pageH-r.cfg.FooterH {
		r.pdf.AddPage()
	}
	drawHeaderRow(r, data[0], colW)
	for _, row := range data[1:] {
		drawDataRow(r, row, colW, data[0])
	}
	r.pdf.Ln(2)
}

func drawHeaderRow(r Reporte, headers []string, colW float64) {
	r.pdf.SetFont(r.cfg.FontName, "B", r.cfg.FontSize)
	r.pdf.SetLineWidth(0.1)
	r.pdf.SetDrawColor(160, 160, 160)
	split := splitCells(r.pdf, headers, colW, r.cfg.CellPadding)
	rowH := maxHeight(split, r.cfg.LineH)
	xStart, yStart := r.cfg.MarginLeft, r.pdf.GetY()

	for col, lines := range split {
		x := xStart + float64(col)*colW
		r.pdf.Rect(x, yStart, colW, rowH, "D")
		offsetY := (rowH - float64(len(lines))*r.cfg.LineH) / 2
		for i, line := range lines {
			r.pdf.SetXY(x+r.cfg.CellPadding, yStart+offsetY+float64(i)*r.cfg.LineH)
			r.pdf.CellFormat(colW-r.cfg.CellPadding*2, r.cfg.LineH, line, "", 0, "L", false, 0, "")
		}
	}
	r.pdf.SetXY(xStart, yStart+rowH)
	r.pdf.SetFont(r.cfg.FontName, "", r.cfg.FontSize)
}

func drawDataRow(r Reporte, row []string, colW float64, headers []string) {
	r.pdf.SetLineWidth(0.1)
	r.pdf.SetDrawColor(160, 160, 160)
	limit := r.pageH - r.cfg.FooterH

	split := splitCells(r.pdf, row, colW, r.cfg.CellPadding)
	maxLines := maxLinesCount(split)
	currentLine := 0

	for currentLine < maxLines {
		// Si fpdf entró en estado de error, sus operaciones de dibujo (incluida
		// AddPage) se vuelven no-op y este bucle no avanzaría nunca. Salir.
		if r.pdf.Err() {
			return
		}
		xStart, yStart := r.cfg.MarginLeft, r.pdf.GetY()
		linesFit := int((limit - yStart) / r.cfg.LineH)
		if linesFit <= 0 {
			r.pdf.AddPage()
			drawHeaderRow(r, headers, colW)
			// Guard: si AddPage no avanzó (Y no se redujo) no hay salto de página
			// real; continuar provocaría un bucle infinito.
			if r.pdf.GetY() >= yStart {
				return
			}
			continue
		}
		endLine := min(currentLine+linesFit, maxLines)
		blockH := float64(endLine-currentLine) * r.cfg.LineH

		for col, cellLines := range split {
			x := xStart + float64(col)*colW
			r.pdf.Rect(x, yStart, colW, blockH, "D")
			for i := currentLine; i < endLine && i < len(cellLines); i++ {
				r.pdf.SetXY(x+r.cfg.CellPadding, yStart+float64(i-currentLine)*r.cfg.LineH)
				r.pdf.CellFormat(colW-r.cfg.CellPadding*2, r.cfg.LineH, cellLines[i], "", 0, "L", false, 0, "")
			}
		}
		r.pdf.SetXY(xStart, yStart+blockH)
		currentLine = endLine
	}
}

func splitCells(pdf *fpdf.Fpdf, row []string, colW, pad float64) [][]string {
	result := make([][]string, len(row))
	usableW := colW - pad*2
	if usableW <= 0 {
		usableW = 1
	}
	for i, cell := range row {
		if cell == "" {
			result[i] = []string{""}
			continue
		}
		lines := pdf.SplitText(cell, usableW)
		if len(lines) == 0 {
			lines = []string{""}
		}
		result[i] = lines
	}
	return result
}

func maxHeight(split [][]string, lineH float64) float64 {
	m := 1
	for _, lines := range split {
		if len(lines) > m {
			m = len(lines)
		}
	}
	return float64(m) * lineH
}

func maxLinesCount(split [][]string) int {
	m := 0
	for _, lines := range split {
		if len(lines) > m {
			m = len(lines)
		}
	}
	return m
}
