package main

import (
	"archive/zip"
	"crypto/rand"
	"embed"
	"encoding/binary"
	"encoding/hex"
	"encoding/json"
	"flag"
	"fmt"
	"html/template"
	"image"
	"image/color"
	"image/draw"
	"image/png"
	"io"
	"log"
	"math"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
)

// --- Configuration ---

type Config struct {
	StencilHeight float64
	WallHeight    float64
	WallThickness float64
	LineWidth     float64
	DPI           float64
	KeepPNG       bool
}

// Default values
const (
	DefaultStencilHeight = 0.16
	DefaultWallHeight    = 2.0
	DefaultWallThickness = 1.0
	DefaultDPI           = 1000.0
)

// --- STL Helpers ---

type Point struct {
	X, Y, Z float64
}

func WriteSTL(filename string, triangles [][3]Point) error {
	f, err := os.Create(filename)
	if err != nil {
		return err
	}
	defer f.Close()

	// Write Binary STL Header (80 bytes)
	header := make([]byte, 80)
	copy(header, "Generated by pcb-to-stencil")
	if _, err := f.Write(header); err != nil {
		return err
	}

	// Write Number of Triangles (4 bytes uint32)
	count := uint32(len(triangles))
	if err := binary.Write(f, binary.LittleEndian, count); err != nil {
		return err
	}

	// Write Triangles
	// Each triangle is 50 bytes:
	// Normal (3 floats = 12 bytes)
	// Vertex 1 (3 floats = 12 bytes)
	// Vertex 2 (3 floats = 12 bytes)
	// Vertex 3 (3 floats = 12 bytes)
	// Attribute byte count (2 bytes uint16)

	// Buffer for a single triangle to minimize syscalls
	buf := make([]byte, 50)

	for _, t := range triangles {
		// Normal (0,0,0)
		binary.LittleEndian.PutUint32(buf[0:4], math.Float32bits(0))
		binary.LittleEndian.PutUint32(buf[4:8], math.Float32bits(0))
		binary.LittleEndian.PutUint32(buf[8:12], math.Float32bits(0))

		// Vertex 1
		binary.LittleEndian.PutUint32(buf[12:16], math.Float32bits(float32(t[0].X)))
		binary.LittleEndian.PutUint32(buf[16:20], math.Float32bits(float32(t[0].Y)))
		binary.LittleEndian.PutUint32(buf[20:24], math.Float32bits(float32(t[0].Z)))

		// Vertex 2
		binary.LittleEndian.PutUint32(buf[24:28], math.Float32bits(float32(t[1].X)))
		binary.LittleEndian.PutUint32(buf[28:32], math.Float32bits(float32(t[1].Y)))
		binary.LittleEndian.PutUint32(buf[32:36], math.Float32bits(float32(t[1].Z)))

		// Vertex 3
		binary.LittleEndian.PutUint32(buf[36:40], math.Float32bits(float32(t[2].X)))
		binary.LittleEndian.PutUint32(buf[40:44], math.Float32bits(float32(t[2].Y)))
		binary.LittleEndian.PutUint32(buf[44:48], math.Float32bits(float32(t[2].Z)))

		// Attribute byte count (0)
		binary.LittleEndian.PutUint16(buf[48:50], 0)

		if _, err := f.Write(buf); err != nil {
			return err
		}
	}
	return nil
}

func AddBox(triangles *[][3]Point, x, y, w, h, zHeight float64) {
	x0, y0 := x, y
	x1, y1 := x+w, y+h
	z0, z1 := 0.0, zHeight

	p000 := Point{x0, y0, z0}
	p100 := Point{x1, y0, z0}
	p110 := Point{x1, y1, z0}
	p010 := Point{x0, y1, z0}
	p001 := Point{x0, y0, z1}
	p101 := Point{x1, y0, z1}
	p111 := Point{x1, y1, z1}
	p011 := Point{x0, y1, z1}

	addQuad := func(a, b, c, d Point) {
		*triangles = append(*triangles, [3]Point{a, b, c})
		*triangles = append(*triangles, [3]Point{c, d, a})
	}

	addQuad(p000, p010, p110, p100) // Bottom
	addQuad(p101, p111, p011, p001) // Top
	addQuad(p000, p100, p101, p001) // Front
	addQuad(p100, p110, p111, p101) // Right
	addQuad(p110, p010, p011, p111) // Back
	addQuad(p010, p000, p001, p011) // Left
}

// --- Meshing Logic ---

