package office

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", "office-api"),
	}
}

func (h *Handler) PublicRoutes() chi.Router {
	r := chi.NewRouter()
	r.Get("/document", h.ServeDocument)
	r.Post("/callback", h.Callback)
	return r
}

func (h *Handler) ProtectedRoutes() chi.Router {
	r := chi.NewRouter()
	read := middleware.RequirePermission(permission.ResourceDrive, permission.LevelRead)
	write := middleware.RequirePermission(permission.ResourceDrive, permission.LevelWrite)
	r.With(read).Post("/session", h.CreateSession)
	r.With(write).Post("/create", h.CreateDocument)
	return r
}

// Routes registers public OnlyOffice callbacks and authenticated session endpoints on one router.
func (h *Handler) Routes(authMiddleware func(http.Handler) http.Handler) chi.Router {
	r := chi.NewRouter()
	r.Get("/document", h.ServeDocument)
	r.Post("/callback", h.Callback)
	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(write).Post("/create", h.CreateDocument)
	})
	return r
}

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 {
		h.logger.Error("ensure nextcloud user", "error", err)
		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"
	}

	cfg, err := h.svc.EditorConfig(r.Context(), ncUser, req.Path, mode, claims.Sub, claims.Name)
	if err != nil {
		h.logger.Error("editor config", "error", err)
		apivalidate.WriteInternal(w, r)
		return
	}
	wrapped, err := h.svc.wrapEditorConfig(cfg)
	if err != nil {
		h.logger.Error("editor config jwt", "error", err)
		apivalidate.WriteInternal(w, r)
		return
	}
	apiresponse.WriteJSON(w, http.StatusOK, map[string]any{
		"config":    wrapped,
		"serverUrl": h.svc.PublicURL(),
	})
}

type createRequest struct {
	ParentPath string `json:"parent_path"`
	Name       string `json:"name"`
	Kind       string `json:"kind"`
}

func (h *Handler) CreateDocument(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 createRequest
	if err := apivalidate.DecodeJSON(w, r, 32<<10, &req); err != nil {
		return
	}
	kind := drive.NewFileKind(strings.TrimSpace(strings.ToLower(req.Kind)))
	target, err := h.drive.CreateNewFile(r.Context(), ncUser, req.ParentPath, req.Name, kind)
	if err != nil {
		apiresponse.WriteError(w, r, http.StatusBadRequest, apiresponse.CodeInvalidRequest, err.Error(), nil)
		return
	}
	apiresponse.WriteJSON(w, http.StatusCreated, map[string]string{"path": target})
}

func (h *Handler) ServeDocument(w http.ResponseWriter, r *http.Request) {
	ncUser := r.URL.Query().Get("user")
	filePath := r.URL.Query().Get("path")
	sig := r.URL.Query().Get("sig")
	if h.svc.Cfg.JWTSecret != "" && !VerifyDocAccess(ncUser, filePath, sig, h.svc.Cfg.JWTSecret) {
		http.Error(w, "forbidden", http.StatusForbidden)
		return
	}
	body, contentType, err := h.svc.OpenDocument(r.Context(), ncUser, filePath)
	if err != nil {
		http.Error(w, "not found", http.StatusNotFound)
		return
	}
	defer body.Close()
	if contentType != "" {
		w.Header().Set("Content-Type", contentType)
	}
	io.Copy(w, body)
}

func (h *Handler) Callback(w http.ResponseWriter, r *http.Request) {
	ncUser := r.URL.Query().Get("user")
	filePath := r.URL.Query().Get("path")

	var payload struct {
		Status int    `json:"status"`
		URL    string `json:"url"`
		Key    string `json:"key"`
	}
	if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
		apiresponse.WriteJSON(w, http.StatusOK, map[string]int{"error": 1})
		return
	}

	// status 2 = last editor closed, save final version; 6 = force save while editing
	if payload.Status == 2 || payload.Status == 6 {
		if payload.URL != "" {
			resp, err := http.Get(payload.URL)
			if err != nil {
				h.logger.Error("office callback fetch", "error", err, "path", filePath, "status", payload.Status)
			} else if resp.StatusCode != http.StatusOK {
				h.logger.Error("office callback fetch status", "status", resp.StatusCode, "path", filePath, "oo_status", payload.Status)
				resp.Body.Close()
			} else {
				defer resp.Body.Close()
				ct := resp.Header.Get("Content-Type")
				if err := h.svc.SaveDocument(r.Context(), ncUser, filePath, resp.Body, ct); err != nil {
					h.logger.Error("office callback save", "error", err, "path", filePath, "status", payload.Status)
				} else if payload.Status == 2 {
					h.svc.RotateDocumentKeyAfterSave(r.Context(), ncUser, filePath)
				}
			}
		}
	}
	apiresponse.WriteJSON(w, http.StatusOK, map[string]int{"error": 0})
}
