package handlers

import (
	"errors"
	"io"
	"log"
	"mime"
	"net/http"
	"os"
	"strconv"
	"strings"

	agentfiles "zlh-agent/internal/files"
	"zlh-agent/internal/state"
)

func filesLogf(cfg *state.Config, format string, args ...any) {
	log.Printf("[files] vmid=%d type=%s "+format, append([]any{cfg.VMID, cfg.ContainerType}, args...)...)
}

func HandleGameFilesList(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		writeJSONError(w, http.StatusMethodNotAllowed, "GET only")
		return
	}

	_, serverRoot, ok := requireFileContainer(w)
	if !ok {
		return
	}

	resp, err := agentfiles.List(serverRoot, r.URL.Query().Get("path"))
	if err != nil {
		writeFilesError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, resp)
}

func HandleGameFilesStat(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		writeJSONError(w, http.StatusMethodNotAllowed, "GET only")
		return
	}

	cfg, serverRoot, ok := requireFileContainer(w)
	if !ok {
		return
	}

	resp, err := agentfiles.Stat(cfg.ContainerType, serverRoot, r.URL.Query().Get("path"))
	if err != nil {
		writeFilesError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, resp)
}

func HandleGameFilesRead(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		writeJSONError(w, http.StatusMethodNotAllowed, "GET only")
		return
	}

	_, serverRoot, ok := requireFileContainer(w)
	if !ok {
		return
	}

	resp, err := agentfiles.Read(serverRoot, r.URL.Query().Get("path"))
	if err != nil {
		writeFilesError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, resp)
}

func HandleGameFilesDownload(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		writeJSONError(w, http.StatusMethodNotAllowed, "GET only")
		return
	}

	_, serverRoot, ok := requireFileContainer(w)
	if !ok {
		return
	}

	file, info, name, err := agentfiles.OpenDownload(serverRoot, r.URL.Query().Get("path"))
	if err != nil {
		writeFilesError(w, err)
		return
	}
	defer file.Close()

	w.Header().Set("Content-Length", strconv.FormatInt(info.Size(), 10))
	w.Header().Set("Content-Disposition", "attachment; filename=\""+name+"\"")
	http.ServeContent(w, r, name, info.ModTime(), file)
}

func HandleGameFilesRoot(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case http.MethodDelete:
		handleGameFilesDelete(w, r)
	case http.MethodPut:
		handleGameFilesWrite(w, r)
	default:
		writeJSONError(w, http.StatusMethodNotAllowed, "PUT or DELETE only")
	}
}

func HandleGameFilesUpload(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		writeJSONError(w, http.StatusMethodNotAllowed, "POST only")
		return
	}
	endOp, ok := beginHandlerOperation(w, "file_upload", false, "uploading file")
	if !ok {
		return
	}
	defer endOp()

	cfg, serverRoot, ok := requireFileContainer(w)
	if !ok {
		return
	}

	mediaType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
	if err != nil || !strings.HasPrefix(mediaType, "multipart/") {
		writeJSONError(w, http.StatusBadRequest, "multipart/form-data required")
		return
	}

	normalizedPath, err := agentfiles.NormalizeVisiblePath(r.URL.Query().Get("path"))
	if err != nil {
		writeFilesError(w, err)
		return
	}
	if isVanillaModLocked(cfg) && isModsPath(normalizedPath) {
		filesLogf(cfg, "action=upload path=%s overwrite=%t status=denied reason=vanilla_mod_policy", normalizedPath, strings.EqualFold(r.URL.Query().Get("overwrite"), "true"))
		log.Printf("[mods] vmid=%d enforcement=vanilla user_mods_blocked=true", cfg.VMID)
		writeJSONError(w, http.StatusForbidden, "Vanilla servers do not support user-installed mods")
		return
	}

	overwrite := strings.EqualFold(r.URL.Query().Get("overwrite"), "true")
	mr, err := r.MultipartReader()
	if err != nil {
		writeJSONError(w, http.StatusBadRequest, "invalid multipart body")
		return
	}
	for {
		part, err := mr.NextPart()
		if err == io.EOF {
			writeJSONError(w, http.StatusBadRequest, "missing file part")
			return
		}
		if err != nil {
			writeJSONError(w, http.StatusBadRequest, "invalid multipart body")
			return
		}
		if part.FormName() != "file" {
			part.Close()
			continue
		}

		size, overwritten, err := agentfiles.Upload(cfg.ContainerType, serverRoot, normalizedPath, part, 0, overwrite)
		part.Close()
		if err != nil {
			filesLogf(cfg, "action=upload path=%s overwrite=%t status=failed err=%v", normalizedPath, overwrite, err)
			writeFilesError(w, err)
			return
		}
		filesLogf(cfg, "action=upload path=%s size=%d overwritten=%t status=ok", normalizedPath, size, overwritten)
		writeJSON(w, http.StatusOK, map[string]any{
			"uploaded":    true,
			"path":        normalizedPath,
			"size":        size,
			"overwritten": overwritten,
		})
		return
	}
}