// ComputeWallMask generates a mask for the wall based on the outline image.
// It identifies the board area (inside the outline) and creates a wall of
// specified thickness around it.
func ComputeWallMask(img image.Image, thicknessMM float64, pixelToMM float64) ([]int, []bool) {
	bounds := img.Bounds()
	w := bounds.Max.X
	h := bounds.Max.Y
	size := w * h

	// Helper for neighbors
	dx := []int{0, 0, 1, -1}
	dy := []int{1, -1, 0, 0}

	// Identify Outline Pixels (White)
	isOutline := make([]bool, size)
	outlineQueue := []int{}
	for i := 0; i < size; i++ {
		cx := i % w
		cy := i / w
		c := img.At(cx, cy)
		r, _, _, _ := c.RGBA()
		if r > 10000 { // White-ish
			isOutline[i] = true
			outlineQueue = append(outlineQueue, i)
		}
	}

	// Dilate Outline to close gaps
	// Dilates the outline by 0.5mm to ensure a closed boundary.
	gapClosingMM := 0.5
	gapClosingPixels := int(gapClosingMM / pixelToMM)
	if gapClosingPixels < 1 {
		gapClosingPixels = 1
	}

	dist := make([]int, size)
	for i := 0; i < size; i++ {
		if isOutline[i] {
			dist[i] = 0
		} else {
			dist[i] = -1
		}
	}

	// BFS for Dilation
	dilatedOutline := make([]bool, size)
	copy(dilatedOutline, isOutline)

	// Use a separate queue for dilation to preserve the original outlineQueue.
	dQueue := make([]int, len(outlineQueue))
	copy(dQueue, outlineQueue)

	for len(dQueue) > 0 {
		idx := dQueue[0]
		dQueue = dQueue[1:]

		d := dist[idx]
		if d >= gapClosingPixels {
			continue
		}

		cx := idx % w
		cy := idx / w

		for i := 0; i < 4; i++ {
			nx, ny := cx+dx[i], cy+dy[i]
			if nx >= 0 && nx < w && ny >= 0 && ny < h {
				nIdx := ny*w + nx
				if dist[nIdx] == -1 {
					dist[nIdx] = d + 1
					dilatedOutline[nIdx] = true
					dQueue = append(dQueue, nIdx)
				}
			}
		}
	}

	// Flood Fill "Outside" area using Dilated Outline as barrier
	isOutside := make([]bool, size)
	// Start from (0,0) - assumed to be outside due to padding
	if !dilatedOutline[0] {
		isOutside[0] = true
		fQueue := []int{0}

		for len(fQueue) > 0 {
			idx := fQueue[0]
			fQueue = fQueue[1:]

			cx := idx % w
			cy := idx / w

			for i := 0; i < 4; i++ {
				nx, ny := cx+dx[i], cy+dy[i]
				if nx >= 0 && nx < w && ny >= 0 && ny < h {
					nIdx := ny*w + nx
					if !isOutside[nIdx] && !dilatedOutline[nIdx] {
						isOutside[nIdx] = true
						fQueue = append(fQueue, nIdx)
					}
				}
			}
		}
	}

	// Restore Board Shape by eroding expansion back to original boundary
	// Resets distance for expansion to touch the real board edge.
	// Board area is then identified as the inverse of the outside area.

	// Reset dist for Outside expansion
	for i := 0; i < size; i++ {
		if isOutside[i] {
			dist[i] = 0
		} else {
			dist[i] = -1
		}
	}

	oQueue := []int{}
	for i := 0; i < size; i++ {
		if isOutside[i] {
			oQueue = append(oQueue, i)
		}
	}

	isOutsideExpanded := make([]bool, size)
	copy(isOutsideExpanded, isOutside)

	for len(oQueue) > 0 {
		idx := oQueue[0]
		oQueue = oQueue[1:]

		d := dist[idx]
		if d >= gapClosingPixels {
			continue
		}

		cx := idx % w
		cy := idx / w

		for i := 0; i < 4; i++ {
			nx, ny := cx+dx[i], cy+dy[i]
			if nx >= 0 && nx < w && ny >= 0 && ny < h {
				nIdx := ny*w + nx
				if dist[nIdx] == -1 {
					dist[nIdx] = d + 1
					isOutsideExpanded[nIdx] = true
					oQueue = append(oQueue, nIdx)
				}
			}
		}
	}

	// 5. Define Board
	isBoard := make([]bool, size)
	for i := 0; i < size; i++ {
		isBoard[i] = !isOutsideExpanded[i]
	}

	// Generate Wall geometry by expanding the Board area outwards.
	// The wall is positioned strictly outside the board boundary.

	thicknessPixels := int(thicknessMM / pixelToMM)
	if thicknessPixels < 1 {
		thicknessPixels = 1
	}

	// Reset dist for Wall generation
	for i := 0; i < size; i++ {
		if isBoard[i] {
			dist[i] = 0
		} else {
			dist[i] = -1
		}
	}

	wQueue := []int{}
	for i := 0; i < size; i++ {
		if isBoard[i] {
			wQueue = append(wQueue, i)
		}
	}

	wallDist := make([]int, size)
	for i := range wallDist {
		wallDist[i] = -1
	}

	for len(wQueue) > 0 {
		idx := wQueue[0]
		wQueue = wQueue[1:]

		d := dist[idx]
		if d >= thicknessPixels {
			continue
		}

		cx := idx % w
		cy := idx / w

		for i := 0; i < 4; i++ {
			nx, ny := cx+dx[i], cy+dy[i]
			if nx >= 0 && nx < w && ny >= 0 && ny < h {
				nIdx := ny*w + nx
				if dist[nIdx] == -1 {
					dist[nIdx] = d + 1
					wallDist[nIdx] = d + 1
					wQueue = append(wQueue, nIdx)
				}
			}
		}
	}

	return wallDist, isBoard
}

