//
//  Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//

package handler

import (
	"fmt"
	"net/http"
	"ragflow/internal/common"
	"ragflow/internal/engine"
	"ragflow/internal/service"
	"ragflow/internal/service/file"

	"github.com/gin-gonic/gin"
	"go.uber.org/zap"
)

// SkillSearchHandler handles skill search HTTP requests
type SkillSearchHandler struct {
	searchService  *service.SkillSearchService
	indexerService *service.SkillIndexerService
	spaceService   *service.SkillSpaceService
	docEngine      engine.DocEngine
}

// NewSkillSearchHandler creates a new skill search handler.
// spaceRemover is the document remover used by the skill space service for file deletion.
func NewSkillSearchHandler(docEngine engine.DocEngine, spaceRemover file.DocRemover) *SkillSearchHandler {
	return &SkillSearchHandler{
		searchService:  service.NewSkillSearchService(),
		indexerService: service.NewSkillIndexerService(),
		spaceService:   service.NewSkillSpaceService(spaceRemover),
		docEngine:      docEngine,
	}
}

// GetConfig handles the get skill search config request
// @Summary Get Skill Search Config
// @Description Get the search configuration for skills
// @Tags skill-search
// @Security ApiKeyAuth
// @Param embd_id query string true "Embedding Model ID"
// @Param space_id query string false "Skill Space ID"
// @Success 200 {object} map[string]interface{}
// @Router /v1/skills/config [get]
func (h *SkillSearchHandler) GetConfig(c *gin.Context) {
	user, errorCode, errorMessage := GetUser(c)
	if errorCode != common.CodeSuccess {
		common.ErrorWithCode(c, errorCode, errorMessage)
		return
	}

	embdID := c.Query("embd_id")
	spaceID := c.Query("space_id")

	ctx := c.Request.Context()
	result, code, err := h.searchService.GetConfig(ctx, user.ID, spaceID, embdID)
	if err != nil {
		common.ErrorWithCode(c, code, err.Error())
		return
	}

	common.SuccessWithData(c, result, "success")
}

// UpdateConfig handles the update skill search config request
// @Summary Update Skill Search Config
// @Description Update the search configuration for skills
// @Tags skill-search
// @Security ApiKeyAuth
// @Param request body service.UpdateConfigRequest true "config info"
// @Success 200 {object} map[string]interface{}
// @Router /v1/skills/config [post]
func (h *SkillSearchHandler) UpdateConfig(c *gin.Context) {
	user, errorCode, errorMessage := GetUser(c)
	if errorCode != common.CodeSuccess {
		common.ErrorWithCode(c, errorCode, errorMessage)
		return
	}

	var req service.UpdateConfigRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		common.ResponseWithCodeData(c, common.CodeDataError, nil, err.Error())
		return
	}

	req.TenantID = user.ID

	ctx := c.Request.Context()
	result, code, err := h.searchService.UpdateConfig(ctx, &req)
	if err != nil {
		common.ErrorWithCode(c, code, err.Error())
		return
	}

	common.SuccessWithData(c, result, "success")
}

// Search handles the skill search request
// @Summary Search Skills
// @Description Search skills using configured search strategy
// @Tags skill-search
// @Security ApiKeyAuth
// @Param request body service.SearchRequest true "search query"
// @Success 200 {object} map[string]interface{}
// @Router /v1/skills/search [post]
func (h *SkillSearchHandler) Search(c *gin.Context) {
	user, errorCode, errorMessage := GetUser(c)
	if errorCode != common.CodeSuccess {
		common.ErrorWithCode(c, errorCode, errorMessage)
		return
	}

	var req service.SearchRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		common.ResponseWithCodeData(c, common.CodeDataError, nil, err.Error())
		return
	}

	req.TenantID = user.ID

	result, code, err := h.searchService.Search(c.Request.Context(), &req, h.docEngine)
	if err != nil {
		common.ErrorWithCode(c, code, err.Error())
		return
	}

	common.SuccessWithData(c, result, "success")
}

// IndexSkillsRequest represents the request to index skills
type IndexSkillsRequest struct {
	Skills  []service.SkillInfo `json:"skills" binding:"required"`
	SpaceID string              `json:"space_id"`
	EmbdID  string              `json:"embd_id"` // Optional, will use config's embd_id if empty
}

