package web

import (
	"encoding/json"
	"net/http"
	"path/filepath"
	"strings"

	"gitea.dooplex.hu/admin/felhom-controller/internal/appexport"
)

// jsonResponse writes a JSON body with a 200 status.
// (Shared JSON helper previously defined in the now-removed storage_handlers.go.)
func jsonResponse(w http.ResponseWriter, v interface{}) {
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	json.NewEncoder(w).Encode(v)
}

// jsonError writes a JSON error response with the given status code.
func jsonError(w http.ResponseWriter, msg string, code int) {
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.WriteHeader(code)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"ok":    false,
		"error": msg,
	})
}

// ServeExportAPI dispatches /api/export/* endpoints.
func (s *Server) ServeExportAPI(w http.ResponseWriter, r *http.Request) {
	path := r.URL.Path

	switch {
	// GET /api/export/estimate?stack=X&drive=Y
	case path == "/api/export/estimate" && r.Method == http.MethodGet:
		s.apiExportEstimate(w, r)

	// POST /api/export/start
	case path == "/api/export/start" && r.Method == http.MethodPost:
		s.apiExportStart(w, r)

	// GET /api/export/status
	case path == "/api/export/status" && r.Method == http.MethodGet:
		s.apiExportStatus(w, r)

	// GET /api/export/bundles — scan for .fab files on all drives
	case path == "/api/export/bundles" && r.Method == http.MethodGet:
		s.apiExportBundles(w, r)

	// POST /api/export/manifest — read manifest from a .fab file
	case path == "/api/export/manifest" && r.Method == http.MethodPost:
		s.apiExportManifest(w, r)

	// POST /api/export/import — start async import
	case path == "/api/export/import" && r.Method == http.MethodPost:
		s.apiImportStart(w, r)

	// GET /api/export/import/status — poll import progress
	case path == "/api/export/import/status" && r.Method == http.MethodGet:
		s.apiImportStatus(w, r)

	default:
		http.NotFound(w, r)
	}
}

// exportPageHandler renders the export form for a specific app.
func (s *Server) exportPageHandler(w http.ResponseWriter, r *http.Request, name string) {
	if s.appExporter == nil {
		http.Error(w, "App export not available", http.StatusServiceUnavailable)
		return
	}

	stack, ok := s.stackMgr.GetStack(name)
	if !ok || !stack.Deployed {
		http.NotFound(w, r)
		return
	}

	// Build drive list for the dropdown
	drives := s.storageDriveList()

	data := map[string]interface{}{
		"Stack":  stack,
		"Drives": drives,
	}
	s.executeTemplate(w, r, "app_export", data)
}

// importPageHandler renders the import page (standalone, not tied to a stack).
func (s *Server) importPageHandler(w http.ResponseWriter, r *http.Request) {
	if s.appExporter == nil {
		http.Error(w, "App import not available", http.StatusServiceUnavailable)
		return
	}

	drives := s.storageDriveList()
	bundles := appexport.ScanForBundles(drives)

	data := map[string]interface{}{
		"Bundles": bundles,
	}
	s.executeTemplate(w, r, "app_import", data)
}

// apiExportEstimate returns size estimation for an export.
func (s *Server) apiExportEstimate(w http.ResponseWriter, r *http.Request) {
	if s.appExporter == nil {
		jsonError(w, "App export not available", http.StatusServiceUnavailable)
		return
	}

	stackName := r.URL.Query().Get("stack")
	drive := r.URL.Query().Get("drive")
	s.logger.Printf("[DEBUG] [web] apiExportEstimate: stack=%q drive=%q", stackName, drive)
	if stackName == "" || drive == "" {
		jsonError(w, "Missing stack or drive parameter", http.StatusBadRequest)
		return
	}

	if !s.isValidDrivePath(drive) {
		s.logger.Printf("[DEBUG] [web] apiExportEstimate: invalid drive path %q", drive)
		jsonError(w, "Invalid drive path", http.StatusBadRequest)
		return
	}

	est, err := s.appExporter.EstimateExport(stackName, drive)
	if err != nil {
		s.logger.Printf("[ERROR] [web] Export estimate failed for %s: %v", stackName, err)
		s.logger.Printf("[DEBUG] [web] apiExportEstimate error: %v", err)
		jsonError(w, err.Error(), http.StatusInternalServerError)
		return
	}

	s.logger.Printf("[DEBUG] [web] apiExportEstimate: total=%s free=%s fits=%v",
		est.TotalSizeHuman, est.DestFreeHuman, est.FitsOnDest)
	jsonResponse(w, map[string]interface{}{
		"ok":   true,
		"data": est,
	})
}

