package upload

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"strings"
	"time"

	"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/config"
	"github.com/google/uuid"
	"github.com/minio/minio-go/v7"
	"github.com/minio/minio-go/v7/pkg/credentials"
)

const maxUploadSize = 25 << 20 // 25 MB per file

var allowedExtensions = map[string]bool{
	".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".webp": true,
	".mp3": true, ".wav": true, ".ogg": true, ".flac": true,
	".mp4": true, ".webm": true, ".mov": true,
	".pdf": true, ".txt": true, ".md": true, ".json": true, ".csv": true,
	".zip": true, ".tar": true, ".gz": true,
}

type Handler struct {
	client *minio.Client
	bucket string
}

func NewHandler(cfg *config.Config) (*Handler, error) {
	endpoint := cfg.MinIO.Endpoint
	if endpoint == "" {
		return nil, fmt.Errorf("minio endpoint not configured")
	}

	client, err := minio.New(endpoint, &minio.Options{
		Creds:  credentials.NewStaticV4(cfg.MinIO.AccessKey, cfg.MinIO.SecretKey, ""),
		Secure: cfg.MinIO.UseSSL,
	})
	if err != nil {
		return nil, fmt.Errorf("minio client: %w", err)
	}

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	exists, err := client.BucketExists(ctx, cfg.MinIO.Bucket)
	if err != nil {
		return nil, fmt.Errorf("bucket check: %w", err)
	}
	if !exists {
		if err := client.MakeBucket(ctx, cfg.MinIO.Bucket, minio.MakeBucketOptions{}); err != nil {
			return nil, fmt.Errorf("make bucket: %w", err)
		}
	}

	return &Handler{
		client: client,
		bucket: cfg.MinIO.Bucket,
	}, nil
}

func (h *Handler) Upload(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize)

	if err := r.ParseMultipartForm(maxUploadSize); err != nil {
		http.Error(w, `{"error":"file too large or invalid form"}`, http.StatusBadRequest)
		return
	}

	file, header, err := r.FormFile("file")
	if err != nil {
		http.Error(w, `{"error":"missing file"}`, http.StatusBadRequest)
		return
	}
	defer file.Close()

	// Validate extension against allowlist
	ext := ""
	if idx := strings.LastIndex(header.Filename, "."); idx >= 0 {
		ext = strings.ToLower(header.Filename[idx:])
	}
	if !allowedExtensions[ext] {
		http.Error(w, `{"error":"file type not allowed"}`, http.StatusBadRequest)
		return
	}

	// Server-side content-type detection (blocks HTML/JS that could be used for XSS)
	buf := make([]byte, 512)
	n, _ := file.Read(buf)
	detectedType := http.DetectContentType(buf[:n])
	file.Seek(0, io.SeekStart)

	blockedTypes := []string{"text/html", "application/javascript", "application/x-executable"}
	for _, blocked := range blockedTypes {
		if strings.HasPrefix(detectedType, blocked) {
			http.Error(w, `{"error":"file type not allowed"}`, http.StatusBadRequest)
			return
		}
	}

	contentType := header.Header.Get("Content-Type")
	if contentType == "" {
		contentType = detectedType
	}

	objectName := uuid.New().String() + ext
	ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
	defer cancel()

	info, err := h.client.PutObject(ctx, h.bucket, objectName, file, header.Size, minio.PutObjectOptions{
		ContentType: contentType,
	})
	if err != nil {
		http.Error(w, `{"error":"upload failed"}`, http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	fmt.Fprintf(w, `{"url":"/files/%s","size":%d}`, objectName, info.Size)
}

func (h *Handler) Serve(w http.ResponseWriter, r *http.Request) {
	objectName := strings.TrimPrefix(r.URL.Path, "/files/")
	if objectName == "" {
		http.Error(w, `{"error":"missing file"}`, http.StatusBadRequest)
		return
	}

	// Sanitize: reject path traversal attempts
	if strings.Contains(objectName, "..") || strings.Contains(objectName, "/") {
		http.Error(w, `{"error":"invalid path"}`, http.StatusBadRequest)
		return
	}

	ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
	defer cancel()

	obj, err := h.client.GetObject(ctx, h.bucket, objectName, minio.GetObjectOptions{})
	if err != nil {
		http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
		return
	}
	defer obj.Close()

	stat, err := obj.Stat()
	if err != nil {
		http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
		return
	}

	w.Header().Set("Content-Type", stat.ContentType)
	w.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size))
	// Inline display for images; attachment for everything else
	if strings.HasPrefix(stat.ContentType, "image/") {
		w.Header().Set("Content-Disposition", "inline")
	} else {
		w.Header().Set("Content-Disposition", "attachment")
	}
	io.Copy(w, obj)
}