// IndexSkills handles the index skills request
// @Summary Index Skills
// @Description Index skills for search. If embd_id is not provided, will use the one from skill search config.
// @Tags skill-search
// @Security ApiKeyAuth
// @Param request body IndexSkillsRequest true "skills to index"
// @Success 200 {object} map[string]interface{}
// @Router /v1/skills/index [post]
func (h *SkillSearchHandler) IndexSkills(c *gin.Context) {
	user, errorCode, errorMessage := GetUser(c)
	if errorCode != common.CodeSuccess {
		common.ErrorWithCode(c, errorCode, errorMessage)
		return
	}

	var req IndexSkillsRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		common.ResponseWithCodeData(c, common.CodeDataError, nil, err.Error())
		return
	}

	// If embd_id not provided, get from skill search config
	embdID := req.EmbdID
	if embdID == "" {
		ctx := c.Request.Context()
		config, code, err := h.searchService.GetConfig(ctx, user.ID, req.SpaceID, "")
		if err != nil {
			common.ResponseWithCodeData(c, code, nil, "failed to get skill search config: "+err.Error())
			return
		}
		val, ok := config["embd_id"].(string)
		if !ok || val == "" {
			common.ResponseWithCodeData(c, common.CodeDataError, nil, "no embedding model configured in skill search config")
			return
		}
		embdID = val
	}

	// Ensure index exists before indexing (for both ES and Infinity)
	common.Info("Ensuring skill index exists before indexing",
		zap.String("tenantID", user.ID),
		zap.String("spaceID", req.SpaceID),
		zap.String("engineType", h.docEngine.GetType()),
		zap.Int("skillCount", len(req.Skills)))

	if h.docEngine.GetType() == "elasticsearch" {
		if err := h.indexerService.EnsureIndex(c.Request.Context(), user.ID, req.SpaceID, h.docEngine, embdID); err != nil {
			common.ResponseWithCodeData(c, common.CodeOperatingError, nil, err.Error())
			return
		}
	}

	if err := h.indexerService.BatchIndexSkills(c.Request.Context(), user.ID, req.SpaceID, req.Skills, h.docEngine, embdID); err != nil {
		common.Error(fmt.Sprintf("Failed to batch index skills: tenantID=%s, spaceID=%s, error=%v", user.ID, req.SpaceID, err), err)
		common.ResponseWithCodeData(c, common.CodeOperatingError, nil, err.Error())
		return
	}

	common.Info("Successfully indexed skills",
		zap.String("tenantID", user.ID),
		zap.String("spaceID", req.SpaceID),
		zap.Int("indexedCount", len(req.Skills)))

	common.SuccessWithData(c, gin.H{
		"indexed_count": len(req.Skills),
	}, "success")
}

// ReindexRequest represents the request to reindex skills
type ReindexRequest struct {
	SpaceID string `json:"space_id" binding:"required"`
	EmbdID  string `json:"embd_id"` // Optional, will use config's embd_id if empty
}

// Reindex handles the reindex all skills request
// @Summary Reindex All Skills
// @Description Reindex all skills for a tenant. If embd_id is not provided, will use the one from skill search config.
// @Tags skill-search
// @Security ApiKeyAuth
// @Param request body ReindexRequest true "skills to reindex"
// @Success 200 {object} map[string]interface{}
// @Router /v1/skills/reindex [post]
func (h *SkillSearchHandler) Reindex(c *gin.Context) {
	user, errorCode, errorMessage := GetUser(c)
	if errorCode != common.CodeSuccess {
		common.ErrorWithCode(c, errorCode, errorMessage)
		return
	}

	var req ReindexRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		common.ResponseWithCodeData(c, common.CodeDataError, nil, err.Error())
		return
	}

	// If embd_id not provided, get from skill search config
	embdID := req.EmbdID
	if embdID == "" {
		ctx := c.Request.Context()
		config, code, err := h.searchService.GetConfig(ctx, user.ID, req.SpaceID, "")
		if err != nil {
			common.ResponseWithCodeData(c, code, nil, "failed to get skill search config: "+err.Error())
			return
		}
		val, ok := config["embd_id"].(string)
		if !ok || val == "" {
			common.ResponseWithCodeData(c, common.CodeDataError, nil, "no embedding model configured in skill search config")
			return
		}
		embdID = val
	}

	result, err := h.indexerService.ReindexAll(c.Request.Context(), user.ID, req.SpaceID, h.docEngine, embdID)
	if err != nil {
		common.ResponseWithCodeData(c, common.CodeOperatingError, nil, err.Error())
		return
	}

	common.SuccessWithData(c, result, "success")
}

