package main

import (
	"archive/zip"
	"fmt"
	"io"
	"io/fs"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"
)

const dosboxConfTemplate = `[autoexec]
@echo off
mount c .
c:
cls
%s
`

// handleUploadGame handles POST /api/upload — receives a ZIP, creates a .jsdos bundle.
func handleUploadGame(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	// Parse multipart form (max 500MB)
	if err := r.ParseMultipartForm(500 << 20); err != nil {
		http.Error(w, "Failed to parse upload: "+err.Error(), http.StatusBadRequest)
		return
	}

	file, header, err := r.FormFile("file")
	if err != nil {
		http.Error(w, "No file provided", http.StatusBadRequest)
		return
	}
	defer file.Close()

	title := r.FormValue("title")
	if title == "" {
		title = strings.TrimSuffix(header.Filename, filepath.Ext(header.Filename))
	}

	gameID := sanitizeID(title)

	// Ensure uniqueness
	baseID := gameID
	counter := 2
	for {
		if _, err := loadGame(gameID); err != nil {
			break // not found, we can use this ID
		}
		gameID = fmt.Sprintf("%s-%d", baseID, counter)
		counter++
	}

	// Save uploaded ZIP to temp file
	tmpDir, err := os.MkdirTemp("", "dostalgia-upload-")
	if err != nil {
		http.Error(w, "Server error", http.StatusInternalServerError)
		return
	}
	defer os.RemoveAll(tmpDir)

	zipPath := filepath.Join(tmpDir, header.Filename)
	dst, err := os.Create(zipPath)
	if err != nil {
		http.Error(w, "Server error", http.StatusInternalServerError)
		return
	}
	if _, err := io.Copy(dst, file); err != nil {
		dst.Close()
		http.Error(w, "Failed to save upload", http.StatusInternalServerError)
		return
	}
	dst.Close()

	// Extract ZIP
	extractDir := filepath.Join(tmpDir, "extracted")
	if err := os.MkdirAll(extractDir, 0755); err != nil {
		http.Error(w, "Server error", http.StatusInternalServerError)
		return
	}
	if err := extractZip(zipPath, extractDir); err != nil {
		http.Error(w, "Invalid ZIP file: "+err.Error(), http.StatusBadRequest)
		return
	}

	// Find main executable
	mainExe := findMainExe(extractDir)
	if mainExe == "" {
		http.Error(w, "No executable (.exe/.com/.bat) found in the archive", http.StatusBadRequest)
		return
	}

	// Determine size and strategy
	sizeMB := dirSizeMB(extractDir)
	bundleType := "standard"
	if sizeMB > 80 {
		bundleType = "sockdrive"
	}

	// Set up game directory
	gameDirPath := gameDir(gameID)
	if err := os.MkdirAll(gameDirPath, 0755); err != nil {
		http.Error(w, "Server error", http.StatusInternalServerError)
		return
	}

	var bundleFile string
	if bundleType == "sockdrive" {
		// Copy all files loose
		if err := copyDir(extractDir, gameDirPath); err != nil {
			http.Error(w, "Failed to store game files", http.StatusInternalServerError)
			return
		}
		// Create dosbox.conf
		confDir := filepath.Join(gameDirPath, ".jsdos")
		os.MkdirAll(confDir, 0755)
		relExe, _ := filepath.Rel(extractDir, mainExe)
		os.WriteFile(filepath.Join(confDir, "dosbox.conf"),
			[]byte(fmt.Sprintf(dosboxConfTemplate, relExe)), 0644)
		bundleFile = ".jsdos/dosbox.conf"
	} else {
		// Create .jsdos ZIP bundle flat in data/games/
		bundleFile = gameID + ".jsdos"
		bundlePath := filepath.Join(filepath.Dir(gameDirPath), bundleFile)
		if err := createJSDOSBundle(extractDir, mainExe, bundlePath); err != nil {
			http.Error(w, "Failed to create bundle: "+err.Error(), http.StatusInternalServerError)
			return
		}
	}

	// Save metadata
	game := &Game{
		ID:         gameID,
		Title:      title,
		BundleType: bundleType,
		BundleFile: bundleFile,
		Ready:      true,
	}
	if err := saveGame(game); err != nil {
		http.Error(w, "Failed to save metadata", http.StatusInternalServerError)
		return
	}

	writeJSON(w, http.StatusCreated, game)
}

// extractZip extracts a ZIP file to the given directory.
func extractZip(zipPath, dest string) error {
	r, err := zip.OpenReader(zipPath)
	if err != nil {
		return err
	}
	defer r.Close()

	for _, f := range r.File {
		// Prevent path traversal
		target := filepath.Join(dest, f.Name)
		if !strings.HasPrefix(filepath.Clean(target), filepath.Clean(dest)+string(os.PathSeparator)) {
			continue
		}

		if f.FileInfo().IsDir() {
			os.MkdirAll(target, 0755)
			continue
		}

		os.MkdirAll(filepath.Dir(target), 0755)
		src, err := f.Open()
		if err != nil {
			return err
		}
		dst, err := os.Create(target)
		if err != nil {
			src.Close()
			return err
		}
		_, err = io.Copy(dst, src)
		src.Close()
		dst.Close()
		if err != nil {
			return err
		}
	}
	return nil
}

// mainExeCandidate is used for sorting executables by relevance.
type mainExeCandidate struct {
	depth       int
	isInstaller bool
	size        int64
	name        string
	path        string
}