func GenerateMeshFromImages(stencilImg, outlineImg image.Image, cfg Config) [][3]Point {
	pixelToMM := 25.4 / cfg.DPI
	bounds := stencilImg.Bounds()
	width := bounds.Max.X
	height := bounds.Max.Y
	var triangles [][3]Point

	var wallDist []int
	var boardMask []bool
	if outlineImg != nil {
		fmt.Println("Computing wall mask...")
		wallDist, boardMask = ComputeWallMask(outlineImg, cfg.WallThickness, pixelToMM)
	}

	// Optimization: Run-Length Encoding
	for y := 0; y < height; y++ {
		var startX = -1
		var currentHeight = 0.0

		for x := 0; x < width; x++ {
			// Check stencil (black = solid)
			sc := stencilImg.At(x, y)
			sr, sg, sb, _ := sc.RGBA()
			isStencilSolid := sr < 10000 && sg < 10000 && sb < 10000

			// Check wall
			isWall := false
			isInsideBoard := true
			if wallDist != nil {
				idx := y*width + x
				isWall = wallDist[idx] >= 0
				if boardMask != nil {
					isInsideBoard = boardMask[idx]
				}
			}

			// Determine height at this pixel
			h := 0.0
			if isWall {
				h = cfg.WallHeight
			} else if isStencilSolid {
				if isInsideBoard {
					h = cfg.WallHeight
				}
			}

			if h > 0 {
				if startX == -1 {
					startX = x
					currentHeight = h
				} else if h != currentHeight {
					// Height changed, end current strip and start new one
					stripLen := x - startX
					AddBox(
						&triangles,
						float64(startX)*pixelToMM,
						float64(y)*pixelToMM,
						float64(stripLen)*pixelToMM,
						pixelToMM,
						currentHeight,
					)
					startX = x
					currentHeight = h
				}
			} else {
				if startX != -1 {
					// End of strip, generate box
					stripLen := x - startX
					AddBox(
						&triangles,
						float64(startX)*pixelToMM,
						float64(y)*pixelToMM,
						float64(stripLen)*pixelToMM,
						pixelToMM,
						currentHeight,
					)
					startX = -1
					currentHeight = 0.0
				}
			}
		}
		if startX != -1 {
			stripLen := width - startX
			AddBox(
				&triangles,
				float64(startX)*pixelToMM,
				float64(y)*pixelToMM,
				float64(stripLen)*pixelToMM,
				pixelToMM,
				currentHeight,
			)
		}
	}
	return triangles
}

// --- Logic ---

func processPCB(gerberPath, outlinePath string, cfg Config, exports []string) ([]string, error) {
	baseName := strings.TrimSuffix(gerberPath, filepath.Ext(gerberPath))
	var generatedFiles []string

	// Helper to check what formats were requested
	wantsType := func(t string) bool {
		for _, e := range exports {
			if e == t {
				return true
			}
		}
		return false
	}

	// Always default to STL if nothing is specified
	if len(exports) == 0 {
		exports = []string{"stl"}
	}

	// Parse Gerber layers for board outline and component placement
	fmt.Printf("Parsing %s...\n", gerberPath)
	gf, err := ParseGerber(gerberPath)
	if err != nil {
		return nil, fmt.Errorf("error parsing gerber: %v", err)
	}

	var outlineGf *GerberFile
	if outlinePath != "" {
		fmt.Printf("Parsing outline %s...\n", outlinePath)
		outlineGf, err = ParseGerber(outlinePath)
		if err != nil {
			return nil, fmt.Errorf("error parsing outline gerber: %v", err)
		}
	}

	// Calculate combined hardware bounds across all layers
	bounds := gf.CalculateBounds()
	if outlineGf != nil {
		outlineBounds := outlineGf.CalculateBounds()
		if outlineBounds.MinX < bounds.MinX {
			bounds.MinX = outlineBounds.MinX
		}
		if outlineBounds.MinY < bounds.MinY {
			bounds.MinY = outlineBounds.MinY
		}
		if outlineBounds.MaxX > bounds.MaxX {
			bounds.MaxX = outlineBounds.MaxX
		}
		if outlineBounds.MaxY > bounds.MaxY {
			bounds.MaxY = outlineBounds.MaxY
		}
	}

	// Expand bounds to accommodate wall thickness and prevent clipping
	margin := cfg.WallThickness + 5.0 // mm
	bounds.MinX -= margin
	bounds.MinY -= margin
	bounds.MaxX += margin
	bounds.MaxY += margin

	// Render vector data to raster images for processing and meshing
	fmt.Println("Rendering to internal image...")
	img := gf.Render(cfg.DPI, &bounds)

	var outlineImg image.Image
	if outlineGf != nil {
		fmt.Println("Rendering outline to internal image...")
		outlineImg = outlineGf.Render(cfg.DPI, &bounds)
	}

	if cfg.KeepPNG {
		pngPath := strings.TrimSuffix(gerberPath, filepath.Ext(gerberPath)) + ".png"
		fmt.Printf("Saving intermediate PNG to %s...\n", pngPath)
		f, err := os.Create(pngPath)
		if err != nil {
			log.Printf("Warning: Could not create PNG file: %v", err)
		} else {
			if err := png.Encode(f, img); err != nil {
				log.Printf("Warning: Could not encode PNG: %v", err)
			}
			f.Close()
		}
	}

	var triangles [][3]Point
	if wantsType("stl") {
		// Generate triangle mesh from rasterized board data (only needed for STL output)
		fmt.Println("Generating mesh...")
		triangles = GenerateMeshFromImages(img, outlineImg, cfg)
	}

	// Export assets in requested file formats
	if wantsType("stl") {
		outputFilename := baseName + ".stl"
		fmt.Printf("Saving to %s (%d triangles)...\n", outputFilename, len(triangles))
		if err := WriteSTL(outputFilename, triangles); err != nil {
			return nil, fmt.Errorf("error writing stl: %v", err)
		}
		generatedFiles = append(generatedFiles, outputFilename)
	}

	if wantsType("svg") {
		outputFilename := baseName + ".svg"
		fmt.Printf("Saving to %s (SVG)...\n", outputFilename)
		if err := WriteSVG(outputFilename, gf, &bounds); err != nil {
			return nil, fmt.Errorf("error writing svg: %v", err)
		}
		generatedFiles = append(generatedFiles, outputFilename)
	}

	if wantsType("png") {
		outputFilename := baseName + ".png"
		fmt.Printf("Saving to %s (PNG)...\n", outputFilename)
		if f, err := os.Create(outputFilename); err == nil {
			png.Encode(f, img)
			f.Close()
			generatedFiles = append(generatedFiles, outputFilename)
		}
	}

	if wantsType("scad") {
		outputFilename := baseName + ".scad"
		fmt.Printf("Saving to %s (Native SCAD)...\n", outputFilename)
		if err := WriteStencilSCAD(outputFilename, gf, outlineGf, cfg, &bounds); err != nil {
			return nil, fmt.Errorf("error writing scad: %v", err)
		}
		generatedFiles = append(generatedFiles, outputFilename)
	}

	return generatedFiles, nil
}

