package main

import (
	"bytes"
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"time"

	"codeberg.org/go-pdf/fpdf"
	"zeitkort-web/repository"
)

// ExtraLine is a manual invoice line item (e.g. "1 stk" of something).
type ExtraLine struct {
	Description string
	Quantity    float64
	Unit        string // "tímar" or "stk"
	UnitPrice   float64
}

type invoiceInput struct {
	InvoiceNumber string
	Date          time.Time
	DueDate       time.Time
	DueDays       int
	Comment       string
	MVGPercent    float64

	FromName, FromAddress, FromPhone, FromEmail, FromWeb, FromVAT string
	BankInfo                                                      string

	Company    repository.Company
	Assignment repository.Assignment
	HourlyRate float64
	Sessions   []repository.WorkSession
	ExtraLines []ExtraLine
}

// ── Handlers ─────────────────────────────────────────────────────────────────

type invoiceFormView struct {
	CompanyID     int64
	AssignmentID  int64
	Company       *repository.Company
	Assignment    *repository.Assignment
	TotalHours    float64
	TotalAmount   float64
	InvoiceNumber string
	Today         string
	DueDays       int
	MVGPercent    float64
	Cfg           Config
}

func (s *Server) handleInvoiceForm(w http.ResponseWriter, r *http.Request) {
	companyID := qInt64(r, "company_id")
	assignmentID := qInt64(r, "assignment_id")
	if assignmentID == 0 {
		s.renderMainWith(w, companyID, 0, "", "Select an assignment before making an invoice.")
		return
	}

	assignment, err := s.repo.GetAssignmentByID(assignmentID)
	if err != nil {
		s.renderMainWith(w, companyID, assignmentID, "", err.Error())
		return
	}
	company, err := s.repo.GetCompanyByID(assignment.CompanyID)
	if err != nil {
		s.renderMainWith(w, companyID, assignmentID, "", err.Error())
		return
	}
	hours, _ := s.repo.TotalHoursForAssignment(assignmentID)
	rate := assignment.HourlyRate
	if rate == 0 {
		rate = company.HourlyRate
	}

	now := time.Now()
	nextNum, _ := s.repo.NextInvoiceNumber(now)

	s.mu.Lock()
	cfg := s.Cfg
	s.mu.Unlock()

	s.render(w, "invoice_form", invoiceFormView{
		CompanyID:     assignment.CompanyID,
		AssignmentID:  assignmentID,
		Company:       company,
		Assignment:    assignment,
		TotalHours:    hours,
		TotalAmount:   hours * rate,
		InvoiceNumber: nextNum,
		Today:         now.Format("02-01-2006"),
		DueDays:       cfg.PaymentTermsDays,
		MVGPercent:    cfg.MVGPercent,
		Cfg:           cfg,
	})
}

// handleInvoiceLine returns one empty extra-line input row (htmx append).
func (s *Server) handleInvoiceLine(w http.ResponseWriter, r *http.Request) {
	s.render(w, "invoice_line", nil)
}

func (s *Server) handleInvoiceGenerate(w http.ResponseWriter, r *http.Request) {
	r.ParseForm()
	companyID := formInt64(r, "company_id")
	assignmentID := formInt64(r, "assignment_id")

	assignment, err := s.repo.GetAssignmentByID(assignmentID)
	if err != nil {
		s.renderMainWith(w, companyID, assignmentID, "", err.Error())
		return
	}
	company, err := s.repo.GetCompanyByID(assignment.CompanyID)
	if err != nil {
		s.renderMainWith(w, companyID, assignmentID, "", err.Error())
		return
	}
	sessions, err := s.repo.WorkSessionsByAssignment(assignmentID, 1000)
	if err != nil {
		s.renderMainWith(w, companyID, assignmentID, "", err.Error())
		return
	}

	rate := assignment.HourlyRate
	if rate == 0 {
		rate = company.HourlyRate
	}

	mvg, _ := strconv.ParseFloat(r.FormValue("mvg"), 64)
	dueDays, _ := strconv.Atoi(r.FormValue("due_days"))
	if dueDays <= 0 {
		dueDays = 8
	}
	now := time.Now()

	in := invoiceInput{
		InvoiceNumber: r.FormValue("invoice_number"),
		Date:          now,
		DueDate:       now.AddDate(0, 0, dueDays),
		DueDays:       dueDays,
		Comment:       r.FormValue("comment"),
		MVGPercent:    mvg,
		FromName:      r.FormValue("from_name"),
		FromAddress:   r.FormValue("from_address"),
		FromPhone:     r.FormValue("from_phone"),
		FromEmail:     r.FormValue("from_email"),
		FromWeb:       r.FormValue("from_web"),
		FromVAT:       r.FormValue("from_vat"),
		BankInfo:      r.FormValue("bank_info"),
		Company:       *company,
		Assignment:    *assignment,
		HourlyRate:    rate,
		Sessions:      sessions,
		ExtraLines:    parseExtraLines(r),
	}

	pdfBytes, total, err := generateInvoicePDF(in)
	if err != nil {
		s.renderMainWith(w, companyID, assignmentID, "", "PDF generation failed: "+err.Error())
		return
	}

	// Record the invoice so numbering keeps incrementing (best effort).
	if _, err := s.repo.InsertInvoice(repository.Invoice{
		AssignmentID:  assignmentID,
		InvoiceNumber: in.InvoiceNumber,
		TotalAmount:   total,
	}); err != nil {
		s.errl.Printf("could not record invoice %s: %v", in.InvoiceNumber, err)
	}

	filename := fmt.Sprintf("faktura_%s_%s.pdf", in.InvoiceNumber, sanitize(assignment.Name))
	w.Header().Set("Content-Type", "application/pdf")
	w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename))
	w.Header().Set("Content-Length", strconv.Itoa(len(pdfBytes)))
	w.Write(pdfBytes)
}