// apiExportStart starts an async export.
func (s *Server) apiExportStart(w http.ResponseWriter, r *http.Request) {
	if s.appExporter == nil {
		jsonError(w, "App export not available", http.StatusServiceUnavailable)
		return
	}

	var req struct {
		StackName string `json:"stack_name"`
		DestDrive string `json:"dest_drive"`
		Password  string `json:"password"`
		StopApp   bool   `json:"stop_app"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		s.logger.Printf("[DEBUG] [web] apiExportStart: invalid body: %v", err)
		jsonError(w, "Invalid request body", http.StatusBadRequest)
		return
	}

	s.logger.Printf("[DEBUG] [web] apiExportStart: stack=%q drive=%q encrypted=%v stopApp=%v",
		req.StackName, req.DestDrive, req.Password != "", req.StopApp)

	if req.StackName == "" || req.DestDrive == "" {
		jsonError(w, "Missing stack_name or dest_drive", http.StatusBadRequest)
		return
	}
	// F2 (defense-in-depth): reject a traversal/escape stack_name before any export work.
	if !validStackName(req.StackName) {
		jsonError(w, "Invalid stack_name", http.StatusBadRequest)
		return
	}

	if !s.isValidDrivePath(req.DestDrive) {
		s.logger.Printf("[DEBUG] [web] apiExportStart: invalid drive path %q", req.DestDrive)
		jsonError(w, "Invalid drive path", http.StatusBadRequest)
		return
	}

	err := s.appExporter.StartExport(appexport.ExportRequest{
		StackName: req.StackName,
		DestDrive: req.DestDrive,
		Password:  req.Password,
		StopApp:   req.StopApp,
	})
	if err != nil {
		s.logger.Printf("[ERROR] [web] Export start failed for %s: %v", req.StackName, err)
		s.logger.Printf("[DEBUG] [web] apiExportStart error: %v", err)
		jsonError(w, err.Error(), http.StatusConflict)
		return
	}

	s.logger.Printf("[INFO] [web] Export started for %s to %s", req.StackName, req.DestDrive)
	jsonResponse(w, map[string]interface{}{"ok": true})
}

// apiExportStatus returns current export/import job status.
func (s *Server) apiExportStatus(w http.ResponseWriter, r *http.Request) {
	if s.appExporter == nil {
		jsonError(w, "App export not available", http.StatusServiceUnavailable)
		return
	}

	job := s.appExporter.GetActiveJob()
	if job == nil {
		jsonResponse(w, map[string]interface{}{
			"ok":      true,
			"running": false,
			"done":    false,
		})
		return
	}

	jsonResponse(w, job.Snapshot())
}

// apiExportBundles scans all drives for .fab bundles.
func (s *Server) apiExportBundles(w http.ResponseWriter, r *http.Request) {
	if s.appExporter == nil {
		jsonError(w, "App export not available", http.StatusServiceUnavailable)
		return
	}

	drives := s.storageDriveList()
	bundles := appexport.ScanForBundles(drives)

	jsonResponse(w, map[string]interface{}{
		"ok":      true,
		"bundles": bundles,
	})
}

// apiExportManifest reads and returns the manifest from a .fab file.
func (s *Server) apiExportManifest(w http.ResponseWriter, r *http.Request) {
	if s.appExporter == nil {
		jsonError(w, "App export not available", http.StatusServiceUnavailable)
		return
	}

	var req struct {
		Path     string `json:"path"`
		Password string `json:"password"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		jsonError(w, "Invalid request body", http.StatusBadRequest)
		return
	}

	if req.Path == "" {
		jsonError(w, "Missing path", http.StatusBadRequest)
		return
	}

	s.logger.Printf("[DEBUG] [web] apiExportManifest: path=%q hasPassword=%v", req.Path, req.Password != "")

	// Security: validate path is within a registered exports directory
	if !s.isValidExportPath(req.Path) {
		s.logger.Printf("[DEBUG] [web] apiExportManifest: invalid path %q", req.Path)
		jsonError(w, "Invalid bundle path", http.StatusBadRequest)
		return
	}

	encrypted, _ := appexport.IsEncryptedFAB(req.Path)
	s.logger.Printf("[DEBUG] [web] apiExportManifest: encrypted=%v", encrypted)

	var manifest *appexport.Manifest
	var err error
	if encrypted {
		if req.Password == "" {
			s.logger.Printf("[DEBUG] [web] apiExportManifest: encrypted, needs password")
			jsonResponse(w, map[string]interface{}{
				"ok":             true,
				"encrypted":      true,
				"needs_password": true,
			})
			return
		}
		manifest, err = appexport.ReadManifestFromEncryptedFAB(req.Path, req.Password)
	} else {
		manifest, err = appexport.ReadManifestFromFAB(req.Path)
	}

	if err != nil {
		s.logger.Printf("[DEBUG] [web] apiExportManifest: error: %v", err)
		jsonError(w, err.Error(), http.StatusBadRequest)
		return
	}

	s.logger.Printf("[DEBUG] [web] apiExportManifest: app=%s display=%s size=%d",
		manifest.AppName, manifest.DisplayName, manifest.TotalSizeBytes)
	jsonResponse(w, map[string]interface{}{
		"ok":       true,
		"manifest": manifest,
	})
}

