package main

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

//go:embed all:frontend/dist
var frontendFS embed.FS

var (
	dataDir string
	devMode bool
)

func main() {
	// Determine data directory
	dataDir = os.Getenv("DOSTALGIA_DATA")
	if dataDir == "" {
		dataDir = "data"
	}
	devMode = os.Getenv("DOSTALGIA_DEV") == "1"

	// Ensure data directories exist
	for _, d := range []string{"games", "artwork", "saves"} {
		os.MkdirAll(filepath.Join(dataDir, d), 0755)
	}

	mux := http.NewServeMux()

	// API routes
	mux.HandleFunc("/api/health", handleHealth)
	mux.HandleFunc("/api/games", handleGames)
	mux.HandleFunc("/api/games/", handleGameByID)
	mux.HandleFunc("/api/upload", handleUploadGame)
	mux.HandleFunc("/api/igdb/search", handleIGDBSearch)

	// Serve game bundles and artwork as static files
	gameFS := http.FileServer(http.Dir(filepath.Join(dataDir, "games")))
	mux.Handle("/games/", http.StripPrefix("/games/", gameFS))

	artFS := http.FileServer(http.Dir(filepath.Join(dataDir, "artwork")))
	mux.Handle("/artwork/", http.StripPrefix("/artwork/", artFS))

	// Sockdrive file server
	mux.HandleFunc("/api/sockdrive/", handleSockdrive)

	// Frontend SPA — serve embedded files, fall back to index.html
	mux.HandleFunc("/", handleFrontend)

	port := os.Getenv("PORT")
	if port == "" {
		port = "8765"
	}

	log.Printf("🎮 Dostalgia starting on :%s (data: %s)", port, dataDir)
	if devMode {
		log.Println("⚠️  DEV mode — run Vite dev server alongside for hot reload")
	}

	if err := http.ListenAndServe(":"+port, mux); err != nil {
		log.Fatal(err)
	}
}

// ─── API Handlers ────────────────────────────────────────────────

func handleHealth(w http.ResponseWriter, r *http.Request) {
	writeJSON(w, http.StatusOK, map[string]string{
		"status":  "ok",
		"version": "0.1.0",
	})
}

func handleGames(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case http.MethodGet:
		games, err := listGames()
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		writeJSON(w, http.StatusOK, map[string]interface{}{
			"games": games,
			"total": len(games),
		})

	case http.MethodPost:
		// Create a game manually (without upload)
		var g Game
		if err := json.NewDecoder(r.Body).Decode(&g); err != nil {
			http.Error(w, "Invalid JSON", http.StatusBadRequest)
			return
		}
		if g.Title == "" {
			http.Error(w, "Title is required", http.StatusBadRequest)
			return
		}
		if g.ID == "" {
			g.ID = sanitizeID(g.Title)
		}
		if err := saveGame(&g); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		writeJSON(w, http.StatusCreated, g)

	default:
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
	}
}

func handleGameByID(w http.ResponseWriter, r *http.Request) {
	// Extract ID from /api/games/{id} or /api/games/{id}/...
	path := strings.TrimPrefix(r.URL.Path, "/api/games/")
	parts := strings.SplitN(path, "/", 2)
	id := parts[0]

	if id == "" {
		http.Error(w, "Game ID required", http.StatusBadRequest)
		return
	}

	// Cover upload sub-endpoint
	if len(parts) > 1 && parts[1] == "cover" {
		handleUploadCover(w, r)
		return
	}

	switch r.Method {
	case http.MethodGet:
		g, err := loadGame(id)
		if err != nil {
			http.Error(w, "Game not found", http.StatusNotFound)
			return
		}
		writeJSON(w, http.StatusOK, g)

	case http.MethodPatch:
		g, err := loadGame(id)
		if err != nil {
			http.Error(w, "Game not found", http.StatusNotFound)
			return
		}
		var updates map[string]interface{}
		if err := json.NewDecoder(r.Body).Decode(&updates); err != nil {
			http.Error(w, "Invalid JSON", http.StatusBadRequest)
			return
		}
		// Apply simple string/number fields
		if v, ok := updates["title"].(string); ok {
			g.Title = v
		}
		if v, ok := updates["year"].(float64); ok {
			g.Year = int(v)
		}
		if v, ok := updates["genre"].(string); ok {
			g.Genre = v
		}
		if v, ok := updates["developer"].(string); ok {
			g.Developer = v
		}
		if v, ok := updates["publisher"].(string); ok {
			g.Publisher = v
		}
		if v, ok := updates["description"].(string); ok {
			g.Description = v
		}
		if v, ok := updates["rating"].(float64); ok {
			g.Rating = v
		}
		if err := saveGame(g); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		writeJSON(w, http.StatusOK, g)

	case http.MethodDelete:
		g, err := loadGame(id)
		if err != nil {
			http.Error(w, "Game not found", http.StatusNotFound)
			return
		}
		// Remove the game directory (game.json, cover, sockdrive files)
		os.RemoveAll(gameDir(g.ID))
		// Also remove the flat .jsdos bundle if it exists
		bundlePath := filepath.Join(dataDir, "games", g.ID+".jsdos")
		os.Remove(bundlePath)
		writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})

	default:
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
	}
}

