package handlers

import (
	"errors"
	"fmt"
	"net/http"
	"path/filepath"
	"strings"
	"time"

	"{{GO_MODULE}}/pkg/app"
	"{{GO_MODULE}}/pkg/auth"
	"{{GO_MODULE}}/pkg/httperror"
	"{{GO_MODULE}}/pkg/httpresponse"
	"{{GO_MODULE}}/pkg/logging"
	"{{GO_MODULE}}/pkg/storage"
	"{{GO_MODULE}}/services/{{COMPONENT_NAME}}/internal/domain"
	"{{GO_MODULE}}/services/{{COMPONENT_NAME}}/internal/port"

	"github.com/go-chi/chi/v5"
	"github.com/google/uuid"
)

// maxUploadSize is the maximum allowed file size for uploads (500MB).
const maxUploadSize = 500 << 20

// allowedMediaTypes is the allowlist of MIME types permitted for upload.
var allowedMediaTypes = map[string]bool{
	"image/jpeg":      true,
	"image/png":       true,
	"image/gif":       true,
	"image/webp":      true,
	"image/svg+xml":   true,
	"video/mp4":       true,
	"video/webm":      true,
	"video/quicktime": true,
	"audio/mpeg":      true,
	"audio/wav":       true,
	"audio/ogg":       true,
	"audio/webm":      true,
	"application/pdf": true,
}

// Media handles media upload and library operations.
type Media struct {
	store  storage.Store
	repo   port.MediaRepository
	logger *logging.Logger
}

// NewMedia creates a new media handler.
func NewMedia(store storage.Store, repo port.MediaRepository, logger *logging.Logger) *Media {
	return &Media{store: store, repo: repo, logger: logger.WithComponent("MediaHandler")}
}

// Routes returns the media subrouter.
func (h *Media) Routes() http.Handler {
	r := chi.NewRouter()
	r.Post("/upload/init", app.Wrap(h.InitUpload))
	r.Post("/upload/complete", app.Wrap(h.CompleteUpload))
	r.Get("/", app.Wrap(h.List))
	r.Get("/{id}", app.Wrap(h.GetOne))
	r.Get("/{id}/url", app.Wrap(h.RefreshURL))
	r.Delete("/{id}", app.Wrap(h.Delete))
	return r
}

// sanitizeFilename removes path separators and dangerous characters from filenames.
func sanitizeFilename(name string) string {
	// Remove any directory components
	name = filepath.Base(name)
	// Replace any remaining path separators (e.g., from URL encoding)
	name = strings.ReplaceAll(name, "/", "_")
	name = strings.ReplaceAll(name, "\\", "_")
	name = strings.ReplaceAll(name, "..", "_")
	// Remove null bytes
	name = strings.ReplaceAll(name, "\x00", "")
	if name == "" || name == "." {
		name = "unnamed"
	}
	return name
}

// initUploadRequest is the request body for POST /media/upload/init.
type initUploadRequest struct {
	Filename    string `json:"filename" validate:"required"`
	ContentType string `json:"contentType" validate:"required"`
	Size        int64  `json:"size"`
}

// InitUpload returns a presigned URL for direct client-to-storage upload.
// The metadata record is created in CompleteUpload after the file is actually stored.
func (h *Media) InitUpload(w http.ResponseWriter, r *http.Request) error {
	var req initUploadRequest
	if err := app.BindAndValidate(r, &req); err != nil {
		return err
	}

	// Validate MIME type against allowlist
	if !allowedMediaTypes[req.ContentType] {
		return httperror.BadRequest("unsupported file type: " + req.ContentType)
	}

	// Validate file size if provided
	if req.Size > maxUploadSize {
		return httperror.BadRequest(fmt.Sprintf("file too large: %d bytes (max %d)", req.Size, maxUploadSize))
	}

	user := auth.GetUser(r.Context())
	if user == nil {
		return httperror.Unauthorized("authentication required")
	}

	// Sanitize filename to prevent path traversal
	safeName := sanitizeFilename(req.Filename)

	// Build object path: media/{userID}/{uuid}/{filename}
	objectPath := fmt.Sprintf("media/%s/%s/%s", user.ID, uuid.New().String(), safeName)

	presigned, err := h.store.UploadPresigned(r.Context(), objectPath, req.ContentType)
	if err != nil {
		h.logger.Error("failed to create presigned upload", "error", err)
		return httperror.Internal("failed to create upload URL")
	}

	httpresponse.OK(w, r, map[string]any{
		"uploadURL":  presigned.URL,
		"objectPath": objectPath,
		"filename":   safeName,
		"headers":    presigned.Headers,
		"method":     presigned.Method,
		"expires":    presigned.Expires,
	})
	return nil
}

