package api

import (
	"errors"
	"log/slog"
	"net/http"
	"strings"

	"github.com/go-chi/chi/v5"

	"github.com/alexei/tinyforge/internal/store"
)

// workloadVolumeRequest is the body shape accepted by the upsert
// endpoint. Defaults to scope=absolute when unset.
type workloadVolumeRequest struct {
	Source string `json:"source"`
	Target string `json:"target"`
	Scope  string `json:"scope"`
	Name   string `json:"name"`
}

// scopeInfo carries one volume scope plus its operator-facing description.
// The UI uses NeedsName to decide whether to show the name input.
type scopeInfo struct {
	Scope       string `json:"scope"`
	Description string `json:"description"`
	NeedsName   bool   `json:"needs_name"`
	PathExample string `json:"path_example"`
}

// listVolumeScopes handles GET /api/volumes/scopes. Returns the catalogue
// of supported volume scopes so the workload-volume editor can render
// scope-specific help text without baking the list into the frontend.
func (s *Server) listVolumeScopes(w http.ResponseWriter, r *http.Request) {
	scopes := []scopeInfo{
		{
			Scope:       "instance",
			Description: "Each deploy gets its own isolated directory keyed by image tag.",
			NeedsName:   false,
			PathExample: "{base}/{workload}/instance-{tag}/{source}",
		},
		{
			Scope:       "stage",
			Description: "Shared across all instances of this workload (alias of project scope).",
			NeedsName:   false,
			PathExample: "{base}/{workload}/{source}",
		},
		{
			Scope:       "project",
			Description: "Shared across all instances of this workload.",
			NeedsName:   false,
			PathExample: "{base}/{workload}/{source}",
		},
		{
			Scope:       "project_named",
			Description: "A named volume within the workload — multiple mounts can share the name.",
			NeedsName:   true,
			PathExample: "{base}/{workload}/_named/{name}/{source}",
		},
		{
			Scope:       "named",
			Description: "Globally named volume shared across workloads (e.g. shared databases).",
			NeedsName:   true,
			PathExample: "{base}/_named/{name}/{source}",
		},
		{
			Scope:       "ephemeral",
			Description: "In-memory tmpfs mount. Fast but data is lost when the container stops.",
			NeedsName:   false,
			PathExample: "(tmpfs — no host path)",
		},
		{
			Scope:       "absolute",
			Description: "Direct host path. Must be under an allowed path configured in settings.",
			NeedsName:   false,
			PathExample: "/mnt/nfs/data (must match allowed paths)",
		},
	}
	respondJSON(w, http.StatusOK, scopes)
}

func (s *Server) listWorkloadVolumes(w http.ResponseWriter, r *http.Request) {
	id := chi.URLParam(r, "id")
	if _, err := s.store.GetWorkloadByID(id); err != nil {
		if errors.Is(err, store.ErrNotFound) {
			respondNotFound(w, "workload")
			return
		}
		respondError(w, http.StatusInternalServerError, "get workload")
		return
	}
	rows, err := s.store.ListWorkloadVolumes(id)
	if err != nil {
		respondError(w, http.StatusInternalServerError, "list workload volumes")
		return
	}
	respondJSON(w, http.StatusOK, rows)
}

func (s *Server) setWorkloadVolume(w http.ResponseWriter, r *http.Request) {
	id := chi.URLParam(r, "id")
	if _, err := s.store.GetWorkloadByID(id); err != nil {
		if errors.Is(err, store.ErrNotFound) {
			respondNotFound(w, "workload")
			return
		}
		respondError(w, http.StatusInternalServerError, "get workload")
		return
	}
	var req workloadVolumeRequest
	if !decodeJSONStrict(w, r, &req) {
		return
	}
	req.Target = strings.TrimSpace(req.Target)
	if req.Target == "" {
		respondError(w, http.StatusBadRequest, "target is required")
		return
	}
	if !strings.HasPrefix(req.Target, "/") {
		respondError(w, http.StatusBadRequest, "target must be an absolute container path")
		return
	}
	if strings.Contains(req.Target, "..") {
		respondError(w, http.StatusBadRequest, "target may not contain path traversal segments")
		return
	}
	scope := req.Scope
	if scope == "" {
		scope = string(store.VolumeScopeAbsolute)
	}
	if !store.IsValidVolumeScope(scope) {
		respondError(w, http.StatusBadRequest, "invalid scope")
		return
	}
	// Absolute-scope mounts must reference a real host path; allow-list
	// enforcement happens at deploy time against settings.AllowedVolumePaths.
	if scope == string(store.VolumeScopeAbsolute) {
		if strings.TrimSpace(req.Source) == "" {
			respondError(w, http.StatusBadRequest, "source is required for absolute scope")
			return
		}
		if strings.Contains(req.Source, "..") {
			respondError(w, http.StatusBadRequest, "source may not contain path traversal segments")
			return
		}
	}
	row, err := s.store.SetWorkloadVolume(store.WorkloadVolume{
		WorkloadID: id,
		Source:     req.Source,
		Target:     req.Target,
		Scope:      scope,
		Name:       req.Name,
	})
	if err != nil {
		slog.Error("set workload volume", "workload", id, "target", req.Target, "error", err)
		respondError(w, http.StatusInternalServerError, "set workload volume")
		return
	}
	respondJSON(w, http.StatusOK, row)
}

func (s *Server) deleteWorkloadVolume(w http.ResponseWriter, r *http.Request) {
	volID := chi.URLParam(r, "volID")
	if err := s.store.DeleteWorkloadVolume(volID); err != nil {
		if errors.Is(err, store.ErrNotFound) {
			respondNotFound(w, "workload volume")
			return
		}
		respondError(w, http.StatusInternalServerError, "delete workload volume")
		return
	}
	respondJSON(w, http.StatusOK, map[string]string{"deleted": volID})
}
