package api

import (
	"archive/zip"
	"bytes"
	"encoding/csv"
	"encoding/json"
	"fmt"
	"net/http"
	"regexp"
	"strings"
	"time"
	"unicode"

	"archivmail/internal/audit"
	"archivmail/internal/userstore"
	"archivmail/pkg/mailparser"
)

// ── Text helpers ──────────────────────────────────────────────────────────

// toLatinSafe converts German umlauts to ASCII digraphs and drops non-ASCII
// characters. Required for PDF Latin-1 encoding.
func toLatinSafe(s string) string {
	replacer := strings.NewReplacer(
		"ä", "ae", "ö", "oe", "ü", "ue",
		"Ä", "Ae", "Ö", "Oe", "Ü", "Ue",
		"ß", "ss",
	)
	s = replacer.Replace(s)
	var b strings.Builder
	for _, r := range s {
		if r <= unicode.MaxASCII {
			b.WriteRune(r)
		}
	}
	return b.String()
}

var htmlTagRe = regexp.MustCompile(`<[^>]+>`)
var wsRe = regexp.MustCompile(`\s+`)

// stripHTML removes HTML tags and collapses whitespace.
func stripHTML(html string) string {
	s := htmlTagRe.ReplaceAllString(html, " ")
	s = wsRe.ReplaceAllString(s, " ")
	return strings.TrimSpace(s)
}

// formatBytesStr returns a human-readable file size string.
func formatBytesStr(n int) string {
	switch {
	case n >= 1024*1024:
		return fmt.Sprintf("%.1f MB", float64(n)/(1024*1024))
	case n >= 1024:
		return fmt.Sprintf("%.1f KB", float64(n)/1024)
	default:
		return fmt.Sprintf("%d B", n)
	}
}

// ── Minimal PDF writer ────────────────────────────────────────────────────
//
// Generates a simple but valid PDF/1.4 document using only stdlib.
// Uses the standard 14 core fonts (Helvetica) which every viewer supports.

type pdfWriter struct {
	buf     bytes.Buffer
	offsets []int // byte offsets of each object
}

func newPDFWriter() *pdfWriter { return &pdfWriter{} }

func (p *pdfWriter) write(s string) { p.buf.WriteString(s) }
func (p *pdfWriter) writef(f string, args ...interface{}) {
	p.buf.WriteString(fmt.Sprintf(f, args...))
}

// startObj records the current byte offset and writes the object header.
func (p *pdfWriter) startObj(n int) {
	// Grow offsets slice if needed
	for len(p.offsets) < n {
		p.offsets = append(p.offsets, 0)
	}
	p.offsets[n-1] = p.buf.Len()
	p.writef("%d 0 obj\n", n)
}

func (p *pdfWriter) endObj() { p.write("endobj\n") }