// --- CLI ---

func runCLI(cfg Config, args []string) {
	if len(args) < 1 {
		fmt.Println("Usage: go run main.go [options] <path_to_gerber_file> [path_to_outline_gerber_file]")
		fmt.Println("Options:")
		flag.PrintDefaults()
		fmt.Println("Example: go run main.go -height=0.3 MyPCB.GTP MyPCB.GKO")
		os.Exit(1)
	}

	gerberPath := args[0]
	var outlinePath string
	if len(args) > 1 {
		outlinePath = args[1]
	}

	_, err := processPCB(gerberPath, outlinePath, cfg, []string{"stl"})
	if err != nil {
		log.Fatalf("Error: %v", err)
	}
	fmt.Println("Success! Happy printing.")
}

// --- Server ---

//go:embed static/*
var staticFiles embed.FS

func randomID() string {
	b := make([]byte, 16)
	rand.Read(b)
	return hex.EncodeToString(b)
}

func indexHandler(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != "/" {
		http.NotFound(w, r)
		return
	}
	// Read index.html from embedded FS
	content, err := staticFiles.ReadFile("static/index.html")
	if err != nil {
		http.Error(w, "Could not load index page", http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "text/html")
	w.Write(content)
}

func uploadHandler(w http.ResponseWriter, r *http.Request) {
	// Multipart form parsing is required to correctly extract numeric fields
	// and layer parameters before they fall back to default values.
	r.ParseMultipartForm(32 << 20)

	if r.Method != "POST" {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	// Create temp dir
	tempDir := filepath.Join(".", "temp")
	os.MkdirAll(tempDir, 0755)

	uuid := randomID()

	// Parse params
	height, _ := strconv.ParseFloat(r.FormValue("height"), 64)
	dpi, _ := strconv.ParseFloat(r.FormValue("dpi"), 64)
	wallHeight, _ := strconv.ParseFloat(r.FormValue("wallHeight"), 64)
	wallThickness, _ := strconv.ParseFloat(r.FormValue("wallThickness"), 64)
	lineWidth, _ := strconv.ParseFloat(r.FormValue("lineWidth"), 64)

	if height == 0 {
		height = DefaultStencilHeight
	}
	if dpi == 0 {
		dpi = DefaultDPI
	}
	if wallHeight == 0 {
		wallHeight = DefaultWallHeight
	}
	if wallThickness == 0 {
		wallThickness = DefaultWallThickness
	}

	cfg := Config{
		StencilHeight: height,
		WallHeight:    wallHeight,
		WallThickness: wallThickness,
		LineWidth:     lineWidth,
		DPI:           dpi,
		KeepPNG:       false,
	}

	// Handle Gerber File
	file, header, err := r.FormFile("gerber")
	if err != nil {
		http.Error(w, "Error retrieving gerber file", http.StatusBadRequest)
		return
	}
	defer file.Close()

	gerberPath := filepath.Join(tempDir, uuid+"_paste"+filepath.Ext(header.Filename))
	outFile, err := os.Create(gerberPath)
	if err != nil {
		http.Error(w, "Server error saving file", http.StatusInternalServerError)
		return
	}
	defer outFile.Close()
	io.Copy(outFile, file)

	// Handle Outline File (Optional)
	outlineFile, outlineHeader, err := r.FormFile("outline")
	var outlinePath string
	if err == nil {
		defer outlineFile.Close()
		outlinePath = filepath.Join(tempDir, uuid+"_outline"+filepath.Ext(outlineHeader.Filename))
		outOutline, err := os.Create(outlinePath)
		if err == nil {
			defer outOutline.Close()
			io.Copy(outOutline, outlineFile)
		}
	}

	// Process
	exports := r.Form["exports"]
	generatedPaths, err := processPCB(gerberPath, outlinePath, cfg, exports)
	if err != nil {
		log.Printf("Error processing: %v", err)
		http.Error(w, fmt.Sprintf("Error processing PCB: %v", err), http.StatusInternalServerError)
		return
	}

	var generatedFiles []string
	for _, p := range generatedPaths {
		generatedFiles = append(generatedFiles, filepath.Base(p))
	}

	// Generate Master Zip if more than 1 file
	var zipFile string
	if len(generatedFiles) > 1 {
		zipPath := filepath.Join(tempDir, uuid+"_all_exports.zip")
		zf, err := os.Create(zipPath)
		if err == nil {
			zw := zip.NewWriter(zf)
			for _, fn := range generatedFiles {
				fw, _ := zw.Create(fn)
				fb, _ := os.ReadFile(filepath.Join(tempDir, fn))
				fw.Write(fb)
			}
			zw.Close()
			zf.Close()
			zipFile = filepath.Base(zipPath)
		}
	}

	// Render Success
	renderResult(w, "Your stencil has been generated successfully.", generatedFiles, "/", zipFile)
}

func enclosureUploadHandler(w http.ResponseWriter, r *http.Request) {
	r.ParseMultipartForm(32 << 20)

	if r.Method != "POST" {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	tempDir := filepath.Join(".", "temp")
	os.MkdirAll(tempDir, 0755)
	uuid := randomID()

	// Parse params
	wallThickness, _ := strconv.ParseFloat(r.FormValue("wallThickness"), 64)
	wallHeight, _ := strconv.ParseFloat(r.FormValue("wallHeight"), 64)
	clearance, _ := strconv.ParseFloat(r.FormValue("clearance"), 64)
	dpi, _ := strconv.ParseFloat(r.FormValue("dpi"), 64)

	if wallThickness == 0 {
		wallThickness = DefaultEncWallThick
	}
	if wallHeight == 0 {
		wallHeight = DefaultEncWallHeight
	}
	if clearance == 0 {
		clearance = DefaultClearance
	}
	if dpi == 0 {
		dpi = 600
	}

	// Handle GerberJob file (required)
	gbrjobFile, gbrjobHeader, err := r.FormFile("gbrjob")
	if err != nil {
		http.Error(w, "Gerber job file (.gbrjob) is required", http.StatusBadRequest)
		return
	}
	defer gbrjobFile.Close()

	gbrjobPath := filepath.Join(tempDir, uuid+"_"+gbrjobHeader.Filename)
	jf, err := os.Create(gbrjobPath)
	if err != nil {
		http.Error(w, "Server error saving file", http.StatusInternalServerError)
		return
	}
	io.Copy(jf, gbrjobFile)
	jf.Close()

	jobResult, err := ParseGerberJob(gbrjobPath)
	if err != nil {
		http.Error(w, fmt.Sprintf("Error parsing gbrjob: %v", err), http.StatusBadRequest)
		return
	}

	// Auto-fill PCB thickness from job file
	pcbThickness := jobResult.BoardThickness
	if pcbThickness == 0 {
		pcbThickness = DefaultPCBThickness
	}

	ecfg := EnclosureConfig{
		PCBThickness:  pcbThickness,
		WallThickness: wallThickness,
		WallHeight:    wallHeight,
		Clearance:     clearance,
		DPI:           dpi,
	}

	// Store uploaded Gerber files and coordinate with the job description
	gerberFiles := r.MultipartForm.File["gerbers"]
	savedGerbers := make(map[string]string) // filename → saved path
	for _, fh := range gerberFiles {
		f, err := fh.Open()
		if err != nil {
			continue
		}
		savePath := filepath.Join(tempDir, uuid+"_"+fh.Filename)
		sf, err := os.Create(savePath)
		if err != nil {
			f.Close()
			continue
		}
		io.Copy(sf, f)
		sf.Close()
		f.Close()
		savedGerbers[fh.Filename] = savePath
	}

	// Find the outline (Edge.Cuts) gerber
	outlinePath, ok := savedGerbers[jobResult.EdgeCutsFile]
	if !ok {
		http.Error(w, fmt.Sprintf("Edge.Cuts file '%s' not found in uploaded gerbers. Upload all .gbr files.", jobResult.EdgeCutsFile), http.StatusBadRequest)
		return
	}

	// Handle PTH Drill File (optional)
	var drillHoles []DrillHole
	drillFile, drillHeader, err := r.FormFile("drill")
	if err == nil {
		defer drillFile.Close()
		drillPath := filepath.Join(tempDir, uuid+"_drill"+filepath.Ext(drillHeader.Filename))
		df, err := os.Create(drillPath)
		if err == nil {
			io.Copy(df, drillFile)
			df.Close()
			holes, err := ParseDrill(drillPath)
			if err != nil {
				log.Printf("Warning: Could not parse PTH drill file: %v", err)
			} else {
				drillHoles = append(drillHoles, holes...)
				fmt.Printf("Parsed %d PTH drill holes\n", len(holes))
			}
		}
	}

	// Handle NPTH Drill File (optional)
	npthFile, npthHeader, err := r.FormFile("npth")
	if err == nil {
		defer npthFile.Close()
		npthPath := filepath.Join(tempDir, uuid+"_npth"+filepath.Ext(npthHeader.Filename))
		nf, err := os.Create(npthPath)
		if err == nil {
			io.Copy(nf, npthFile)
			nf.Close()
			holes, err := ParseDrill(npthPath)
			if err != nil {
				log.Printf("Warning: Could not parse NPTH drill file: %v", err)
			} else {
				drillHoles = append(drillHoles, holes...)
				fmt.Printf("Parsed %d NPTH drill holes\n", len(holes))
			}
		}
	}

	// Filter out vias — only keep component and mounting holes
	var filteredHoles []DrillHole
	for _, h := range drillHoles {
		if h.Type != DrillTypeVia {
			filteredHoles = append(filteredHoles, h)
		}
	}
	fmt.Printf("After filtering: %d holes (vias removed)\n", len(filteredHoles))

	// Parse outline gerber
	fmt.Printf("Parsing outline %s...\n", outlinePath)
	outlineGf, err := ParseGerber(outlinePath)
	if err != nil {
		http.Error(w, fmt.Sprintf("Error parsing outline: %v", err), http.StatusInternalServerError)
		return
	}

	outlineBounds := outlineGf.CalculateBounds()

	// Save actual board dimensions before adding margins
	actualBoardW := outlineBounds.MaxX - outlineBounds.MinX
	actualBoardH := outlineBounds.MaxY - outlineBounds.MinY

	// Add margin for enclosure walls
	margin := ecfg.WallThickness + ecfg.Clearance + 5.0
	outlineBounds.MinX -= margin
	outlineBounds.MinY -= margin
	outlineBounds.MaxX += margin
	outlineBounds.MaxY += margin

	// Render outline to image
	fmt.Println("Rendering outline...")
	ecfg.OutlineBounds = &outlineBounds
	outlineImg := outlineGf.Render(ecfg.DPI, &outlineBounds)

	// Compute board box and count from the rendered image
	minBX, minBY, maxBX, maxBY := outlineImg.Bounds().Max.X, outlineImg.Bounds().Max.Y, -1, -1
	var boardCenterY float64
	var boardCount int

	wallMaskInt, boardMask := ComputeWallMask(outlineImg, ecfg.WallThickness+ecfg.Clearance, ecfg.DPI/25.4)
	_ = wallMaskInt // Mask required for internal board topology mapping

	imgW, imgH := outlineImg.Bounds().Max.X, outlineImg.Bounds().Max.Y
	for y := 0; y < imgH; y++ {
		for x := 0; x < imgW; x++ {
			if boardMask[y*imgW+x] {
				if x < minBX {
					minBX = x
				}
				if y < minBY {
					minBY = y
				}
				if x > maxBX {
					maxBX = x
				}
				if y > maxBY {
					maxBY = y
				}
				boardCenterY += float64(y)
				boardCount++
			}
		}
	}
	if boardCount > 0 {
		boardCenterY /= float64(boardCount)
	}

	// Auto-discover and render F.Courtyard from job file
	var courtyardImg image.Image
	if courtPath, ok := savedGerbers[jobResult.CourtyardFile]; ok && jobResult.CourtyardFile != "" {
		courtGf, err := ParseGerber(courtPath)
		if err != nil {
			log.Printf("Warning: Could not parse courtyard gerber: %v", err)
		} else {
			fmt.Println("Rendering courtyard layer...")
			courtyardImg = courtGf.Render(ecfg.DPI, &outlineBounds)
		}
	}

	// Auto-discover and render F.Mask from job file
	var soldermaskImg image.Image
	if maskPath, ok := savedGerbers[jobResult.SoldermaskFile]; ok && jobResult.SoldermaskFile != "" {
		maskGf, err := ParseGerber(maskPath)
		if err != nil {
			log.Printf("Warning: Could not parse soldermask gerber: %v", err)
		} else {
			fmt.Println("Rendering soldermask layer...")
			soldermaskImg = maskGf.Render(ecfg.DPI, &outlineBounds)
		}
	}

	// Also try F.Fab as fallback courtyard (many boards have F.Fab but not F.Courtyard)
	if courtyardImg == nil && jobResult.FabFile != "" {
		if fabPath, ok := savedGerbers[jobResult.FabFile]; ok {
			fabGf, err := ParseGerber(fabPath)
			if err != nil {
				log.Printf("Warning: Could not parse fab gerber: %v", err)
			} else {
				fmt.Println("Rendering F.Fab layer as courtyard fallback...")
				courtyardImg = fabGf.Render(ecfg.DPI, &outlineBounds)
			}
		}
	}

	pixelToMM := 25.4 / ecfg.DPI
	session := &EnclosureSession{
		Exports:       r.Form["exports"],
		OutlineGf:     outlineGf,
		OutlineImg:    outlineImg,
		CourtyardImg:  courtyardImg,
		SoldermaskImg: soldermaskImg,
		DrillHoles:    filteredHoles,
		Config:        ecfg,
		OutlineBounds: outlineBounds,
		BoardW:        actualBoardW,
		BoardH:        actualBoardH,
		TotalH:        ecfg.WallHeight + ecfg.PCBThickness + 1.0,
		MinBX:         float64(minBX)*pixelToMM + outlineBounds.MinX,
		MaxBX:         float64(maxBX)*pixelToMM + outlineBounds.MinX,
		BoardCenterY:  outlineBounds.MaxY - boardCenterY*pixelToMM,
		Sides:         ExtractBoardSidesFromMask(boardMask, imgW, imgH, pixelToMM, &outlineBounds),
	}
	sessionsMu.Lock()
	sessions[uuid] = session
	sessionsMu.Unlock()

	// Redirect to preview page
	http.Redirect(w, r, "/preview?id="+uuid, http.StatusSeeOther)

}

func footprintUploadHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	err := r.ParseMultipartForm(50 << 20) // 50 MB
	if err != nil {
		http.Error(w, "Error parsing form", http.StatusBadRequest)
		return
	}

	sessionID := r.FormValue("sessionId")
	if sessionID == "" {
		http.Error(w, "Missing sessionId", http.StatusBadRequest)
		return
	}

	sessionsMu.Lock()
	session, ok := sessions[sessionID]
	sessionsMu.Unlock()
	if !ok {
		http.Error(w, "Invalid session", http.StatusBadRequest)
		return
	}

	files := r.MultipartForm.File["gerbers"]
	var allFootprints []Footprint
	var fabGfList []*GerberFile

	for _, fileHeader := range files {
		f, err := fileHeader.Open()
		if err != nil {
			continue
		}

		b := make([]byte, 8)
		rand.Read(b)
		tempPath := filepath.Join("temp", fmt.Sprintf("%x_%s", b, fileHeader.Filename))
		out, err := os.Create(tempPath)
		if err == nil {
			io.Copy(out, f)
			out.Close()
			gf, err := ParseGerber(tempPath)
			if err == nil {
				allFootprints = append(allFootprints, ExtractFootprints(gf)...)
				fabGfList = append(fabGfList, gf)
			}
		}
		f.Close()
	}

	// Composite Fab images into one transparent overlay
	if len(fabGfList) > 0 {
		bounds := session.OutlineBounds
		imgW := int((bounds.MaxX - bounds.MinX) * session.Config.DPI / 25.4)
		imgH := int((bounds.MaxY - bounds.MinY) * session.Config.DPI / 25.4)
		if imgW > 0 && imgH > 0 {
			composite := image.NewRGBA(image.Rect(0, 0, imgW, imgH))
			// Initialize with pure transparency
			draw.Draw(composite, composite.Bounds(), &image.Uniform{color.Transparent}, image.Point{}, draw.Src)

			for _, gf := range fabGfList {
				layerImg := gf.Render(session.Config.DPI, &bounds)
				if rgba, ok := layerImg.(*image.RGBA); ok {
					for y := 0; y < imgH; y++ {
						for x := 0; x < imgW; x++ {
							// Gerber render background is Black. White is drawn pixels.
							if r, _, _, _ := rgba.At(x, y).RGBA(); r > 0x7FFF {
								// Set as cyan overlay for visibility
								composite.Set(x, y, color.RGBA{0, 255, 255, 180})
							}
						}
					}
				}
			}
			sessionsMu.Lock()
			session.FabImg = composite
			sessionsMu.Unlock()
		}
	}

	// Return all parsed footprints for visual selection
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(allFootprints)
}

func renderResult(w http.ResponseWriter, message string, files []string, backURL string, zipFile string) {
	tmpl, err := template.ParseFS(staticFiles, "static/result.html")
	if err != nil {
		http.Error(w, "Template error", http.StatusInternalServerError)
		return
	}
	data := struct {
		Message string
		Files   []string
		BackURL string
		ZipFile string
	}{Message: message, Files: files, BackURL: backURL, ZipFile: zipFile}
	tmpl.Execute(w, data)
}

// --- Enclosure Preview Session ---

type EnclosureSession struct {
	Exports       []string
	OutlineGf     *GerberFile
	OutlineImg    image.Image
	CourtyardImg  image.Image
	SoldermaskImg image.Image
	DrillHoles    []DrillHole
	Config        EnclosureConfig
	OutlineBounds Bounds
	BoardW        float64
	BoardH        float64
	TotalH        float64
	MinBX         float64
	MaxBX         float64
	BoardCenterY  float64
	Sides         []BoardSide
	FabImg        image.Image
}

var (
	sessions   = make(map[string]*EnclosureSession)
	sessionsMu sync.Mutex
)

func previewHandler(w http.ResponseWriter, r *http.Request) {
	id := r.URL.Query().Get("id")
	sessionsMu.Lock()
	session, ok := sessions[id]
	sessionsMu.Unlock()
	if !ok {
		http.Error(w, "Session not found. Please re-upload your files.", http.StatusNotFound)
		return
	}

	boardInfo := struct {
		BoardW float64     `json:"boardW"`
		BoardH float64     `json:"boardH"`
		TotalH float64     `json:"totalH"`
		Sides  []BoardSide `json:"sides"`
		MinX   float64     `json:"minX"`
		MaxY   float64     `json:"maxY"`
		DPI    float64     `json:"dpi"`
	}{
		BoardW: session.BoardW,
		BoardH: session.BoardH,
		TotalH: session.TotalH,
		Sides:  session.Sides,
		MinX:   session.OutlineBounds.MinX,
		MaxY:   session.OutlineBounds.MaxY,
		DPI:    session.Config.DPI,
	}
	boardJSON, _ := json.Marshal(boardInfo)

	tmpl, err := template.ParseFS(staticFiles, "static/preview.html")
	if err != nil {
		http.Error(w, fmt.Sprintf("Template error: %v", err), http.StatusInternalServerError)
		return
	}

	data := struct {
		SessionID     string
		BoardInfoJSON template.JS
	}{
		SessionID:     id,
		BoardInfoJSON: template.JS(boardJSON),
	}
	tmpl.Execute(w, data)
}

func previewImageHandler(w http.ResponseWriter, r *http.Request) {
	parts := strings.Split(r.URL.Path, "/")
	if len(parts) < 3 {
		http.NotFound(w, r)
		return
	}
	id := parts[2]

	sessionsMu.Lock()
	session, ok := sessions[id]
	sessionsMu.Unlock()
	if !ok {
		http.NotFound(w, r)
		return
	}

	w.Header().Set("Content-Type", "image/png")
	png.Encode(w, session.OutlineImg)
}

func generateEnclosureHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != "POST" {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}
	r.ParseForm()

	id := r.FormValue("sessionId")
	sessionsMu.Lock()
	session, ok := sessions[id]
	sessionsMu.Unlock()
	if !ok {
		http.Error(w, "Session expired. Please re-upload your files.", http.StatusNotFound)
		return
	}

	// Parse side cutouts from JSON
	var sideCutouts []SideCutout
	cutoutsJSON := r.FormValue("sideCutouts")
	if cutoutsJSON != "" && cutoutsJSON != "[]" {
		var rawCutouts []struct {
			Side int     `json:"side"`
			X    float64 `json:"x"`
			Y    float64 `json:"y"`
			W    float64 `json:"w"`
			H    float64 `json:"h"`
			R    float64 `json:"r"`
			L    string  `json:"l"`
		}
		if err := json.Unmarshal([]byte(cutoutsJSON), &rawCutouts); err != nil {
			log.Printf("Warning: could not parse side cutouts: %v", err)
		} else {
			for _, rc := range rawCutouts {
				layer := rc.L
				if layer == "" {
					layer = "F"
				}
				sideCutouts = append(sideCutouts, SideCutout{
					Side:         rc.Side,
					X:            rc.X,
					Y:            rc.Y,
					Width:        rc.W,
					Height:       rc.H,
					CornerRadius: rc.R,
					Layer:        layer,
				})
			}
		}
		fmt.Printf("Side cutouts: %d\n", len(sideCutouts))
	}

	var generatedFiles []string

	// Helper to check what formats were requested
	wantsType := func(t string) bool {
		for _, e := range session.Exports {
			if e == t {
				return true
			}
		}
		return false
	}

	// Always default to STL if nothing is specified
	if len(session.Exports) == 0 {
		session.Exports = []string{"stl"}
	}

	// Process STL
	if wantsType("stl") {
		fmt.Println("Generating STLs...")
		result := GenerateEnclosure(session.OutlineImg, session.DrillHoles, session.Config, session.CourtyardImg, session.SoldermaskImg, sideCutouts, session.Sides)
		encPath := filepath.Join("temp", id+"_enclosure.stl")
		trayPath := filepath.Join("temp", id+"_tray.stl")
		WriteSTL(encPath, result.EnclosureTriangles)
		WriteSTL(trayPath, result.TrayTriangles)
		generatedFiles = append(generatedFiles, filepath.Base(encPath), filepath.Base(trayPath))
	}

	// Process SCAD
	if wantsType("scad") {
		fmt.Println("Generating Native SCAD scripts...")
		scadPathEnc := filepath.Join("temp", id+"_enclosure.scad")
		scadPathTray := filepath.Join("temp", id+"_tray.scad")
		outlinePoly := ExtractPolygonFromGerber(session.OutlineGf)
		WriteNativeSCAD(scadPathEnc, false, outlinePoly, session.Config, session.DrillHoles, sideCutouts, session.Sides, session.MinBX, session.MaxBX, session.BoardCenterY)
		WriteNativeSCAD(scadPathTray, true, outlinePoly, session.Config, session.DrillHoles, sideCutouts, session.Sides, session.MinBX, session.MaxBX, session.BoardCenterY)
		generatedFiles = append(generatedFiles, filepath.Base(scadPathEnc), filepath.Base(scadPathTray))
	}

	// Process SVG
	if wantsType("svg") && session.OutlineGf != nil {
		fmt.Println("Generating SVG vector outline...")
		svgPath := filepath.Join("temp", id+"_outline.svg")
		WriteSVG(svgPath, session.OutlineGf, &session.OutlineBounds)
		generatedFiles = append(generatedFiles, filepath.Base(svgPath))
	}

	// Process PNG
	if wantsType("png") && session.OutlineImg != nil {
		fmt.Println("Generating PNG raster outline...")
		pngPath := filepath.Join("temp", id+"_outline.png")
		fImg, _ := os.Create(pngPath)
		png.Encode(fImg, session.OutlineImg)
		fImg.Close()
		generatedFiles = append(generatedFiles, filepath.Base(pngPath))
	}

	// Generate Master Zip if more than 1 file
	var zipFile string
	if len(generatedFiles) > 1 {
		zipPath := filepath.Join("temp", id+"_all_exports.zip")
		zf, err := os.Create(zipPath)
		if err == nil {
			zw := zip.NewWriter(zf)
			for _, fn := range generatedFiles {
				fw, _ := zw.Create(fn)
				fb, _ := os.ReadFile(filepath.Join("temp", fn))
				fw.Write(fb)
			}
			zw.Close()
			zf.Close()
			zipFile = filepath.Base(zipPath)
		}
	}

	// Maintain the session for UI adjustments and re-generation
	renderResult(w, "Your files have been generated successfully.", generatedFiles, "/preview?id="+id, zipFile)
}

func downloadHandler(w http.ResponseWriter, r *http.Request) {
	vars := strings.Split(r.URL.Path, "/")
	if len(vars) < 3 {
		http.NotFound(w, r)
		return
	}
	filename := vars[2]

	// Validate filename to prevent path traversal
	if strings.Contains(filename, "/") || strings.Contains(filename, "\\") || strings.Contains(filename, "..") {
		http.Error(w, "Invalid filename", http.StatusBadRequest)
		return
	}

	path := filepath.Join("temp", filename)
	if _, err := os.Stat(path); os.IsNotExist(err) {
		http.NotFound(w, r)
		return
	}

	w.Header().Set("Content-Disposition", "attachment; filename="+filename)
	w.Header().Set("Content-Type", "application/octet-stream")
	http.ServeFile(w, r, path)
}

func runServer(port string) {
	// Serve static assets from the embedded filesystem
	http.Handle("/static/", http.FileServer(http.FS(staticFiles)))

	http.HandleFunc("/", indexHandler)
	http.HandleFunc("/upload", uploadHandler)
	http.HandleFunc("/upload-enclosure", enclosureUploadHandler)
	http.HandleFunc("/upload-footprints", footprintUploadHandler)
	http.HandleFunc("/preview", previewHandler)
	http.HandleFunc("/preview-image/", previewImageHandler)
	http.HandleFunc("/generate-enclosure", generateEnclosureHandler)
	http.HandleFunc("/download/", downloadHandler)

	fmt.Printf("Starting server on http://0.0.0.0:%s\n", port)
	log.Fatal(http.ListenAndServe(":"+port, nil))
}

// --- Main ---

var (
	flagStencilHeight float64
	flagWallHeight    float64
	flagWallThickness float64
	flagDPI           float64
	flagKeepPNG       bool
	flagServer        bool
	flagPort          string
)

func main() {
	flag.Float64Var(&flagStencilHeight, "height", DefaultStencilHeight, "Stencil height in mm")
	flag.Float64Var(&flagWallHeight, "wall-height", DefaultWallHeight, "Wall height in mm")
	flag.Float64Var(&flagWallThickness, "wall-thickness", DefaultWallThickness, "Wall thickness in mm")
	flag.Float64Var(&flagDPI, "dpi", DefaultDPI, "DPI for rendering (lower = smaller file, rougher curves)")
	flag.BoolVar(&flagKeepPNG, "keep-png", false, "Save intermediate PNG file")

	flag.BoolVar(&flagServer, "server", false, "Start in server mode")
	flag.StringVar(&flagPort, "port", "8080", "Port to run the server on")

	flag.Parse()

	if flagServer {
		runServer(flagPort)
	} else {
		cfg := Config{
			StencilHeight: flagStencilHeight,
			WallHeight:    flagWallHeight,
			WallThickness: flagWallThickness,
			DPI:           flagDPI,
			KeepPNG:       flagKeepPNG,
		}
		runCLI(cfg, flag.Args())
	}
}
