package api

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

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

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

// sanitizeFilename removes characters unsafe for Content-Disposition headers.
func sanitizeFilename(name string) string {
	return strings.Map(func(r rune) rune {
		if r == '"' || r == '\\' || r == '\n' || r == '\r' {
			return '_'
		}
		return r
	}, name)
}

const maxUploadSize = 100 * 1024 * 1024 // 100MB

// resolveVolumeRoot looks up a volume and resolves its host path.
func (s *Server) resolveVolumeRoot(w http.ResponseWriter, r *http.Request) (string, bool) {
	projectID := chi.URLParam(r, "id")
	volID := chi.URLParam(r, "volId")

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

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

	// Verify volume belongs to this project.
	if vol.ProjectID != projectID {
		respondNotFound(w, "volume")
		return "", false
	}

	if vol.Scope == "ephemeral" {
		respondError(w, http.StatusBadRequest, "ephemeral volumes have no host path to browse")
		return "", false
	}

	settings, err := s.store.GetSettings()
	if err != nil {
		slog.Error("failed to get settings", "error", err)
		respondError(w, http.StatusInternalServerError, "failed to get settings")
		return "", false
	}

	q := r.URL.Query()
	params := volume.ResolveParams{
		BasePath:           settings.BaseVolumePath,
		ProjectName:        proj.Name,
		StageName:          q.Get("stage"),
		ImageTag:           q.Get("tag"),
		AllowedVolumePaths: settings.AllowedVolumePaths,
	}

	rootPath, err := volume.ResolvePath(vol, params)
	if err != nil {
		respondError(w, http.StatusBadRequest, err.Error())
		return "", false
	}

	return rootPath, true
}

// browseVolume handles GET /api/projects/{id}/volumes/{volId}/browse?path=&stage=&tag=
func (s *Server) browseVolume(w http.ResponseWriter, r *http.Request) {
	rootPath, ok := s.resolveVolumeRoot(w, r)
	if !ok {
		return
	}

	relPath := r.URL.Query().Get("path")
	entries, err := volume.ListDir(rootPath, relPath)
	if err != nil {
		slog.Error("failed to list directory", "root", rootPath, "path", relPath, "error", err)
		respondError(w, http.StatusInternalServerError, "failed to list directory")
		return
	}

	respondJSON(w, http.StatusOK, map[string]any{
		"path":    relPath,
		"entries": entries,
	})
}

// downloadVolume handles GET /api/projects/{id}/volumes/{volId}/download?path=&stage=&tag=
// Downloads a single file directly, or a directory/root as a zip archive.
func (s *Server) downloadVolume(w http.ResponseWriter, r *http.Request) {
	rootPath, ok := s.resolveVolumeRoot(w, r)
	if !ok {
		return
	}

	relPath := r.URL.Query().Get("path")

	// If path is empty or points to a directory, serve as zip.
	if relPath == "" {
		s.serveZip(w, rootPath, "", "volume")
		return
	}

	// Check if it's a file or directory.
	f, info, err := volume.OpenFile(rootPath, relPath)
	if err != nil {
		// Might be a directory — try zip.
		entries, listErr := volume.ListDir(rootPath, relPath)
		if listErr == nil && entries != nil {
			name := filepath.Base(relPath)
			s.serveZip(w, rootPath, relPath, name)
			return
		}
		slog.Error("failed to open file", "root", rootPath, "path", relPath, "error", err)
		respondError(w, http.StatusNotFound, "file not found")
		return
	}
	defer f.Close()

	// Serve single file with forced download.
	safeName := sanitizeFilename(filepath.Base(relPath))
	w.Header().Set("Content-Type", "application/octet-stream")
	w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, safeName))
	w.Header().Set("Content-Length", fmt.Sprintf("%d", info.Size()))
	w.WriteHeader(http.StatusOK)
	io.Copy(w, f)
}

func (s *Server) serveZip(w http.ResponseWriter, rootPath, relPath, name string) {
	safeName := sanitizeFilename(name)
	w.Header().Set("Content-Type", "application/zip")
	w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.zip"`, safeName))
	w.WriteHeader(http.StatusOK)
	if err := volume.WriteZip(rootPath, relPath, w); err != nil {
		slog.Error("failed to write zip", "root", rootPath, "path", relPath, "error", err)
	}
}

// uploadToVolume handles POST /api/projects/{id}/volumes/{volId}/upload?path=&stage=&tag=
// Accepts multipart form uploads. Overrides the global body limit for large files.
func (s *Server) uploadToVolume(w http.ResponseWriter, r *http.Request) {
	// Override the global 1MB body limit for uploads.
	r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize)

	rootPath, ok := s.resolveVolumeRoot(w, r)
	if !ok {
		return
	}

	if err := r.ParseMultipartForm(maxUploadSize); err != nil {
		respondError(w, http.StatusBadRequest, "upload too large (max 100MB)")
		return
	}

	relPath := r.URL.Query().Get("path")
	uploaded := []string{}

	for _, fileHeaders := range r.MultipartForm.File {
		for _, fh := range fileHeaders {
			f, err := fh.Open()
			if err != nil {
				slog.Error("failed to open upload", "filename", fh.Filename, "error", err)
				continue
			}

			// Strip directory components from filename to prevent directory creation attacks.
			targetRel := filepath.Join(relPath, filepath.Base(fh.Filename))
			if err := volume.SaveFile(rootPath, targetRel, f); err != nil {
				f.Close()
				slog.Error("failed to save upload", "filename", fh.Filename, "error", err)
				respondError(w, http.StatusInternalServerError, "failed to save file: "+fh.Filename)
				return
			}
			f.Close()
			uploaded = append(uploaded, fh.Filename)
		}
	}

	respondJSON(w, http.StatusOK, map[string]any{
		"uploaded": uploaded,
		"count":    len(uploaded),
	})
}