// escapePDF escapes special characters in PDF string literals.
func escapePDF(s string) string {
	s = strings.ReplaceAll(s, `\`, `\\`)
	s = strings.ReplaceAll(s, `(`, `\(`)
	s = strings.ReplaceAll(s, `)`, `\)`)
	s = strings.ReplaceAll(s, "\r", `\r`)
	s = strings.ReplaceAll(s, "\n", `\n`)
	return s
}

// wrapText wraps text at maxChars width, returning lines.
func wrapText(text string, maxChars int) []string {
	if maxChars <= 0 {
		maxChars = 80
	}
	var lines []string
	for _, para := range strings.Split(text, "\n") {
		words := strings.Fields(para)
		if len(words) == 0 {
			lines = append(lines, "")
			continue
		}
		var cur strings.Builder
		for _, w := range words {
			if cur.Len() == 0 {
				cur.WriteString(w)
			} else if cur.Len()+1+len(w) <= maxChars {
				cur.WriteByte(' ')
				cur.WriteString(w)
			} else {
				lines = append(lines, cur.String())
				cur.Reset()
				cur.WriteString(w)
			}
		}
		if cur.Len() > 0 {
			lines = append(lines, cur.String())
		}
	}
	return lines
}

// buildMailPDF produces the raw bytes of a PDF representing the given mail.
func buildMailPDF(id string, pm *mailparser.ParsedMail, rawSize int) []byte {
	p := newPDFWriter()

	// Page dimensions: A4 in points (72 pt/inch)
	// 210mm x 297mm => 595.28 x 841.89 pt
	pageW := 595.28
	pageH := 841.89
	margin := 50.0
	lineH := 14.0
	smallLineH := 12.0
	contentW := pageW - 2*margin

	// Prepare body content
	body := pm.TextBody
	if body == "" && pm.HTMLBody != "" {
		body = stripHTML(pm.HTMLBody)
	}
	body = toLatinSafe(body)
	bodyLines := wrapText(body, 85)

	// Prepare header lines
	var dateStr string
	if !pm.Date.IsZero() {
		dateStr = pm.Date.Format("02.01.2006 15:04:05")
	}

	type hline struct{ label, value string }
	headers := []hline{
		{"Von:", toLatinSafe(pm.From)},
		{"An:", toLatinSafe(strings.Join(pm.To, ", "))},
		{"CC:", toLatinSafe(strings.Join(pm.CC, ", "))},
		{"Betreff:", toLatinSafe(pm.Subject)},
		{"Datum:", dateStr},
		{"Groesse:", formatBytesStr(rawSize)},
	}

	// Attachment lines
	var attLines []string
	for _, a := range pm.Attachments {
		attLines = append(attLines, toLatinSafe(fmt.Sprintf(
			"  - %s  [%s, %s]", a.Filename, a.ContentType, formatBytesStr(a.Size),
		)))
	}

	// Build page content stream
	var cs bytes.Buffer
	y := pageH - margin

	// Helper: move to next line
	newline := func(h float64) { y -= h }

	// BT block builder helper — writes text at absolute position
	text := func(x, ypos float64, size float64, bold bool, s string) {
		font := "Helvetica"
		if bold {
			font = "Helvetica-Bold"
		}
		cs.WriteString(fmt.Sprintf("BT /%s %.1f Tf %.1f %.1f Td (%s) Tj ET\n",
			font, size, x, ypos, escapePDF(s)))
	}

	// Horizontal line
	hline2 := func(ypos float64) {
		cs.WriteString(fmt.Sprintf("%.1f %.1f m %.1f %.1f l S\n",
			margin, ypos, pageW-margin, ypos))
	}

	// Title
	text(margin, y, 16, true, "archivmail Export")
	newline(lineH * 1.8)

	// Header table
	labelX := margin
	valueX := margin + 70
	for _, h := range headers {
		text(labelX, y, 10, true, h.label)
		// Wrap value if long
		valLines := wrapText(h.value, 70)
		for i, vl := range valLines {
			text(valueX, y, 10, false, vl)
			if i < len(valLines)-1 {
				newline(smallLineH)
			}
		}
		newline(smallLineH)
	}

	newline(4)
	// Separator
	hline2(y)
	newline(lineH)

	// Body
	if len(bodyLines) > 0 {
		for _, bl := range bodyLines {
			if y < margin+20 {
				// Simple overflow protection: stop rendering
				text(margin, y, 8, false, "[... Text abgeschnitten ...]")
				y = margin
				break
			}
			text(margin, y, 9, false, bl)
			newline(smallLineH)
		}
	} else {
		text(margin, y, 9, false, "(kein Textinhalt)")
		newline(smallLineH)
	}

	// Attachments section
	if len(attLines) > 0 {
		newline(6)
		if y > margin+20 {
			hline2(y)
			newline(lineH)
			text(margin, y, 10, true, fmt.Sprintf("Anhaenge (%d):", len(pm.Attachments)))
			newline(lineH)
			for _, al := range attLines {
				if y < margin+20 {
					break
				}
				text(margin, y, 9, false, al)
				newline(smallLineH)
			}
		}
	}

	contentStream := cs.String()

	// ── PDF objects ───────────────────────────────────────────────────────
	// Obj 1: Catalog
	// Obj 2: Pages
	// Obj 3: Page
	// Obj 4: Content stream

	p.write("%PDF-1.4\n")

	// Obj 1: Catalog
	p.startObj(1)
	p.write("<< /Type /Catalog /Pages 2 0 R >>\n")
	p.endObj()

	// Obj 2: Pages
	p.startObj(2)
	p.writef("<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 %.2f %.2f] >>\n", pageW, pageH)
	p.endObj()

	// Obj 3: Page
	p.startObj(3)
	p.writef("<< /Type /Page /Parent 2 0 R\n")
	p.writef("   /Resources << /Font << /Helvetica << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\n")
	p.writef("                            /Helvetica-Bold << /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold >> >> >>\n")
	p.writef("   /Contents 4 0 R\n")
	p.writef("   /MediaBox [0 0 %.2f %.2f]\n", pageW, pageH)
	p.write(">>\n")
	p.endObj()

	// Obj 4: Content stream
	p.startObj(4)
	// Set line width for horizontal rules
	streamWithSetup := "0.5 w\n" + contentStream
	streamBytes := []byte(streamWithSetup)
	p.writef("<< /Length %d >>\n", len(streamBytes))
	p.write("stream\n")
	p.buf.Write(streamBytes)
	p.write("\nendstream\n")
	p.endObj()

	// Cross-reference table
	xrefOffset := p.buf.Len()
	p.writef("xref\n0 5\n")
	p.writef("0000000000 65535 f \n")
	for i := 0; i < 4; i++ {
		off := 0
		if i < len(p.offsets) {
			off = p.offsets[i]
		}
		p.writef("%010d 00000 n \n", off)
	}

	// Ensure contentW is used (suppress unused variable warning)
	_ = contentW

	p.write("trailer\n")
	p.writef("<< /Size 5 /Root 1 0 R >>\n")
	p.write("startxref\n")
	p.writef("%d\n", xrefOffset)
	p.write("%%EOF\n")

	return p.buf.Bytes()
}

// ── PDF handler ───────────────────────────────────────────────────────────

func (s *Server) handleExportPDF(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	// SEC-22: Validate mail ID format to prevent path traversal.
	if !isValidMailID(id) {
		writeError(w, http.StatusBadRequest, "invalid mail id")
		return
	}

	raw, err := s.store.Load(id)
	if err != nil {
		writeError(w, http.StatusNotFound, "mail not found")
		return
	}

	pm, err := mailparser.Parse(raw)
	if err != nil {
		writeError(w, http.StatusInternalServerError, "failed to parse mail")
		return
	}

	sess := sessionFromCtx(r.Context())

	// SEC-05: Tenant isolation for PDF export.
	if sess.TenantID != nil {
		mailTenant, _ := s.store.GetTenantForMail(r.Context(), id)
		if mailTenant == nil || *mailTenant != *sess.TenantID {
			writeError(w, http.StatusForbidden, "access denied")
			return
		}
	}

	// Global auditor (no tenant_id): only mails with no tenant assignment.
	// A tenant-scoped auditor is already constrained by the tenant-isolation
	// block above (sess.TenantID != nil) — PROJ-55.
	if auditorIsGlobal(sess) {
		ok, err := s.store.IsWithoutTenant(r.Context(), id)
		if err != nil || !ok {
			writeError(w, http.StatusForbidden, "access denied")
			return
		}
	}

	// user: only own mails; domain_auditor: all tenant mails (no filter)
	if sess.Role == userstore.RoleUser {
		u, err := s.users.GetByUsername(sess.Username)
		if err != nil || !mailBelongsToUser(pm, u.Email) {
			writeError(w, http.StatusForbidden, "access denied")
			return
		}
	}

	safeID := id
	if len(safeID) > 16 {
		safeID = safeID[:16]
	}
	filename := safeID + ".pdf"

	pdfBytes := buildMailPDF(id, pm, len(raw))

	w.Header().Set("Content-Type", "application/pdf")
	w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
	w.Header().Set("Content-Length", fmt.Sprintf("%d", len(pdfBytes)))
	w.WriteHeader(http.StatusOK)
	w.Write(pdfBytes) //nolint:errcheck

	s.audlog.Log(audit.Entry{
		EventType: audit.EventExport,
		Username:  sess.Username,
		TenantID:  sess.TenantID,
		IPAddress: s.remoteIP(r),
		MailID:    id,
		Detail:    "pdf",
		Success:   true,
	})
}

// ── ZIP export ────────────────────────────────────────────────────────────

func (s *Server) handleExportZIP(w http.ResponseWriter, r *http.Request) {
	var req struct {
		IDs []string `json:"ids"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeError(w, http.StatusBadRequest, "invalid request body")
		return
	}
	if len(req.IDs) == 0 {
		writeError(w, http.StatusBadRequest, "ids must not be empty")
		return
	}
	if len(req.IDs) > 500 {
		writeError(w, http.StatusBadRequest, "too many ids (max 500)")
		return
	}

	// SEC-22: Validate all mail IDs before processing.
	for _, id := range req.IDs {
		if !isValidMailID(id) {
			writeError(w, http.StatusBadRequest, "invalid mail id")
			return
		}
	}

	sess := sessionFromCtx(r.Context())

	// SEC-05: Tenant isolation — verify all requested IDs belong to caller's tenant.
	if sess.TenantID != nil {
		allowedIDs, err := s.store.GetAllIDsByTenant(r.Context(), sess.TenantID)
		if err != nil {
			writeError(w, http.StatusInternalServerError, "tenant check failed")
			return
		}
		allowed := make(map[string]struct{}, len(allowedIDs))
		for _, aid := range allowedIDs {
			allowed[aid] = struct{}{}
		}
		for _, id := range req.IDs {
			if _, ok := allowed[id]; !ok {
				writeError(w, http.StatusForbidden, "access denied")
				return
			}
		}
	}

	// Global auditor (no tenant_id): pre-load the set of no-tenant mail IDs for
	// efficient per-mail checks. A tenant-scoped auditor is already constrained
	// by the tenant-isolation block above (PROJ-55).
	var auditorAllowed map[string]struct{}
	if auditorIsGlobal(sess) {
		ids, err := s.store.GetAllIDsWithoutTenant(r.Context())
		if err != nil {
			writeError(w, http.StatusInternalServerError, "tenant check failed")
			return
		}
		auditorAllowed = make(map[string]struct{}, len(ids))
		for _, aid := range ids {
			auditorAllowed[aid] = struct{}{}
		}
	}

	// User: look up email once for per-mail access checks.
	var userEmail string
	if sess.Role == userstore.RoleUser {
		u, err := s.users.GetByUsername(sess.Username)
		if err != nil {
			writeError(w, http.StatusInternalServerError, "user lookup failed")
			return
		}
		userEmail = u.Email
	}

	w.Header().Set("Content-Type", "application/zip")
	w.Header().Set("Content-Disposition", `attachment; filename="archivmail-export.zip"`)

	zw := zip.NewWriter(w)
	defer zw.Close()

	type manifestRow struct {
		filename  string
		messageID string
		from      string
		to        string
		subject   string
		date      string
	}
	var manifest []manifestRow
	exported := 0

	for _, id := range req.IDs {
		raw, err := s.store.Load(id)
		if err != nil {
			continue
		}

		pm, err := mailparser.Parse(raw)
		if err != nil {
			continue
		}

		// auditor: only mails with no tenant assignment.
		if auditorAllowed != nil {
			if _, ok := auditorAllowed[id]; !ok {
				continue
			}
		}
		// user: only own mails; domain_auditor: all tenant mails (no filter)
		if sess.Role == userstore.RoleUser && !mailBelongsToUser(pm, userEmail) {
			continue
		}

		safeID := id
		if len(safeID) > 16 {
			safeID = safeID[:16]
		}
		emlFilename := safeID + ".eml"

		fw, err := zw.Create(emlFilename)
		if err != nil {
			continue
		}
		if _, err := fw.Write(raw); err != nil {
			continue
		}

		var dateStr string
		if !pm.Date.IsZero() {
			dateStr = pm.Date.UTC().Format(time.RFC3339)
		}
		manifest = append(manifest, manifestRow{
			filename:  emlFilename,
			messageID: pm.MessageID,
			from:      pm.From,
			to:        strings.Join(pm.To, ", "),
			subject:   pm.Subject,
			date:      dateStr,
		})
		exported++
	}

	// Write manifest.csv
	mf, err := zw.Create("manifest.csv")
	if err == nil {
		cw := csv.NewWriter(mf)
		cw.Write([]string{"filename", "message_id", "from", "to", "subject", "date"}) //nolint:errcheck
		for _, row := range manifest {
			cw.Write([]string{ //nolint:errcheck
				row.filename,
				row.messageID,
				row.from,
				row.to,
				row.subject,
				row.date,
			})
		}
		cw.Flush()
	}

	s.audlog.Log(audit.Entry{
		EventType: audit.EventExport,
		Username:  sess.Username,
		TenantID:  sess.TenantID,
		IPAddress: s.remoteIP(r),
		Detail:    fmt.Sprintf("zip: %d mails", exported),
		Success:   true,
	})
}
