package mail

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

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

	"github.com/ultisuite/ulti-backend/internal/api/apiresponse"
	"github.com/ultisuite/ulti-backend/internal/api/apivalidate"
	driveapi "github.com/ultisuite/ulti-backend/internal/api/drive"
	"github.com/ultisuite/ulti-backend/internal/api/middleware"
	"github.com/ultisuite/ulti-backend/internal/filescan"
	"github.com/ultisuite/ulti-backend/internal/mail/limits"
)

const maxJSONRequestBody = 32 << 10

func (h *Handler) ListMessageAttachments(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	messageID := chi.URLParam(r, "messageID")
	if h.denyUnlessMessageInScope(w, r, messageID) {
		return
	}

	list, err := h.svc.ListMessageAttachments(r.Context(), claims.Sub, messageID)
	if err != nil {
		if errors.Is(err, ErrNotFound) {
			apivalidate.WriteNotFound(w, r, "not found")
			return
		}
		h.logger.Error("list attachments", "error", err)
		apivalidate.WriteInternal(w, r)
		return
	}
	apiresponse.WriteJSON(w, http.StatusOK, map[string]any{"attachments": list})
}

func (h *Handler) MessageAttachmentCIDMap(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	messageID := chi.URLParam(r, "messageID")
	if h.denyUnlessMessageInScope(w, r, messageID) {
		return
	}

	mapping, err := h.svc.MessageAttachmentCIDMap(r.Context(), claims.Sub, messageID)
	if err != nil {
		if errors.Is(err, ErrNotFound) {
			apivalidate.WriteNotFound(w, r, "not found")
			return
		}
		h.logger.Error("attachment cid map", "error", err)
		apivalidate.WriteInternal(w, r)
		return
	}
	apiresponse.WriteJSON(w, http.StatusOK, map[string]any{"cid_map": mapping})
}

func (h *Handler) ReindexMessageAttachments(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	messageID := chi.URLParam(r, "messageID")
	if h.denyUnlessMessageInScope(w, r, messageID) {
		return
	}
	if h.accountSync == nil {
		apiresponse.WriteError(w, r, http.StatusServiceUnavailable, "sync_unavailable", "mail sync is not configured", nil)
		return
	}
	if _, err := h.svc.GetMessage(r.Context(), claims.Sub, messageID); err != nil {
		if errors.Is(err, ErrNotFound) {
			apivalidate.WriteNotFound(w, r, "not found")
			return
		}
		h.logger.Error("load message for attachment reindex", "message_id", messageID, "error", err)
		apivalidate.WriteInternal(w, r)
		return
	}
	if err := h.accountSync.ReindexMessageAttachments(r.Context(), claims.Sub, messageID); err != nil {
		if strings.Contains(err.Error(), "not found") {
			apivalidate.WriteNotFound(w, r, "not found")
			return
		}
		h.logger.Error("reindex message attachments", "message_id", messageID, "error", err)
		apiresponse.WriteError(w, r, http.StatusBadGateway, "reindex_failed", "attachment reindex failed", nil)
		return
	}
	apiresponse.WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}

func (h *Handler) UploadMessageAttachment(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	messageID := chi.URLParam(r, "messageID")
	if h.denyUnlessMessageInScope(w, r, messageID) {
		return
	}

	if err := r.ParseMultipartForm(limits.MaxMultipartUploadBytes); err != nil {
		apiresponse.WriteError(w, r, http.StatusBadRequest, apiresponse.CodeInvalidRequest, "invalid multipart form", nil)
		return
	}

	file, header, err := r.FormFile("file")
	if err != nil {
		apiresponse.WriteError(w, r, http.StatusBadRequest, apiresponse.CodeInvalidRequest, "file field required", nil)
		return
	}
	defer file.Close()

	filename := filepath.Base(header.Filename)
	contentType := header.Header.Get("Content-Type")
	if contentType == "" {
		contentType = mime.TypeByExtension(filepath.Ext(filename))
	}
	if contentType == "" {
		contentType = "application/octet-stream"
	}
	contentID := strings.TrimSpace(r.FormValue("content_id"))
	isInline := strings.EqualFold(r.FormValue("inline"), "true") || contentID != ""

	id, err := h.svc.UploadMessageAttachment(
		r.Context(), claims.Sub, messageID, filename, contentType, contentID, isInline,
		file, header.Size,
	)
	if err != nil {
		if errors.Is(err, ErrNotFound) {
			apivalidate.WriteNotFound(w, r, "not found")
			return
		}
		if writeAttachmentUploadError(w, r, err) {
			return
		}
		h.logger.Error("upload attachment", "error", err)
		apivalidate.WriteInternal(w, r)
		return
	}
	apiresponse.WriteJSON(w, http.StatusCreated, map[string]string{"id": id})
}