// ─── Sockdrive File Server ──────────────────────────────────────

func handleSockdrive(w http.ResponseWriter, r *http.Request) {
	// /api/sockdrive/{slug}/file?path=...
	path := strings.TrimPrefix(r.URL.Path, "/api/sockdrive/")
	parts := strings.SplitN(path, "/", 2)
	if len(parts) < 2 {
		http.Error(w, "Invalid path", http.StatusBadRequest)
		return
	}
	slug := parts[0]

	if parts[1] == "file" {
		filePath := r.URL.Query().Get("path")
		if filePath == "" {
			http.Error(w, "path query parameter required", http.StatusBadRequest)
			return
		}
		fullPath := filepath.Join(gameDir(slug), filePath)
		// Prevent traversal
		fullPath = filepath.Clean(fullPath)
		if !strings.HasPrefix(fullPath, filepath.Clean(gameDir(slug))) {
			http.Error(w, "Forbidden", http.StatusForbidden)
			return
		}
		http.ServeFile(w, r, fullPath)
		return
	}

	if parts[1] == "files" {
		// List all files in game directory
		var files []map[string]interface{}
		gameDirPath := gameDir(slug)
		filepath.WalkDir(gameDirPath, func(path string, d fs.DirEntry, err error) error {
			if err != nil || d.IsDir() {
				return nil
			}
			info, _ := d.Info()
			rel, _ := filepath.Rel(gameDirPath, path)
			files = append(files, map[string]interface{}{
				"path": strings.ReplaceAll(rel, "\\", "/"),
				"size": info.Size(),
			})
			return nil
		})
		writeJSON(w, http.StatusOK, map[string]interface{}{
			"files": files,
			"total": len(files),
		})
		return
	}

	http.Error(w, "Not found", http.StatusNotFound)
}

// ─── Frontend SPA ───────────────────────────────────────────────

func handleFrontend(w http.ResponseWriter, r *http.Request) {
	// Don't intercept API or static routes
	if strings.HasPrefix(r.URL.Path, "/api/") ||
		strings.HasPrefix(r.URL.Path, "/games/") ||
		strings.HasPrefix(r.URL.Path, "/artwork/") {
		http.NotFound(w, r)
		return
	}

	var content fs.FS
	var err error

	if devMode {
		content = os.DirFS("frontend/dist")
	} else {
		content, err = fs.Sub(frontendFS, "frontend/dist")
		if err != nil {
			http.Error(w, "Frontend not built", http.StatusServiceUnavailable)
			return
		}
	}

	// Try to serve the exact file
	filePath := strings.TrimPrefix(r.URL.Path, "/")
	if filePath == "" {
		filePath = "index.html"
	}

	f, err := content.Open(filePath)
	if err == nil {
		f.Close()
		http.FileServer(http.FS(content)).ServeHTTP(w, r)
		return
	}

	// Fallback to index.html for SPA routing
	indexData, err := fs.ReadFile(content, "index.html")
	if err != nil {
		http.Error(w, "Frontend not built. Run 'cd frontend && npm run build'", http.StatusServiceUnavailable)
		return
	}
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	w.Write(indexData)
}

// ─── Helpers ────────────────────────────────────────────────────

func writeJSON(w http.ResponseWriter, status int, data interface{}) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	json.NewEncoder(w).Encode(data)
}