func parseExtraLines(r *http.Request) []ExtraLine {
	descs := r.Form["extra_desc"]
	qtys := r.Form["extra_qty"]
	units := r.Form["extra_unit"]
	prices := r.Form["extra_price"]

	var lines []ExtraLine
	for i := range descs {
		desc := strings.TrimSpace(descs[i])
		if desc == "" {
			continue
		}
		qty, unit, price := 0.0, "stk", 0.0
		if i < len(qtys) {
			qty, _ = strconv.ParseFloat(qtys[i], 64)
		}
		if i < len(units) && units[i] != "" {
			unit = units[i]
		}
		if i < len(prices) {
			price, _ = strconv.ParseFloat(prices[i], 64)
		}
		if qty == 0 || price == 0 {
			continue
		}
		lines = append(lines, ExtraLine{Description: desc, Quantity: qty, Unit: unit, UnitPrice: price})
	}
	return lines
}

// ── PDF generation (ported from the desktop app) ─────────────────────────────

// stripTaskNumber removes the "N - " prefix from task descriptions.
// "1 - Frontend work" → "Frontend work"
func stripTaskNumber(desc string) string {
	if idx := strings.Index(desc, " - "); idx >= 0 {
		prefix := desc[:idx]
		allDigits := true
		for _, c := range prefix {
			if c < '0' || c > '9' {
				allDigits = false
				break
			}
		}
		if allDigits {
			return desc[idx+3:]
		}
	}
	return desc
}