// completeUploadRequest is the request body for POST /media/upload/complete.
type completeUploadRequest struct {
	ObjectPath  string `json:"objectPath" validate:"required"`
	Filename    string `json:"filename"`
	ContentType string `json:"contentType"`
	Size        int64  `json:"size"`
}

// CompleteUpload confirms an upload is done, creates the metadata record, and returns the final URL.
func (h *Media) CompleteUpload(w http.ResponseWriter, r *http.Request) error {
	var req completeUploadRequest
	if err := app.BindAndValidate(r, &req); err != nil {
		return err
	}

	// Verify the object path belongs to the authenticated user
	user := auth.GetUser(r.Context())
	if user == nil {
		return httperror.Unauthorized("authentication required")
	}
	expectedPrefix := fmt.Sprintf("media/%s/", user.ID)
	if !strings.HasPrefix(req.ObjectPath, expectedPrefix) {
		return httperror.Forbidden("cannot complete upload for another user's media")
	}

	url, err := h.store.GetURL(r.Context(), req.ObjectPath)
	if err != nil {
		h.logger.Error("failed to get object URL", "error", err, "path", req.ObjectPath)
		return httperror.Internal("failed to confirm upload")
	}

	// Create the metadata record now that the file is in storage.
	now := time.Now()
	filename := sanitizeFilename(req.Filename)
	if filename == "unnamed" {
		// Extract filename from the object path (last segment)
		parts := strings.Split(req.ObjectPath, "/")
		if len(parts) > 0 {
			filename = parts[len(parts)-1]
		}
	}

	mediaObj := &domain.MediaObject{
		ID:          domain.MediaObjectID("med_" + uuid.New().String()),
		UserID:      domain.UserID(user.ID),
		Path:        req.ObjectPath,
		Filename:    filename,
		ContentType: req.ContentType,
		Size:        req.Size,
		CreatedAt:   now,
		UpdatedAt:   now,
	}
	if err := h.repo.Create(r.Context(), mediaObj); err != nil {
		h.logger.Error("failed to create media record", "error", err)
		return httperror.Internal("failed to create upload record")
	}

	httpresponse.OK(w, r, map[string]any{
		"id":   string(mediaObj.ID),
		"url":  url,
		"path": req.ObjectPath,
	})
	return nil
}

// List returns the user's media objects with pagination.
func (h *Media) List(w http.ResponseWriter, r *http.Request) error {
	user := auth.GetUser(r.Context())
	if user == nil {
		return httperror.Unauthorized("authentication required")
	}

	opts := port.ListMediaOptions{
		ContentTypePrefix: r.URL.Query().Get("type"),
		Limit:             intQueryParam(r, "limit", 50),
		Offset:            intQueryParam(r, "offset", 0),
	}

	objects, total, err := h.repo.ListByUser(r.Context(), domain.UserID(user.ID), opts)
	if err != nil {
		h.logger.Error("failed to list media", "error", err)
		return httperror.Internal("failed to list media")
	}

	// Enrich each object with a fresh signed URL
	type mediaItem struct {
		ID          string    `json:"id"`
		Path        string    `json:"path"`
		URL         string    `json:"url"`
		Filename    string    `json:"filename"`
		ContentType string    `json:"contentType"`
		Size        int64     `json:"size"`
		CreatedAt   time.Time `json:"createdAt"`
	}

	items := make([]mediaItem, 0, len(objects))
	for _, obj := range objects {
		url, urlErr := h.store.GetURL(r.Context(), obj.Path)
		if urlErr != nil {
			h.logger.Warn("failed to get URL for media object", "path", obj.Path, "error", urlErr)
			continue
		}
		items = append(items, mediaItem{
			ID:          string(obj.ID),
			Path:        obj.Path,
			URL:         url,
			Filename:    obj.Filename,
			ContentType: obj.ContentType,
			Size:        obj.Size,
			CreatedAt:   obj.CreatedAt,
		})
	}

	httpresponse.OK(w, r, map[string]any{
		"items": items,
		"total": total,
		"count": len(items),
	})
	return nil
}