// DeleteSkillIndex handles the delete skill index request
// @Summary Delete Skill Index
// @Description Delete a skill's search index
// @Tags skill-search
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param skill_id query string true "Skill ID (skill name)"
// @Param space_id query string true "Space ID"
// @Success 200 {object} map[string]interface{}
// @Router /v1/skills/index [delete]
func (h *SkillSearchHandler) DeleteSkillIndex(c *gin.Context) {
	user, errorCode, errorMessage := GetUser(c)
	if errorCode != common.CodeSuccess {
		common.ErrorWithCode(c, errorCode, errorMessage)
		return
	}

	skillID := c.Query("skill_id")
	spaceID := c.Query("space_id")
	if skillID == "" {
		common.ResponseWithCodeData(c, common.CodeDataError, nil, "skill_id is required")
		return
	}

	err := h.indexerService.DeleteSkillIndex(c.Request.Context(), user.ID, spaceID, skillID, h.docEngine)
	if err != nil {
		common.ResponseWithCodeData(c, common.CodeOperatingError, nil, "failed to delete skill index")
		return
	}

	common.SuccessWithData(c, true, "success")
}

// InitializeIndex handles the initialize skill search index request
// @Summary Initialize Skill Search Index
// @Description Initialize the skill search index for a tenant
// @Tags skill-search
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param embd_id query string true "Embedding Model ID"
// @Param space_id query string false "Skill Space ID"
// @Success 200 {object} map[string]interface{}
// @Router /v1/skill/search/init [post]
func (h *SkillSearchHandler) InitializeIndex(c *gin.Context) {
	user, errorCode, errorMessage := GetUser(c)
	if errorCode != common.CodeSuccess {
		common.ErrorWithCode(c, errorCode, errorMessage)
		return
	}

	embdID := c.Query("embd_id")
	spaceID := c.Query("space_id")
	if embdID == "" {
		common.ResponseWithCodeData(c, common.CodeDataError, nil, "embd_id is required")
		return
	}

	if err := h.indexerService.InitializeIndex(c.Request.Context(), user.ID, spaceID, h.docEngine, embdID); err != nil {
		common.ResponseWithCodeData(c, common.CodeOperatingError, nil, err.Error())
		return
	}

	common.SuccessWithData(c, gin.H{"initialized": true}, "success")
}

// ==================== Skill Space Management ====================

// ListSpaces handles the list skill spaces request
// @Summary List Skill Spaces
// @Description List all skill spaces for the current tenant
// @Tags skill-space
// @Security ApiKeyAuth
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/skills/spaces [get]
func (h *SkillSearchHandler) ListSpaces(c *gin.Context) {
	user, errorCode, errorMessage := GetUser(c)
	if errorCode != common.CodeSuccess {
		common.ErrorWithCode(c, errorCode, errorMessage)
		return
	}

	ctx := c.Request.Context()
	result, code, err := h.spaceService.ListSpaces(ctx, user.ID)
	if err != nil {
		common.ErrorWithCode(c, code, err.Error())
		return
	}

	common.SuccessWithData(c, result, "success")
}

// CreateSpaceRequest represents the request to create a skill space
type CreateSpaceRequest struct {
	Name        string `json:"name" binding:"required"`
	Description string `json:"description"`
	EmbdID      string `json:"embd_id"`
	RerankID    string `json:"rerank_id"`
}

// CreateSpace handles the create skill space request
// @Summary Create Skill Space
// @Description Create a new skill space with associated folder
// @Tags skill-space
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param request body CreateSpaceRequest true "space info"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/skills/spaces [post]
func (h *SkillSearchHandler) CreateSpace(c *gin.Context) {
	user, errorCode, errorMessage := GetUser(c)
	if errorCode != common.CodeSuccess {
		common.ErrorWithCode(c, errorCode, errorMessage)
		return
	}

	var req CreateSpaceRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		common.ResponseWithCodeData(c, common.CodeDataError, nil, err.Error())
		return
	}

	ctx := c.Request.Context()
	result, code, err := h.spaceService.CreateSpace(ctx, &service.CreateSpaceRequest{
		TenantID:    user.ID,
		Name:        req.Name,
		Description: req.Description,
		EmbdID:      req.EmbdID,
		RerankID:    req.RerankID,
	})
	if err != nil {
		common.ErrorWithCode(c, code, err.Error())
		return
	}

	common.SuccessWithData(c, result, "success")
}