// apiImportStart starts an async import.
func (s *Server) apiImportStart(w http.ResponseWriter, r *http.Request) {
	if s.appExporter == nil {
		jsonError(w, "App import not available", http.StatusServiceUnavailable)
		return
	}

	var req struct {
		Path     string `json:"path"`
		Password string `json:"password"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		s.logger.Printf("[DEBUG] [web] apiImportStart: invalid body: %v", err)
		jsonError(w, "Invalid request body", http.StatusBadRequest)
		return
	}

	s.logger.Printf("[DEBUG] [web] apiImportStart: path=%q hasPassword=%v", req.Path, req.Password != "")

	if req.Path == "" {
		jsonError(w, "Missing path", http.StatusBadRequest)
		return
	}

	if !s.isValidExportPath(req.Path) {
		s.logger.Printf("[DEBUG] [web] apiImportStart: invalid path %q", req.Path)
		jsonError(w, "Invalid bundle path", http.StatusBadRequest)
		return
	}

	err := s.appExporter.StartImport(appexport.ImportRequest{
		FABPath:  req.Path,
		Password: req.Password,
	})
	if err != nil {
		s.logger.Printf("[ERROR] [web] Import start failed for %s: %v", req.Path, err)
		s.logger.Printf("[DEBUG] [web] apiImportStart error: %v", err)
		jsonError(w, err.Error(), http.StatusConflict)
		return
	}

	s.logger.Printf("[INFO] [web] Import started from %s", req.Path)
	jsonResponse(w, map[string]interface{}{"ok": true})
}

// apiImportStatus returns current import job status (same as export status).
func (s *Server) apiImportStatus(w http.ResponseWriter, r *http.Request) {
	s.apiExportStatus(w, r)
}

// storageDriveList converts settings StoragePaths to appexport DrivePathInfo.
func (s *Server) storageDriveList() []appexport.DrivePathInfo {
	paths := s.settings.GetStoragePaths()
	drives := make([]appexport.DrivePathInfo, 0, len(paths))
	for _, sp := range paths {
		drives = append(drives, appexport.DrivePathInfo{
			Path:  sp.Path,
			Label: sp.Label,
		})
	}
	return drives
}

// isValidDrivePath checks if a path is a registered storage path.
func (s *Server) isValidDrivePath(path string) bool {
	for _, sp := range s.settings.GetStoragePaths() {
		if sp.Path == path {
			return true
		}
	}
	return false
}

// isValidExportPath checks if a file path is within a registered exports directory.
func (s *Server) isValidExportPath(filePath string) bool {
	cleanPath := filepath.Clean(filePath)
	for _, sp := range s.settings.GetStoragePaths() {
		exportDir := appexport.ExportDir(sp.Path)
		if strings.HasPrefix(cleanPath, filepath.Clean(exportDir)+string(filepath.Separator)) {
			return true
		}
	}
	return false
}
