package richtext

import (
	"encoding/json"
	"io"
	"log/slog"
	"net/http"
	"strings"

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

	"github.com/ultisuite/ulti-backend/internal/api/apiresponse"
	"github.com/ultisuite/ulti-backend/internal/api/apivalidate"
	"github.com/ultisuite/ulti-backend/internal/api/drive"
	"github.com/ultisuite/ulti-backend/internal/api/middleware"
	"github.com/ultisuite/ulti-backend/internal/permission"
)

type Handler struct {
	svc    *Service
	drive  *drive.Service
	logger *slog.Logger
}

func NewHandler(svc *Service, driveSvc *drive.Service) *Handler {
	return &Handler{
		svc:    svc,
		drive:  driveSvc,
		logger: slog.Default().With("component", "richtext-api"),
	}
}

func (h *Handler) Routes(authMiddleware func(http.Handler) http.Handler) chi.Router {
	r := chi.NewRouter()
	r.Get("/document", h.ServeDocument)
	r.Put("/document", h.PutDocument)
	r.Post("/hooks/store", h.HookStore)
	r.Get("/internal/document", h.InternalLoadDocument)

	r.Group(func(pr chi.Router) {
		pr.Use(authMiddleware)
		read := middleware.RequirePermission(permission.ResourceDrive, permission.LevelRead)
		write := middleware.RequirePermission(permission.ResourceDrive, permission.LevelWrite)
		pr.With(read).Post("/session", h.CreateSession)
		pr.With(read).Post("/import", h.Import)
		pr.With(read).Post("/export", h.Export)
		pr.With(write).Put("/save", h.Save)
	})
	return r
}

func (h *Handler) RegisterPublicShareRoutes(r chi.Router) {
	r.Post("/shares/{token}/richtext/session", h.PublicShareSession)
	r.Post("/shares/{token}/richtext/import", h.PublicShareImport)
	r.Get("/shares/{token}/richtext/document", h.PublicShareDocument)
	r.Put("/shares/{token}/richtext/document", h.PublicSharePutDocument)
}

type sessionRequest struct {
	Path string `json:"path"`
	Mode string `json:"mode"`
}

func (h *Handler) CreateSession(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	ncUser, err := h.svc.EnsureNextcloudUser(r.Context(), claims)
	if err != nil {
		apivalidate.WriteInternal(w, r)
		return
	}
	var req sessionRequest
	if err := apivalidate.DecodeJSON(w, r, 32<<10, &req); err != nil {
		return
	}
	if strings.TrimSpace(req.Path) == "" {
		apivalidate.WriteValidationError(w, r, apivalidate.NewValidationError(
			apivalidate.FieldDetail{Field: "path", Message: "required"},
		))
		return
	}
	mode := strings.TrimSpace(req.Mode)
	if mode == "" {
		mode = "edit"
	}
	result, err := h.svc.CreateSession(r.Context(), ncUser, req.Path, mode, claims.Sub, claims.Name)
	if err != nil {
		h.logger.Error("richtext session", "error", err)
		apiresponse.WriteError(w, r, http.StatusBadRequest, apiresponse.CodeInvalidRequest, err.Error(), nil)
		return
	}
	apiresponse.WriteJSON(w, http.StatusOK, result)
}

func (h *Handler) Import(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	ncUser, err := h.svc.EnsureNextcloudUser(r.Context(), claims)
	if err != nil {
		apivalidate.WriteInternal(w, r)
		return
	}
	var req ImportRequest
	if err := apivalidate.DecodeJSON(w, r, 32<<20, &req); err != nil {
		return
	}
	canonical, err := h.svc.ImportDocument(r.Context(), ncUser, claims.Sub, req)
	if err != nil {
		apiresponse.WriteError(w, r, http.StatusBadRequest, apiresponse.CodeInvalidRequest, err.Error(), nil)
		return
	}
	apiresponse.WriteJSON(w, http.StatusOK, map[string]string{"canonical_path": canonical})
}

func (h *Handler) Export(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	ncUser, err := h.svc.EnsureNextcloudUser(r.Context(), claims)
	if err != nil {
		apivalidate.WriteInternal(w, r)
		return
	}
	var req ExportRequest
	if err := apivalidate.DecodeJSON(w, r, 32<<10, &req); err != nil {
		return
	}
	body, ct, err := h.svc.ExportDocument(r.Context(), ncUser, req.Path, req.Format)
	if err != nil {
		apiresponse.WriteError(w, r, http.StatusBadRequest, apiresponse.CodeInvalidRequest, err.Error(), nil)
		return
	}
	w.Header().Set("Content-Type", ct)
	w.WriteHeader(http.StatusOK)
	_, _ = w.Write(body)
}

type saveRequest struct {
	Path     string          `json:"path"`
	Document json.RawMessage `json:"document"`
	YjsState string          `json:"yjsState,omitempty"`
}