// GetSpace handles the get skill space request
// @Summary Get Skill Space
// @Description Get a skill space by ID
// @Tags skill-space
// @Security ApiKeyAuth
// @Param space_id path string true "Space ID"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/skills/spaces/{space_id} [get]
func (h *SkillSearchHandler) GetSpace(c *gin.Context) {
	user, errorCode, errorMessage := GetUser(c)
	if errorCode != common.CodeSuccess {
		common.ErrorWithCode(c, errorCode, errorMessage)
		return
	}

	spaceID := c.Param("space_id")
	if spaceID == "" {
		common.ResponseWithCodeData(c, common.CodeDataError, nil, "space_id is required")
		return
	}

	ctx := c.Request.Context()
	result, code, err := h.spaceService.GetSpace(ctx, spaceID, user.ID)
	if err != nil {
		common.ErrorWithCode(c, code, err.Error())
		return
	}

	common.SuccessWithData(c, result, "success")
}

// UpdateSpaceRequest represents the request to update a skill space
type UpdateSpaceRequest struct {
	Name        string `json:"name"`
	Description string `json:"description"`
	EmbdID      string `json:"embd_id"`
	RerankID    string `json:"rerank_id"`
	TopK        int    `json:"top_k"`
}

// UpdateSpace handles the update skill space request
// @Summary Update Skill Space
// @Description Update a skill space
// @Tags skill-space
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param space_id path string true "Space ID"
// @Param request body UpdateSpaceRequest true "space updates"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/skills/spaces/{space_id} [put]
func (h *SkillSearchHandler) UpdateSpace(c *gin.Context) {
	user, errorCode, errorMessage := GetUser(c)
	if errorCode != common.CodeSuccess {
		common.ErrorWithCode(c, errorCode, errorMessage)
		return
	}

	spaceID := c.Param("space_id")
	if spaceID == "" {
		common.ResponseWithCodeData(c, common.CodeDataError, nil, "space_id is required")
		return
	}

	var req UpdateSpaceRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		common.ResponseWithCodeData(c, common.CodeDataError, nil, err.Error())
		return
	}

	ctx := c.Request.Context()
	result, code, err := h.spaceService.UpdateSpace(ctx, spaceID, user.ID, &service.UpdateSpaceRequest{
		Name:        req.Name,
		Description: req.Description,
		EmbdID:      req.EmbdID,
		RerankID:    req.RerankID,
		TopK:        req.TopK,
	})
	if err != nil {
		common.ErrorWithCode(c, code, err.Error())
		return
	}

	common.SuccessWithData(c, result, "success")
}

// DeleteSpace handles the delete skill space request
// @Summary Delete Skill Space
// @Description Delete a skill space and its associated folder
// @Tags skill-space
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param space_id path string true "Space ID"
// @Success 202 {object} map[string]interface{}
// @Router /api/v1/skills/spaces/{space_id} [delete]
func (h *SkillSearchHandler) DeleteSpace(c *gin.Context) {
	user, errorCode, errorMessage := GetUser(c)
	if errorCode != common.CodeSuccess {
		common.ErrorWithCode(c, errorCode, errorMessage)
		return
	}

	spaceID := c.Param("space_id")
	if spaceID == "" {
		common.ResponseWithCodeData(c, common.CodeDataError, nil, "space_id is required")
		return
	}

	code, err := h.spaceService.DeleteSpace(c.Request.Context(), spaceID, user.ID, h.docEngine)
	if err != nil {
		common.ErrorWithCode(c, code, err.Error())
		return
	}

	// Return 202 Accepted since deletion is async
	c.JSON(http.StatusAccepted, gin.H{
		"code":    0,
		"data":    gin.H{"deleting": true, "space_id": spaceID},
		"message": "success",
	})
}

// GetSpaceByFolder handles the get skill space by folder ID request
// @Summary Get Skill Space by Folder
// @Description Get a skill space by its folder ID
// @Tags skill-space
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param folder_id query string true "Folder ID"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/skills/space/by-folder [get]
func (h *SkillSearchHandler) GetSpaceByFolder(c *gin.Context) {
	user, errorCode, errorMessage := GetUser(c)
	if errorCode != common.CodeSuccess {
		common.ErrorWithCode(c, errorCode, errorMessage)
		return
	}

	folderID := c.Query("folder_id")
	if folderID == "" {
		common.ResponseWithCodeData(c, common.CodeDataError, nil, "folder_id is required")
		return
	}

	ctx := c.Request.Context()
	result, code, err := h.spaceService.GetSpaceByFolderID(ctx, folderID, user.ID)
	if err != nil {
		common.ErrorWithCode(c, code, err.Error())
		return
	}

	common.SuccessWithData(c, result, "success")
}