func (h *Handler) DownloadAttachment(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	inline := strings.HasSuffix(r.URL.Path, "/inline") || r.URL.Query().Get("inline") == "true"
	attachmentID := chi.URLParam(r, "attachmentID")
	if h.denyUnlessAttachmentInScope(w, r, attachmentID) {
		return
	}

	filename, contentType, size, isInline, body, err := h.svc.OpenAttachment(r.Context(), claims.Sub, attachmentID)
	if err != nil {
		if errors.Is(err, ErrAttachmentNotFound) {
			apivalidate.WriteNotFound(w, r, "not found")
			return
		}
		h.logger.Error("download attachment", "error", err)
		apivalidate.WriteInternal(w, r)
		return
	}
	defer body.Close()

	disposition := "attachment"
	if inline || isInline {
		disposition = "inline"
	}
	w.Header().Set("Content-Type", contentType)
	w.Header().Set("Content-Disposition", fmt.Sprintf(`%s; filename="%s"`, disposition, filename))
	if size > 0 {
		w.Header().Set("Content-Length", fmt.Sprintf("%d", size))
	}
	_, _ = io.Copy(w, body)
}

func (h *Handler) UploadDraftAttachment(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	draftID := chi.URLParam(r, "draftID")

	if err := r.ParseMultipartForm(limits.MaxMultipartUploadBytes); err != nil {
		apiresponse.WriteError(w, r, http.StatusBadRequest, apiresponse.CodeInvalidRequest, "invalid multipart form", nil)
		return
	}

	file, header, err := r.FormFile("file")
	if err != nil {
		apiresponse.WriteError(w, r, http.StatusBadRequest, apiresponse.CodeInvalidRequest, "file field required", nil)
		return
	}
	defer file.Close()

	filename := filepath.Base(header.Filename)
	contentType := header.Header.Get("Content-Type")
	if contentType == "" {
		contentType = mime.TypeByExtension(filepath.Ext(filename))
	}
	if contentType == "" {
		contentType = "application/octet-stream"
	}
	contentID := strings.TrimSpace(r.FormValue("content_id"))
	isInline := strings.EqualFold(r.FormValue("inline"), "true") || contentID != ""

	id, err := h.svc.UploadDraftAttachment(
		r.Context(), claims.Sub, draftID, filename, contentType, contentID, isInline,
		file, header.Size,
	)
	if err != nil {
		if errors.Is(err, ErrNotFound) {
			apivalidate.WriteNotFound(w, r, "not found")
			return
		}
		if writeAttachmentUploadError(w, r, err) {
			return
		}
		h.logger.Error("upload draft attachment", "error", err)
		apivalidate.WriteInternal(w, r)
		return
	}
	apiresponse.WriteJSON(w, http.StatusCreated, map[string]string{"id": id})
}

func (h *Handler) DownloadDraftAttachment(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	draftID := chi.URLParam(r, "draftID")
	attachmentID := chi.URLParam(r, "attachmentID")
	inline := strings.HasSuffix(r.URL.Path, "/inline") || r.URL.Query().Get("inline") == "true"

	filename, contentType, body, err := h.svc.OpenDraftAttachment(r.Context(), claims.Sub, draftID, attachmentID)
	if err != nil {
		if errors.Is(err, ErrNotFound) || errors.Is(err, ErrAttachmentNotFound) {
			apivalidate.WriteNotFound(w, r, "not found")
			return
		}
		h.logger.Error("download draft attachment", "error", err)
		apivalidate.WriteInternal(w, r)
		return
	}
	defer body.Close()

	disposition := "attachment"
	if inline {
		disposition = "inline"
	}
	w.Header().Set("Content-Type", contentType)
	w.Header().Set("Content-Disposition", fmt.Sprintf(`%s; filename="%s"`, disposition, filename))
	_, _ = io.Copy(w, body)
}