// GetOne returns a single media object with a fresh URL.
func (h *Media) GetOne(w http.ResponseWriter, r *http.Request) error {
	id := chi.URLParam(r, "id")
	if id == "" {
		return httperror.BadRequest("media ID is required")
	}

	obj, err := h.repo.Get(r.Context(), domain.MediaObjectID(id))
	if err != nil {
		if errors.Is(err, domain.ErrNotFound) {
			return httperror.NotFound("media object not found")
		}
		return httperror.Internal("failed to get media object")
	}

	// Verify ownership
	user := auth.GetUser(r.Context())
	if user == nil || domain.UserID(user.ID) != obj.UserID {
		return httperror.Forbidden("access denied")
	}

	url, err := h.store.GetURL(r.Context(), obj.Path)
	if err != nil {
		h.logger.Error("failed to get URL", "error", err, "path", obj.Path)
		return httperror.Internal("failed to get media URL")
	}

	httpresponse.OK(w, r, map[string]any{
		"id":          string(obj.ID),
		"path":        obj.Path,
		"url":         url,
		"filename":    obj.Filename,
		"contentType": obj.ContentType,
		"size":        obj.Size,
		"createdAt":   obj.CreatedAt,
	})
	return nil
}

// RefreshURL returns a fresh signed URL for a media object.
func (h *Media) RefreshURL(w http.ResponseWriter, r *http.Request) error {
	id := chi.URLParam(r, "id")
	if id == "" {
		return httperror.BadRequest("media ID is required")
	}

	obj, err := h.repo.Get(r.Context(), domain.MediaObjectID(id))
	if err != nil {
		if errors.Is(err, domain.ErrNotFound) {
			return httperror.NotFound("media object not found")
		}
		return httperror.Internal("failed to get media object")
	}

	// Verify ownership
	user := auth.GetUser(r.Context())
	if user == nil || domain.UserID(user.ID) != obj.UserID {
		return httperror.Forbidden("access denied")
	}

	url, err := h.store.GetURL(r.Context(), obj.Path)
	if err != nil {
		h.logger.Error("failed to refresh URL", "error", err, "path", obj.Path)
		return httperror.Internal("failed to refresh media URL")
	}

	httpresponse.OK(w, r, map[string]any{
		"id":  string(obj.ID),
		"url": url,
	})
	return nil
}

// Delete soft-deletes a media object.
func (h *Media) Delete(w http.ResponseWriter, r *http.Request) error {
	id := chi.URLParam(r, "id")
	if id == "" {
		return httperror.BadRequest("media ID is required")
	}

	obj, err := h.repo.Get(r.Context(), domain.MediaObjectID(id))
	if err != nil {
		if errors.Is(err, domain.ErrNotFound) {
			return httperror.NotFound("media object not found")
		}
		return httperror.Internal("failed to get media object")
	}

	// Verify ownership
	user := auth.GetUser(r.Context())
	if user == nil || domain.UserID(user.ID) != obj.UserID {
		return httperror.Forbidden("cannot delete another user's media")
	}

	if err := h.repo.SoftDelete(r.Context(), domain.MediaObjectID(id)); err != nil {
		h.logger.Error("failed to delete media", "error", err, "id", id)
		return httperror.Internal("failed to delete media")
	}

	httpresponse.OK(w, r, map[string]any{"deleted": id})
	return nil
}

// intQueryParam parses an integer query parameter with a default value.
func intQueryParam(r *http.Request, key string, defaultVal int) int {
	val := r.URL.Query().Get(key)
	if val == "" {
		return defaultVal
	}
	var n int
	if _, err := fmt.Sscanf(val, "%d", &n); err != nil || n < 0 {
		return defaultVal
	}
	return n
}