func HandleGameFilesRevert(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		writeJSONError(w, http.StatusMethodNotAllowed, "POST only")
		return
	}
	endOp, ok := beginHandlerOperation(w, "file_revert", false, "reverting file")
	if !ok {
		return
	}
	defer endOp()

	cfg, serverRoot, ok := requireFileContainer(w)
	if !ok {
		return
	}

	revertedPath, err := agentfiles.Revert(cfg.ContainerType, serverRoot, r.URL.Query().Get("path"))
	if err != nil {
		filesLogf(cfg, "action=revert path=%s status=failed err=%v", r.URL.Query().Get("path"), err)
		writeFilesError(w, err)
		return
	}
	filesLogf(cfg, "action=revert path=%s status=ok", revertedPath)
	writeJSON(w, http.StatusOK, map[string]any{
		"reverted": true,
		"path":     revertedPath,
	})
}

func handleGameFilesDelete(w http.ResponseWriter, r *http.Request) {
	endOp, ok := beginHandlerOperation(w, "file_delete", false, "deleting file")
	if !ok {
		return
	}
	defer endOp()

	cfg, serverRoot, ok := requireFileContainer(w)
	if !ok {
		return
	}

	deletedPath, err := agentfiles.Delete(cfg.ContainerType, serverRoot, r.URL.Query().Get("path"))
	if err != nil {
		filesLogf(cfg, "action=delete path=%s status=failed err=%v", r.URL.Query().Get("path"), err)
		writeFilesError(w, err)
		return
	}
	filesLogf(cfg, "action=delete path=%s status=ok", deletedPath)
	writeJSON(w, http.StatusOK, map[string]any{
		"deleted": true,
		"path":    deletedPath,
	})
}

func handleGameFilesWrite(w http.ResponseWriter, r *http.Request) {
	endOp, ok := beginHandlerOperation(w, "file_write", false, "writing file")
	if !ok {
		return
	}
	defer endOp()

	cfg, serverRoot, ok := requireFileContainer(w)
	if !ok {
		return
	}

	normalizedPath, err := agentfiles.NormalizeVisiblePath(r.URL.Query().Get("path"))
	if err != nil {
		writeFilesError(w, err)
		return
	}

	limitedBody := io.LimitReader(r.Body, agentfiles.MaxWriteSize+1)
	data, err := io.ReadAll(limitedBody)
	if err != nil {
		writeJSONError(w, http.StatusBadRequest, "invalid request body")
		return
	}
	backupCreated, err := agentfiles.Write(cfg.ContainerType, serverRoot, normalizedPath, data)
	if err != nil {
		filesLogf(cfg, "action=write path=%s size=%d status=failed err=%v", normalizedPath, len(data), err)
		writeFilesError(w, err)
		return
	}
	filesLogf(cfg, "action=write path=%s size=%d backup_created=%t status=ok", normalizedPath, len(data), backupCreated)
	writeJSON(w, http.StatusOK, map[string]any{
		"saved":         true,
		"path":          normalizedPath,
		"backupCreated": backupCreated,
	})
}

func writeFilesError(w http.ResponseWriter, err error) {
	switch {
	case errors.Is(err, agentfiles.ErrInvalidPath):
		writeJSONError(w, http.StatusBadRequest, err.Error())
	case errors.Is(err, agentfiles.ErrPathEscape), errors.Is(err, agentfiles.ErrForbiddenPath):
		writeJSONError(w, http.StatusForbidden, err.Error())
	case errors.Is(err, agentfiles.ErrDeleteDenied), errors.Is(err, agentfiles.ErrWriteDenied), errors.Is(err, agentfiles.ErrUploadDenied):
		writeJSONError(w, http.StatusForbidden, err.Error())
	case errors.Is(err, agentfiles.ErrAlreadyExists):
		writeJSONError(w, http.StatusConflict, err.Error())
	case errors.Is(err, agentfiles.ErrNotDir), errors.Is(err, agentfiles.ErrNotFile), errors.Is(err, agentfiles.ErrBinaryFile):
		writeJSONError(w, http.StatusBadRequest, err.Error())
	case errors.Is(err, agentfiles.ErrTooLarge), errors.Is(err, agentfiles.ErrWriteTooLarge):
		writeJSONError(w, http.StatusRequestEntityTooLarge, err.Error())
	case errors.Is(err, os.ErrNotExist), errors.Is(err, http.ErrMissingFile):
		writeJSONError(w, http.StatusNotFound, "path not found")
	default:
		writeJSONError(w, http.StatusInternalServerError, err.Error())
	}
}

func requireFileContainer(w http.ResponseWriter) (*state.Config, string, bool) {
	cfg, err := state.LoadConfig()
	if err != nil {
		writeJSONError(w, http.StatusBadRequest, "no config loaded")
		return nil, "", false
	}

	switch strings.ToLower(cfg.ContainerType) {
	case "game", "dev":
		return cfg, agentfiles.RuntimeRoot(cfg), true
	default:
		writeJSONError(w, http.StatusBadRequest, "unsupported container type")
		return nil, "", false
	}
}