func (h *Handler) Save(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	ncUser, err := h.svc.EnsureNextcloudUser(r.Context(), claims)
	if err != nil {
		apivalidate.WriteInternal(w, r)
		return
	}
	var req saveRequest
	if err := apivalidate.DecodeJSON(w, r, 32<<20, &req); err != nil {
		return
	}
	doc := NewUltiDoc(req.Document, nil)
	doc.YjsState = req.YjsState
	payload, err := doc.Marshal()
	if err != nil {
		apivalidate.WriteInternal(w, r)
		return
	}
	if err := h.svc.SaveDocument(r.Context(), ncUser, req.Path, payload, claims.Sub); err != nil {
		apivalidate.WriteInternal(w, r)
		return
	}
	apiresponse.WriteJSON(w, http.StatusOK, map[string]string{"path": normalizePath(req.Path)})
}

func (h *Handler) ServeDocument(w http.ResponseWriter, r *http.Request) {
	sig := strings.TrimSpace(r.URL.Query().Get("sig"))
	path := strings.TrimSpace(r.URL.Query().Get("path"))
	user := strings.TrimSpace(r.URL.Query().Get("user"))
	if h.svc.Cfg.HocuspocusSecret != "" {
		if _, err := verifyDocAccessSig(sig, user, path, h.svc.Cfg.HocuspocusSecret); err != nil {
			http.Error(w, "forbidden", http.StatusForbidden)
			return
		}
	}
	body, err := h.svc.LoadDocumentForUser(r.Context(), user, path)
	if err != nil {
		http.Error(w, "not found", http.StatusNotFound)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	_, _ = w.Write(body)
}

func (h *Handler) PutDocument(w http.ResponseWriter, r *http.Request) {
	sig := strings.TrimSpace(r.URL.Query().Get("sig"))
	path := strings.TrimSpace(r.URL.Query().Get("path"))
	user := strings.TrimSpace(r.URL.Query().Get("user"))
	platformUser := strings.TrimSpace(r.URL.Query().Get("sub"))
	if h.svc.Cfg.HocuspocusSecret != "" {
		if _, err := verifyDocAccessSig(sig, user, path, h.svc.Cfg.HocuspocusSecret); err != nil {
			http.Error(w, "forbidden", http.StatusForbidden)
			return
		}
	}
	raw, err := io.ReadAll(r.Body)
	if err != nil {
		apivalidate.WriteInternal(w, r)
		return
	}
	if err := h.svc.SaveDocument(r.Context(), user, path, raw, platformUser); err != nil {
		http.Error(w, "save failed", http.StatusInternalServerError)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) InternalLoadDocument(w http.ResponseWriter, r *http.Request) {
	secret := r.Header.Get("X-Hocuspocus-Secret")
	if h.svc.Cfg.HocuspocusSecret != "" && secret != h.svc.Cfg.HocuspocusSecret {
		http.Error(w, "forbidden", http.StatusForbidden)
		return
	}
	user := strings.TrimSpace(r.URL.Query().Get("user"))
	path := strings.TrimSpace(r.URL.Query().Get("path"))
	body, err := h.svc.LoadDocumentForUser(r.Context(), user, path)
	if err != nil {
		http.Error(w, "not found", http.StatusNotFound)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	_, _ = w.Write(body)
}

type hookStorePayload struct {
	Room     string `json:"room"`
	Path     string `json:"path"`
	User     string `json:"user"`
	Sub      string `json:"sub"`
	YjsState string `json:"yjsState"`
	Document json.RawMessage `json:"document,omitempty"`
}

func (h *Handler) HookStore(w http.ResponseWriter, r *http.Request) {
	secret := r.Header.Get("X-Hocuspocus-Secret")
	if h.svc.Cfg.HocuspocusSecret != "" && secret != h.svc.Cfg.HocuspocusSecret {
		http.Error(w, "forbidden", http.StatusForbidden)
		return
	}
	var payload hookStorePayload
	if err := apivalidate.DecodeJSON(w, r, 32<<20, &payload); err != nil {
		return
	}
	path := normalizePath(payload.Path)
	var raw []byte
	if len(payload.Document) > 0 {
		doc := NewUltiDoc(payload.Document, nil)
		doc.YjsState = payload.YjsState
		var err error
		raw, err = doc.Marshal()
		if err != nil {
			apivalidate.WriteInternal(w, r)
			return
		}
	} else if payload.YjsState != "" {
		doc := UltiDoc{SchemaVersion: schemaVersion, Editor: "tiptap", YjsState: payload.YjsState, Content: emptyDocContent()}
		var err error
		raw, err = doc.Marshal()
		if err != nil {
			apivalidate.WriteInternal(w, r)
			return
		}
	} else {
		apivalidate.WriteValidationError(w, r, apivalidate.NewValidationError(
			apivalidate.FieldDetail{Field: "document", Message: "required"},
		))
		return
	}
	if err := h.svc.SaveDocument(r.Context(), payload.User, path, raw, payload.Sub); err != nil {
		h.logger.Error("hook store", "error", err, "path", path)
		apivalidate.WriteInternal(w, r)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}