type saveToDriveRequest struct {
	FolderPath string `json:"folder_path"`
}

func (h *Handler) SaveAttachmentToDrive(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	messageID := chi.URLParam(r, "messageID")
	attachmentID := chi.URLParam(r, "attachmentID")
	if h.denyUnlessMessageInScope(w, r, messageID) {
		return
	}

	var req saveToDriveRequest
	if err := apivalidate.DecodeJSON(w, r, maxJSONRequestBody, &req); err != nil {
		return
	}

	drivePath, err := h.svc.SaveAttachmentToDrive(
		r.Context(),
		claims.Sub,
		claims.Email,
		claims.Sub,
		claims.Name,
		messageID,
		attachmentID,
		req.FolderPath,
	)
	if err != nil {
		if writeSaveToDriveError(w, r, h, err) {
			return
		}
		h.logger.Error("save attachment to drive", "error", err)
		apivalidate.WriteInternal(w, r)
		return
	}
	apiresponse.WriteJSON(w, http.StatusOK, map[string]string{"drive_path": drivePath})
}

func (h *Handler) SaveMessageAttachmentsToDrive(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	messageID := chi.URLParam(r, "messageID")
	if h.denyUnlessMessageInScope(w, r, messageID) {
		return
	}

	var req saveToDriveRequest
	if err := apivalidate.DecodeJSON(w, r, maxJSONRequestBody, &req); err != nil {
		return
	}

	list, err := h.svc.SaveMessageAttachmentsToDrive(
		r.Context(),
		claims.Sub,
		claims.Email,
		claims.Sub,
		claims.Name,
		messageID,
		req.FolderPath,
	)
	if err != nil {
		if writeSaveToDriveError(w, r, h, err) {
			return
		}
		h.logger.Error("save message attachments to drive", "error", err)
		apivalidate.WriteInternal(w, r)
		return
	}
	apiresponse.WriteJSON(w, http.StatusOK, map[string]any{"attachments": list})
}

func writeSaveToDriveError(w http.ResponseWriter, r *http.Request, h *Handler, err error) bool {
	switch {
	case errors.Is(err, ErrNotFound), errors.Is(err, ErrAttachmentNotFound):
		apivalidate.WriteNotFound(w, r, "not found")
		return true
	case errors.Is(err, ErrDriveUnavailable):
		apiresponse.WriteError(w, r, http.StatusServiceUnavailable, "drive_unavailable", "drive is not available", nil)
		return true
	case errors.Is(err, driveapi.ErrQuotaExceeded):
		apiresponse.WriteError(w, r, http.StatusInsufficientStorage, "drive.quota_exceeded", "drive quota exceeded", nil)
		return true
	case errors.Is(err, driveapi.ErrForbidden):
		apiresponse.WriteError(w, r, http.StatusForbidden, apiresponse.CodeAuthForbidden, "drive access denied", nil)
		return true
	default:
		return false
	}
}

func writeAttachmentUploadError(w http.ResponseWriter, r *http.Request, err error) bool {
	switch {
	case errors.Is(err, limits.ErrAttachmentTooLarge), errors.Is(err, limits.ErrAttachmentsTotalTooLarge):
		apiresponse.WriteError(w, r, http.StatusRequestEntityTooLarge, apiresponse.CodeInvalidRequest, "attachment too large", nil)
		return true
	case errors.Is(err, limits.ErrTooManyAttachments):
		apiresponse.WriteError(w, r, http.StatusBadRequest, apiresponse.CodeInvalidRequest, "too many attachments", nil)
		return true
	case errors.Is(err, ErrMalware), errors.Is(err, filescan.ErrMalicious):
		apiresponse.WriteError(w, r, http.StatusUnprocessableEntity, "mail.malware_detected", "malware detected in attachment", nil)
		return true
	default:
		return false
	}
}