// generateInvoicePDF builds the invoice and returns the PDF bytes and the
// grand total (incl. MVG) for record keeping.
func generateInvoicePDF(d invoiceInput) ([]byte, float64, error) {
	pdf := fpdf.New("P", "mm", "A4", "")
	pdf.SetMargins(10, 15, 10)
	pdf.SetAutoPageBreak(true, 30)

	fontFamily := "Helvetica"
	txt := pdf.UnicodeTranslatorFromDescriptor("")

	footer1 := d.FromName
	if d.FromAddress != "" {
		footer1 += " / " + d.FromAddress
	}
	var footer2Parts []string
	if d.FromVAT != "" {
		footer2Parts = append(footer2Parts, "V-tal: "+d.FromVAT)
	}
	if d.FromPhone != "" {
		footer2Parts = append(footer2Parts, "tlf.: "+d.FromPhone)
	}
	if d.FromEmail != "" {
		footer2Parts = append(footer2Parts, "teldupostur: "+d.FromEmail)
	}
	if d.FromWeb != "" {
		footer2Parts = append(footer2Parts, "heimas\u00ed\u00f0a: "+d.FromWeb)
	}
	footer2 := strings.Join(footer2Parts, " / ")

	pdf.SetFooterFunc(func() {
		pdf.SetY(-20)
		pdf.SetTextColor(100, 100, 100)
		pdf.SetFont(fontFamily, "", 8)
		pdf.SetDrawColor(100, 100, 100)
		pdf.SetLineWidth(0.2)
		pdf.Line(10, pdf.GetY(), 200, pdf.GetY())
		pdf.Ln(2)
		pdf.CellFormat(190, 4, txt(footer1), "", 0, "C", false, 0, "")
		pdf.Ln(-1)
		if footer2 != "" {
			pdf.CellFormat(190, 4, txt(footer2), "", 0, "C", false, 0, "")
		}
	})

	pdf.AddPage()

	darkBlue := func() { pdf.SetTextColor(68, 84, 106) }
	black := func() { pdf.SetTextColor(0, 0, 0) }
	setFont := func(style string, size float64) { pdf.SetFont(fontFamily, style, size) }

	// Header
	darkBlue()
	setFont("B", 20)
	pdf.SetXY(10, 20)
	pdf.CellFormat(190, 10, txt(d.FromName), "", 0, "R", false, 0, "")

	// Customer info
	pdf.SetXY(10, 35)
	darkBlue()
	setFont("B", 11)
	pdf.Cell(190, 5, txt(d.Company.Name))
	pdf.Ln(5)

	setFont("", 10)
	darkBlue()
	if d.Company.Address != "" {
		pdf.Cell(190, 5, txt(d.Company.Address))
		pdf.Ln(5)
	}
	if d.Company.ContactEmail != "" {
		pdf.Cell(190, 5, txt(d.Company.ContactEmail))
		pdf.Ln(5)
	}
	if d.Company.ContactPhone != "" {
		pdf.Cell(190, 5, txt(d.Company.ContactPhone))
		pdf.Ln(5)
	}
	pdf.Ln(10)

	// Invoice meta
	yMeta := pdf.GetY()
	black()
	setFont("", 10)
	pdf.CellFormat(12, 5, txt("Dato: "), "", 0, "L", false, 0, "")
	setFont("B", 10)
	pdf.CellFormat(50, 5, d.Date.Format("02/01/2006"), "", 0, "L", false, 0, "")

	pdf.SetXY(140, yMeta)
	setFont("", 10)
	pdf.CellFormat(30, 5, txt("Fakturanr.    "), "", 0, "R", false, 0, "")
	setFont("B", 10)
	pdf.CellFormat(20, 5, txt(d.InvoiceNumber), "", 0, "R", false, 0, "")

	pdf.SetXY(10, yMeta+6)
	if d.Comment != "" {
		setFont("", 10)
		pdf.MultiCell(190, 5, txt(d.Comment), "", "L", false)
	}
	pdf.Ln(10)

	// Title
	darkBlue()
	setFont("B", 16)
	pdf.Cell(190, 8, txt("Faktura"))
	pdf.Ln(10)

	// Table
	colW := []float64{80, 20, 20, 25, 20, 25}

	pdf.SetDrawColor(68, 84, 106)
	pdf.SetLineWidth(0.6)
	pdf.Line(10, pdf.GetY(), 200, pdf.GetY())
	pdf.SetY(pdf.GetY() + 1)

	darkBlue()
	setFont("", 10)
	pdf.CellFormat(colW[0], 7, txt("Lýsing"), "", 0, "L", false, 0, "")
	pdf.CellFormat(colW[1], 7, txt("Nøgd"), "", 0, "R", false, 0, "")
	pdf.CellFormat(colW[2], 7, txt("Eind"), "", 0, "L", false, 0, "")
	pdf.CellFormat(colW[3], 7, txt("Stk. prísur"), "", 0, "R", false, 0, "")
	pdf.CellFormat(colW[4], 7, "", "", 0, "R", false, 0, "")
	pdf.CellFormat(colW[5], 7, txt("Upphædd"), "", 0, "R", false, 0, "")
	pdf.Ln(-1)

	pdf.Line(10, pdf.GetY(), 200, pdf.GetY())
	pdf.SetY(pdf.GetY() + 1)

	// group sessions by task
	type taskLine struct {
		desc   string
		hours  float64
		amount float64
	}
	taskMap := make(map[int64]*taskLine)
	var taskOrder []int64

	for _, ws := range d.Sessions {
		if ws.EndTime == nil {
			continue
		}
		hours := ws.DurationMinutes / 60.0
		amount := hours * d.HourlyRate
		if tl, ok := taskMap[ws.TaskID]; ok {
			tl.hours += hours
			tl.amount += amount
		} else {
			taskMap[ws.TaskID] = &taskLine{desc: ws.TaskDescription, hours: hours, amount: amount}
			taskOrder = append(taskOrder, ws.TaskID)
		}
	}

	setFont("", 10)
	pdf.SetTextColor(50, 50, 50)

	subtotal := 0.0
	for _, tid := range taskOrder {
		tl := taskMap[tid]
		desc := stripTaskNumber(tl.desc)
		if len(desc) > 45 {
			desc = desc[:42] + "..."
		}
		pdf.CellFormat(colW[0], 7, txt(desc), "", 0, "L", false, 0, "")
		pdf.CellFormat(colW[1], 7, fmtNum(tl.hours), "", 0, "R", false, 0, "")
		pdf.CellFormat(colW[2], 7, txt(" tímar"), "", 0, "L", false, 0, "")
		pdf.CellFormat(colW[3], 7, fmtNum(d.HourlyRate), "", 0, "R", false, 0, "")
		pdf.CellFormat(colW[4], 7, "", "", 0, "R", false, 0, "")
		pdf.CellFormat(colW[5], 7, fmtNum(tl.amount), "", 0, "R", false, 0, "")
		pdf.Ln(-1)
		subtotal += tl.amount
	}

	// Extra lines
	for _, el := range d.ExtraLines {
		desc := el.Description
		if len(desc) > 45 {
			desc = desc[:42] + "..."
		}
		amount := el.Quantity * el.UnitPrice
		pdf.CellFormat(colW[0], 7, txt(desc), "", 0, "L", false, 0, "")
		pdf.CellFormat(colW[1], 7, fmtNum(el.Quantity), "", 0, "R", false, 0, "")
		pdf.CellFormat(colW[2], 7, txt(" "+el.Unit), "", 0, "L", false, 0, "")
		pdf.CellFormat(colW[3], 7, fmtNum(el.UnitPrice), "", 0, "R", false, 0, "")
		pdf.CellFormat(colW[4], 7, "", "", 0, "R", false, 0, "")
		pdf.CellFormat(colW[5], 7, fmtNum(amount), "", 0, "R", false, 0, "")
		pdf.Ln(-1)
		subtotal += amount
	}

	pdf.Ln(5)

	// Totals
	offset := colW[0] + colW[1] + colW[2]
	totW := colW[3] + colW[4] + colW[5]
	vat := subtotal * (d.MVGPercent / 100.0)
	total := subtotal + vat

	darkBlue()
	setFont("", 10)
	pdf.SetX(10 + offset)
	pdf.Line(10+offset, pdf.GetY(), 200, pdf.GetY())
	pdf.SetY(pdf.GetY() + 1)

	pdf.SetX(10 + offset)
	pdf.CellFormat(totW-colW[5], 6, txt("Í alt DKK uttan MVG"), "", 0, "L", false, 0, "")
	black()
	pdf.CellFormat(colW[5], 6, fmtNum(subtotal), "", 0, "R", false, 0, "")
	pdf.Ln(-1)

	darkBlue()
	pdf.SetX(10 + offset)
	pdf.CellFormat(totW-colW[5], 6, txt(fmt.Sprintf("%.0f%% MVG", d.MVGPercent)), "", 0, "L", false, 0, "")
	black()
	pdf.CellFormat(colW[5], 6, fmtNum(vat), "", 0, "R", false, 0, "")
	pdf.Ln(-1)

	darkBlue()
	setFont("B", 10)
	pdf.SetX(10 + offset)
	pdf.Line(10+offset, pdf.GetY(), 200, pdf.GetY())
	pdf.SetY(pdf.GetY() + 1)

	pdf.SetX(10 + offset)
	pdf.CellFormat(totW-colW[5], 6, txt("Í alt DKK vid MVG"), "", 0, "L", false, 0, "")
	black()
	pdf.CellFormat(colW[5], 6, fmtNum(total), "", 0, "R", false, 0, "")
	pdf.Ln(-1)

	pdf.SetX(10 + offset)
	pdf.Line(10+offset, pdf.GetY(), 200, pdf.GetY())
	pdf.Ln(20)

	// Payment terms
	darkBlue()
	setFont("", 9)
	pdf.Cell(190, 5, txt(fmt.Sprintf(
		"Gjaldstreytir: Netto %d dagar - Fellir á dato %s",
		d.DueDays, d.DueDate.Format("02/01/2006"),
	)))
	pdf.Ln(8)

	if d.BankInfo != "" {
		pdf.Cell(190, 5, txt("Upphæddin skal setast inn á bankakontu:"))
		pdf.Ln(5)
		for _, line := range strings.Split(d.BankInfo, "\n") {
			pdf.Cell(190, 5, txt(strings.TrimRight(line, "\r")))
			pdf.Ln(5)
		}
	}

	pdf.Cell(190, 5, txt(fmt.Sprintf("Biði verður um Fakturanr. %s við flytingini", d.InvoiceNumber)))
	pdf.Ln(8)
	pdf.Cell(190, 5, txt("Verður ikki goldið innan lokna gjaldsfrest, verður renta tilskriva á 0,70%, umframt eitt gjald uppá 100,00 DKK"))

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

func fmtNum(v float64) string {
	return fmt.Sprintf("%.2f", v)
}

func sanitize(name string) string {
	var b strings.Builder
	for _, c := range name {
		if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' {
			b.WriteRune(c)
		} else if c == ' ' {
			b.WriteByte('_')
		}
	}
	return b.String()
}