// findMainExe finds the most likely main executable in an extracted game directory.
func findMainExe(dir string) string {
	var candidates []mainExeCandidate

	filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
		if err != nil || d.IsDir() {
			return nil
		}
		ext := strings.ToLower(filepath.Ext(d.Name()))
		if ext != ".exe" && ext != ".com" && ext != ".bat" {
			return nil
		}
		info, _ := d.Info()
		name := strings.ToLower(d.Name())
		isInstaller := strings.Contains(name, "install") ||
			strings.Contains(name, "setup") ||
			strings.Contains(name, "config")
		depth := strings.Count(path[len(dir):], string(os.PathSeparator))
		candidates = append(candidates, mainExeCandidate{
			depth:       depth,
			isInstaller: isInstaller,
			size:        info.Size(),
			name:        name,
			path:        path,
		})
		return nil
	})

	if len(candidates) == 0 {
		return ""
	}

	// Sort: shallow depth, non-installer, larger files first
	sortCandidates(candidates)
	return candidates[0].path
}

func sortCandidates(c []mainExeCandidate) {
	for i := 0; i < len(c); i++ {
		for j := i + 1; j < len(c); j++ {
			// Compare: depth (lower first), installer (false first), size (larger first)
			swap := false
			if c[i].depth != c[j].depth {
				swap = c[i].depth > c[j].depth
			} else if c[i].isInstaller != c[j].isInstaller {
				swap = c[i].isInstaller
			} else {
				swap = c[i].size < c[j].size
			}
			if swap {
				c[i], c[j] = c[j], c[i]
			}
		}
	}
}

// dirSizeMB returns the total size of a directory in megabytes.
func dirSizeMB(dir string) float64 {
	var total int64
	filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
		if err != nil || d.IsDir() {
			return nil
		}
		info, _ := d.Info()
		total += info.Size()
		return nil
	})
	return float64(total) / (1024 * 1024)
}

// copyDir copies a directory recursively.
func copyDir(src, dst string) error {
	return filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		rel, _ := filepath.Rel(src, path)
		target := filepath.Join(dst, rel)
		if d.IsDir() {
			return os.MkdirAll(target, 0755)
		}
		data, err := os.ReadFile(path)
		if err != nil {
			return err
		}
		return os.WriteFile(target, data, 0644)
	})
}

// createJSDOSBundle creates a .jsdos bundle ZIP from extracted game files.
func createJSDOSBundle(extractDir, mainExe, bundlePath string) error {
	// Create a temp directory for bundling
	tmpDir, err := os.MkdirTemp("", "dostalgia-bundle-")
	if err != nil {
		return err
	}
	defer os.RemoveAll(tmpDir)

	// Copy game files
	if err := copyDir(extractDir, tmpDir); err != nil {
		return err
	}

	// Create .jsdos config
	jsdosDir := filepath.Join(tmpDir, ".jsdos")
	if err := os.MkdirAll(jsdosDir, 0755); err != nil {
		return err
	}

	relExe, _ := filepath.Rel(extractDir, mainExe)
	conf := fmt.Sprintf(dosboxConfTemplate, relExe)
	if err := os.WriteFile(filepath.Join(jsdosDir, "dosbox.conf"), []byte(conf), 0644); err != nil {
		return err
	}

	// Create the ZIP
	zf, err := os.Create(bundlePath)
	if err != nil {
		return err
	}
	defer zf.Close()

	zw := zip.NewWriter(zf)
	defer zw.Close()

	return filepath.WalkDir(tmpDir, func(path string, d fs.DirEntry, err error) error {
		if err != nil || d.IsDir() {
			return err
		}
		rel, _ := filepath.Rel(tmpDir, path)
		w, err := zw.Create(rel)
		if err != nil {
			return err
		}
		data, err := os.ReadFile(path)
		if err != nil {
			return err
		}
		_, err = w.Write(data)
		return err
	})
}

// handleUploadCover handles POST /api/games/{id}/cover — upload cover art.
func handleUploadCover(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	id := r.PathValue("id")
	if _, err := loadGame(id); err != nil {
		http.Error(w, "Game not found", http.StatusNotFound)
		return
	}

	if err := r.ParseMultipartForm(10 << 20); err != nil {
		http.Error(w, "File too large", http.StatusBadRequest)
		return
	}

	file, header, err := r.FormFile("file")
	if err != nil {
		http.Error(w, "No file provided", http.StatusBadRequest)
		return
	}
	defer file.Close()

	// Determine extension
	ext := strings.ToLower(filepath.Ext(header.Filename))
	if ext != ".jpg" && ext != ".jpeg" && ext != ".png" && ext != ".webp" {
		http.Error(w, "Only JPG, PNG, or WebP images accepted", http.StatusBadRequest)
		return
	}

	coverPath := filepath.Join(gameDir(id), "cover"+ext)
	// Remove old cover files
	for _, oldExt := range []string{".jpg", ".jpeg", ".png", ".webp"} {
		os.Remove(filepath.Join(gameDir(id), "cover"+oldExt))
	}

	dst, err := os.Create(coverPath)
	if err != nil {
		http.Error(w, "Server error", http.StatusInternalServerError)
		return
	}
	defer dst.Close()

	if _, err := io.Copy(dst, file); err != nil {
		http.Error(w, "Failed to save cover", http.StatusInternalServerError)
		return
	}

	writeJSON(w, http.StatusOK, map[string]string{"status": "ok", "path": coverPath})
}

var _ = time.Now // keep import
