package api

import (
	"errors"
	"log/slog"
	"net/http"
	"path/filepath"
	"regexp"
	"strings"

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

	"github.com/alexei/docker-watcher/internal/store"
	"github.com/alexei/docker-watcher/internal/volume"
)

// safeNamePattern restricts volume names to alphanumeric, dash, underscore, and dot.
var safeNamePattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]*$`)

// validateVolumePath checks that the source path does not contain path traversal.
func validateVolumePath(source string) bool {
	cleaned := filepath.Clean(source)
	return !strings.Contains(cleaned, "..")
}

// volumeRequest is the expected JSON body for creating/updating a volume.
type volumeRequest struct {
	Source string `json:"source"`
	Target string `json:"target"`
	Mode   string `json:"mode,omitempty"` // legacy — ignored if scope is set
	Scope  string `json:"scope"`
	Name   string `json:"name"`
}

// validScopes lists all accepted scope values.
var validScopes = map[string]bool{
	"instance": true, "stage": true, "project": true,
	"project_named": true, "named": true, "ephemeral": true,
	"absolute": true,
}

// validateVolumeScope validates the scope, name, and source combination.
func validateVolumeScope(scope, name, source, allowedPathsJSON string) string {
	if !validScopes[scope] {
		return "scope must be one of: instance, stage, project, project_named, named, ephemeral, absolute"
	}
	if (scope == "project_named" || scope == "named") && strings.TrimSpace(name) == "" {
		return "name is required for " + scope + " scope"
	}
	if name != "" && !safeNamePattern.MatchString(name) {
		return "name must start with a letter or digit and contain only letters, digits, dashes, underscores, or dots"
	}
	if scope == "absolute" {
		if source == "" {
			return "source path is required for absolute scope"
		}
		if !filepath.IsAbs(source) {
			return "absolute scope requires an absolute source path"
		}
		// Validate against allowlist.
		allowed, err := volume.ParseAllowedPaths(allowedPathsJSON)
		if err != nil {
			return "failed to parse allowed volume paths"
		}
		if len(allowed) == 0 {
			return "absolute volume paths are disabled — configure allowed paths in settings first"
		}
		matched := false
		cleanSource := filepath.Clean(source)
		for _, prefix := range allowed {
			cleanPrefix := filepath.Clean(prefix)
			if strings.HasPrefix(cleanSource, cleanPrefix+string(filepath.Separator)) || cleanSource == cleanPrefix {
				matched = true
				break
			}
		}
		if !matched {
			return "source path is not under any allowed volume path"
		}
	}
	return ""
}

// scopeDescriptions returns metadata about each scope for the UI.
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 all available scopes with descriptions for UI hints.
func (s *Server) listVolumeScopes(w http.ResponseWriter, r *http.Request) {
	scopes := []scopeInfo{
		{
			Scope:       "instance",
			Description: "Each deploy gets its own isolated directory. Data is not shared between deploys.",
			NeedsName:   false,
			PathExample: "{base}/{project}/{stage}-{tag}/{source}",
		},
		{
			Scope:       "stage",
			Description: "All deploys within the same stage share this volume. Data persists across blue-green deployments.",
			NeedsName:   false,
			PathExample: "{base}/{project}/{stage}/{source}",
		},
		{
			Scope:       "project",
			Description: "Shared across all stages of the project. Good for common config or shared assets.",
			NeedsName:   false,
			PathExample: "{base}/{project}/{source}",
		},
		{
			Scope:       "project_named",
			Description: "A named volume within the project. Multiple stages can reference the same name to share data selectively.",
			NeedsName:   true,
			PathExample: "{base}/{project}/_named/{name}/{source}",
		},
		{
			Scope:       "named",
			Description: "A globally named volume shared across projects. Use for cross-project resources like 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. Good for temp files and caches.",
			NeedsName:   false,
			PathExample: "(tmpfs — no host path)",
		},
		{
			Scope:       "absolute",
			Description: "Direct host path. Must be under an allowed path configured in settings. Use for external mounts like NFS or pre-existing directories.",
			NeedsName:   false,
			PathExample: "/mnt/nfs/data (must match allowed paths)",
		},
	}
	respondJSON(w, http.StatusOK, scopes)
}

// listVolumes handles GET /api/projects/{id}/volumes.
func (s *Server) listVolumes(w http.ResponseWriter, r *http.Request) {
	projectID := chi.URLParam(r, "id")

	if _, err := s.store.GetProjectByID(projectID); err != nil {
		if errors.Is(err, store.ErrNotFound) {
			respondNotFound(w, "project")
			return
		}
		slog.Error("failed to get project", "project_id", projectID, "error", err)
		respondError(w, http.StatusInternalServerError, "failed to get project")
		return
	}

	vols, err := s.store.GetVolumesByProjectID(projectID)
	if err != nil {
		slog.Error("failed to list volumes", "project_id", projectID, "error", err)
		respondError(w, http.StatusInternalServerError, "failed to list volumes")
		return
	}

	respondJSON(w, http.StatusOK, vols)
}

// createVolume handles POST /api/projects/{id}/volumes.
func (s *Server) createVolume(w http.ResponseWriter, r *http.Request) {
	projectID := chi.URLParam(r, "id")

	if _, err := s.store.GetProjectByID(projectID); err != nil {
		if errors.Is(err, store.ErrNotFound) {
			respondNotFound(w, "project")
			return
		}
		slog.Error("failed to get project", "project_id", projectID, "error", err)
		respondError(w, http.StatusInternalServerError, "failed to get project")
		return
	}

	var req volumeRequest
	if !decodeJSON(w, r, &req) {
		return
	}

	// Ephemeral volumes don't need a source path.
	if req.Scope != "ephemeral" {
		if req.Source == "" {
			respondError(w, http.StatusBadRequest, "source is required")
			return
		}
		if !validateVolumePath(req.Source) {
			respondError(w, http.StatusBadRequest, "source path must not contain '..'")
			return
		}
	}
	if req.Target == "" {
		respondError(w, http.StatusBadRequest, "target is required")
		return
	}
	if !validateVolumePath(req.Target) {
		respondError(w, http.StatusBadRequest, "target path must not contain '..'")
		return
	}

	// Resolve scope — support legacy mode field.
	scope := req.Scope
	if scope == "" {
		switch req.Mode {
		case "isolated":
			scope = "instance"
		case "shared":
			scope = "project"
		default:
			scope = "project"
		}
	}

	// Fetch settings for absolute path allowlist validation.
	settings, err := s.store.GetSettings()
	if err != nil {
		slog.Error("failed to get settings for volume validation", "error", err)
		respondError(w, http.StatusInternalServerError, "failed to validate volume")
		return
	}

	if errMsg := validateVolumeScope(scope, req.Name, req.Source, settings.AllowedVolumePaths); errMsg != "" {
		respondError(w, http.StatusBadRequest, errMsg)
		return
	}

	vol, err := s.store.CreateVolume(store.Volume{
		ProjectID: projectID,
		Source:    req.Source,
		Target:   req.Target,
		Scope:    scope,
		Name:     strings.TrimSpace(req.Name),
	})
	if err != nil {
		slog.Error("failed to create volume", "project_id", projectID, "error", err)
		respondError(w, http.StatusInternalServerError, "failed to create volume")
		return
	}
	respondJSON(w, http.StatusCreated, vol)
}

// updateVolume handles PUT /api/projects/{id}/volumes/{volId}.
func (s *Server) updateVolume(w http.ResponseWriter, r *http.Request) {
	volID := chi.URLParam(r, "volId")

	existing, err := s.store.GetVolumeByID(volID)
	if err != nil {
		if errors.Is(err, store.ErrNotFound) {
			respondNotFound(w, "volume")
			return
		}
		slog.Error("failed to get volume", "volume_id", volID, "error", err)
		respondError(w, http.StatusInternalServerError, "failed to get volume")
		return
	}

	var req volumeRequest
	if !decodeJSON(w, r, &req) {
		return
	}

	updated := existing
	if req.Source != "" {
		if !validateVolumePath(req.Source) {
			respondError(w, http.StatusBadRequest, "source path must not contain '..'")
			return
		}
		updated.Source = req.Source
	}
	if req.Target != "" {
		if !validateVolumePath(req.Target) {
			respondError(w, http.StatusBadRequest, "target path must not contain '..'")
			return
		}
		updated.Target = req.Target
	}
	if req.Scope != "" {
		settings, err := s.store.GetSettings()
		if err != nil {
			slog.Error("failed to get settings for volume validation", "error", err)
			respondError(w, http.StatusInternalServerError, "failed to validate volume")
			return
		}
		source := updated.Source
		if req.Source != "" {
			source = req.Source
		}
		if errMsg := validateVolumeScope(req.Scope, req.Name, source, settings.AllowedVolumePaths); errMsg != "" {
			respondError(w, http.StatusBadRequest, errMsg)
			return
		}
		updated.Scope = req.Scope
		updated.Name = strings.TrimSpace(req.Name)
	}

	// Non-ephemeral scopes require a source path.
	if updated.Scope != "ephemeral" && updated.Source == "" {
		respondError(w, http.StatusBadRequest, "source is required for non-ephemeral scopes")
		return
	}

	if err := s.store.UpdateVolume(updated); err != nil {
		slog.Error("failed to update volume", "volume_id", volID, "error", err)
		respondError(w, http.StatusInternalServerError, "failed to update volume")
		return
	}
	respondJSON(w, http.StatusOK, updated)
}

// deleteVolume handles DELETE /api/projects/{id}/volumes/{volId}.
func (s *Server) deleteVolume(w http.ResponseWriter, r *http.Request) {
	volID := chi.URLParam(r, "volId")
	if err := s.store.DeleteVolume(volID); err != nil {
		if errors.Is(err, store.ErrNotFound) {
			respondNotFound(w, "volume")
			return
		}
		slog.Error("failed to delete volume", "volume_id", volID, "error", err)
		respondError(w, http.StatusInternalServerError, "failed to delete volume")
		return
	}
	respondJSON(w, http.StatusOK, map[string]string{"deleted": volID})
}
