package api

import (
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/gin-gonic/gin"

	"github.com/rycroftapparel/workpulse-api/internal/httpapi"
)

const (
	maxReportAttachmentBytes   = 5 << 20 // 5 MB
	maxReportAttachmentsPerRow = 10
)

var allowedReportAttachmentMimes = map[string]string{
	"image/jpeg": ".jpg",
	"image/png":  ".png",
	"image/webp": ".webp",
	"image/gif":  ".gif",
}

func (s *Server) reportAttachmentsDir(reportID uint64) string {
	return filepath.Join(s.Cfg.UploadDir, "reports", fmt.Sprintf("%d", reportID))
}

func (s *Server) reportAttachmentPublicPath(reportID uint64, filename string) string {
	return fmt.Sprintf("/api/v1/uploads/reports/%d/%s", reportID, filename)
}

func newReportAttachmentID() string {
	b := make([]byte, 16)
	if _, err := rand.Read(b); err != nil {
		return fmt.Sprintf("att-%d", os.Getpid())
	}
	return hex.EncodeToString(b)
}

// postReportAttachment accepts multipart field "file" and appends metadata to reports.body.
func (s *Server) postReportAttachment(c *gin.Context) {
	reportID, err := strconv.ParseUint(c.Param("id"), 10, 64)
	if err != nil {
		c.JSON(http.StatusBadRequest, httpapi.Fail("validation", "invalid id"))
		return
	}
	body, _, ok := s.loadReportForBodyMutation(c, reportID)
	if !ok {
		return
	}
	existing := reportAttachmentsFromBody(body)
	if len(existing) >= maxReportAttachmentsPerRow {
		c.JSON(http.StatusBadRequest, httpapi.Fail("validation", fmt.Sprintf("Maksimal %d lampiran per laporan.", maxReportAttachmentsPerRow)))
		return
	}

	file, err := c.FormFile("file")
	if err != nil {
		c.JSON(http.StatusBadRequest, httpapi.Fail("validation", "Pilih file gambar (JPG, PNG, atau WebP)."))
		return
	}
	if file.Size <= 0 || file.Size > maxReportAttachmentBytes {
		c.JSON(http.StatusBadRequest, httpapi.Fail("validation", "Ukuran gambar maksimal 5 MB."))
		return
	}

	src, err := file.Open()
	if err != nil {
		c.JSON(http.StatusBadRequest, httpapi.Fail("validation", "File tidak dapat dibaca."))
		return
	}
	defer src.Close()

	head := make([]byte, 512)
	n, _ := io.ReadFull(src, head)
	head = head[:n]
	ext, okExt := sniffImageExt(head, file.Header.Get("Content-Type"))
	if !okExt {
		c.JSON(http.StatusBadRequest, httpapi.Fail("validation", "Format gambar harus JPG, PNG, WebP, atau GIF."))
		return
	}

	attID := newReportAttachmentID()
	filename := attID + ext
	dir := s.reportAttachmentsDir(reportID)
	if err := os.MkdirAll(dir, 0o755); err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("storage", "Gagal menyiapkan penyimpanan."))
		return
	}
	destPath := filepath.Join(dir, filename)
	dst, err := os.Create(destPath)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("storage", "Gagal menyimpan gambar."))
		return
	}
	if _, err := dst.Write(head); err != nil {
		_ = dst.Close()
		c.JSON(http.StatusInternalServerError, httpapi.Fail("storage", "Gagal menyimpan gambar."))
		return
	}
	if _, err := io.Copy(dst, src); err != nil {
		_ = dst.Close()
		c.JSON(http.StatusInternalServerError, httpapi.Fail("storage", "Gagal menyimpan gambar."))
		return
	}
	_ = dst.Close()

	name := strings.TrimSpace(file.Filename)
	if name == "" {
		name = "screenshot" + ext
	}
	att := reportAttachmentJSON{
		ID:       attID,
		URL:      s.reportAttachmentPublicPath(reportID, filename),
		Name:     name,
		MimeType: mimeFromExt(ext),
		Size:     file.Size,
	}
	existing = append(existing, att)
	newBody, err := reportBodyWithAttachments(body, existing)
	if err != nil {
		_ = os.Remove(destPath)
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", "Gagal memperbarui laporan."))
		return
	}

	ctx, cancel := s.ctx(c)
	defer cancel()
	if _, err := s.DB.ExecContext(ctx, `UPDATE reports SET body = $1 WHERE id = $2`, newBody, reportID); err != nil {
		_ = os.Remove(destPath)
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	if err := s.syncReportCalendarEvent(ctx, reportID); err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	s.Hub.BroadcastEvent("report.updated", gin.H{"id": reportID})
	c.JSON(http.StatusOK, httpapi.OK(gin.H{"attachment": att, "attachments": existing}))
}

func mimeFromExt(ext string) string {
	switch ext {
	case ".jpg", ".jpeg":
		return "image/jpeg"
	case ".png":
		return "image/png"
	case ".webp":
		return "image/webp"
	case ".gif":
		return "image/gif"
	default:
		return "application/octet-stream"
	}
}

// deleteReportAttachment removes a stored file and its metadata from reports.body.
func (s *Server) deleteReportAttachment(c *gin.Context) {
	reportID, err := strconv.ParseUint(c.Param("id"), 10, 64)
	if err != nil {
		c.JSON(http.StatusBadRequest, httpapi.Fail("validation", "invalid id"))
		return
	}
	attID := strings.TrimSpace(c.Param("attachmentId"))
	if attID == "" {
		c.JSON(http.StatusBadRequest, httpapi.Fail("validation", "invalid attachment id"))
		return
	}

	body, _, ok := s.loadReportForBodyMutation(c, reportID)
	if !ok {
		return
	}
	existing := reportAttachmentsFromBody(body)
	var kept []reportAttachmentJSON
	var removed *reportAttachmentJSON
	for i := range existing {
		if existing[i].ID == attID {
			copy := existing[i]
			removed = &copy
			continue
		}
		kept = append(kept, existing[i])
	}
	if removed == nil {
		c.JSON(http.StatusNotFound, httpapi.Fail("not_found", "attachment not found"))
		return
	}

	newBody, err := reportBodyWithAttachments(body, kept)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", "Gagal memperbarui laporan."))
		return
	}

	ctx, cancel := s.ctx(c)
	defer cancel()
	if _, err := s.DB.ExecContext(ctx, `UPDATE reports SET body = $1 WHERE id = $2`, newBody, reportID); err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	s.removeReportAttachmentFile(reportID, removed.URL)
	if err := s.syncReportCalendarEvent(ctx, reportID); err != nil {
		c.JSON(http.StatusInternalServerError, httpapi.Fail("db", err.Error()))
		return
	}
	s.Hub.BroadcastEvent("report.updated", gin.H{"id": reportID})
	c.JSON(http.StatusOK, httpapi.OK(gin.H{"attachments": kept}))
}

func (s *Server) removeReportAttachmentFile(reportID uint64, publicURL string) {
	publicURL = strings.TrimSpace(publicURL)
	prefix := fmt.Sprintf("/api/v1/uploads/reports/%d/", reportID)
	if idx := strings.LastIndex(publicURL, prefix); idx >= 0 {
		name := filepath.Base(publicURL[idx+len(prefix):])
		if name != "" && name != "." && name != ".." {
			_ = os.Remove(filepath.Join(s.reportAttachmentsDir(reportID), name))
		}
	}
}
