//
//  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 (
	"context"
	"fmt"
	"ragflow/internal/common"
	"ragflow/internal/dao"
	"ragflow/internal/entity"
	"strconv"

	"github.com/gin-gonic/gin"
)

// fileCommitService is the consumer-side interface for FileCommitHandler's service dependency.
type fileCommitService interface {
	CreateCommit(ctx context.Context, folderID, authorID, message string, changes []entity.FileChange) (*entity.FileCommit, error)
	ListCommits(ctx context.Context, folderID string, page, pageSize int, orderBy string, desc bool) ([]*entity.FileCommit, int64, error)
	GetCommit(ctx context.Context, commitID string) (*entity.FileCommit, error)
	ListCommitFiles(ctx context.Context, commitID string) ([]*entity.FileCommitItem, error)
	DiffCommits(ctx context.Context, fromID, toID string) ([]entity.DiffEntry, error)
	GetUncommittedChanges(ctx context.Context, folderID string) ([]entity.DiffEntry, error)
	GetCommitTree(ctx context.Context, commitID string) (map[string]interface{}, error)
	GetCommitFileContent(ctx context.Context, folderID, commitID, fileID string) ([]byte, error)
	GetFileVersionHistory(ctx context.Context, fileID string) ([]entity.VersionEntry, error)
	ListPageCommits(ctx context.Context, datasetID, pageType, slug string, page, pageSize int) ([]*entity.FileCommit, int64, error)
}

// FileCommitHandler file commit handler
type FileCommitHandler struct {
	commitService fileCommitService
	kbDAO         *dao.KnowledgebaseDAO
	fileDAO       *dao.FileDAO
}

// NewFileCommitHandler create file commit handler
func NewFileCommitHandler(commitService fileCommitService) *FileCommitHandler {
	return &FileCommitHandler{
		commitService: commitService,
		kbDAO:         dao.NewKnowledgebaseDAO(),
		fileDAO:       dao.NewFileDAO(),
	}
}

// ResolveFolderID resolves a resource ID (dataset/memory/skill) to its folder_id.
// entityType is the plural resource name (e.g. "datasets", "memories", "skills").
func (h *FileCommitHandler) ResolveFolderID(ctx context.Context, entityType, entityID string) (string, error) {
	switch entityType {
	case "datasets":
		return h.resolveDatasetFolderID(ctx, entityID)
	default:
		return "", fmt.Errorf("unsupported entity type: %s", entityType)
	}
}

// CommitFolderResolver returns a Gin middleware that resolves an entity ID
// (e.g. dataset_id, memory_id, skill_id) to a folder_id and injects it into
// c.Params so that the standard commit handlers can read it via c.Param("folder_id").
//
// entityType must match the key used in ResolveFolderID (e.g. "datasets").
// urlParam is the gin URL param name (e.g. "dataset_id").
func CommitFolderResolver(h *FileCommitHandler, entityType, urlParam string) gin.HandlerFunc {
	return func(c *gin.Context) {
		id := c.Param(urlParam)
		if id == "" {
			common.ResponseWithCodeData(c, common.CodeParamError, nil, fmt.Sprintf("%s is required", urlParam))
			c.Abort()
			return
		}
		ctx := c.Request.Context()
		folderID, err := h.ResolveFolderID(ctx, entityType, id)
		if err != nil {
			common.ResponseWithCodeData(c, common.CodeNotFound, nil, fmt.Sprintf("%s folder not found", entityType))
			c.Abort()
			return
		}
		c.Params = append(c.Params, gin.Param{Key: "folder_id", Value: folderID})
		c.Next()
	}
}

func (h *FileCommitHandler) resolveDatasetFolderID(ctx context.Context, datasetID string) (string, error) {
	kb, err := h.kbDAO.GetByID(ctx, dao.DB, datasetID)
	if err != nil {
		return "", err
	}
	files, err := h.fileDAO.Query(ctx, dao.DB, kb.Name, "", kb.TenantID)
	if err != nil {
		return "", err
	}
	for _, f := range files {
		if f.SourceType == string(entity.FileSourceKnowledgebase) && f.Type == "folder" && f.TenantID == kb.TenantID {
			return f.ID, nil
		}
	}
	return "", common.ErrNotFound
}

// CreateCommitRequest represents the request body for creating a commit
type CreateCommitRequest struct {
	Message string              `json:"message" binding:"required"`
	Files   []entity.FileChange `json:"files" binding:"required"`
}

// CreateCommit creates a new commit for a workspace folder
// @Summary Create Commit
// @Description Create a new commit with file changes for a workspace folder
// @Tags file_commit
// @Param folder_id path string true "workspace folder ID"
// @Param body body CreateCommitRequest true "commit request"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/workspaces/{folder_id}/commits [post]
func (h *FileCommitHandler) CreateCommit(c *gin.Context) {
	user, errorCode, errorMessage := GetUser(c)
	if errorCode != common.CodeSuccess {
		common.ErrorWithCode(c, errorCode, errorMessage)
		return
	}

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

	var req CreateCommitRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		common.ErrorWithCode(c, common.CodeBadRequest, err.Error())
		return
	}

	ctx := c.Request.Context()
	commit, err := h.commitService.CreateCommit(ctx, folderID, user.ID, req.Message, req.Files)
	if err != nil {
		jsonInternalError(c, err)
		return
	}

	ct := int64(0)
	if commit.CreateTime != nil {
		ct = *commit.CreateTime
	}

	common.SuccessWithData(c, entity.CommitResponse{
		ID:         commit.ID,
		FolderID:   commit.FolderID,
		ParentID:   commit.ParentID,
		Message:    commit.Message,
		AuthorID:   commit.AuthorID,
		FileCount:  commit.FileCount,
		TreeState:  commit.TreeState,
		CreateTime: &ct,
	}, common.CodeSuccess.Message())
}

// ListCommits lists commits for a workspace folder
// @Summary List Commits
// @Description List all commits for a workspace folder with pagination
// @Tags file_commit
// @Accept json
// @Produce json
// @Param folder_id path string true "workspace folder ID"
// @Param page query int false "page number"
// @Param page_size query int false "items per page"
// @Param order_by query string false "order by field"
// @Param desc query bool false "descending order"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/workspaces/{folder_id}/commits [get]
func (h *FileCommitHandler) ListCommits(c *gin.Context) {
	_, errorCode, errorMessage := GetUser(c)
	if errorCode != common.CodeSuccess {
		common.ErrorWithCode(c, errorCode, errorMessage)
		return
	}

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

	page := 1
	if pageStr := c.Query("page"); pageStr != "" {
		if p, err := strconv.Atoi(pageStr); err == nil && p >= 1 {
			page = p
		}
	}

	pageSize := 15
	if psStr := c.Query("page_size"); psStr != "" {
		if ps, err := strconv.Atoi(psStr); err == nil {
			if ps < 1 {
				ps = 15
			}
			pageSize = ps
		}
	}

	orderBy := c.DefaultQuery("order_by", "create_time")
	desc := true
	if descStr := c.Query("desc"); descStr != "" {
		desc = descStr != "false"
	}

	ctx := c.Request.Context()

	// Python's list_commits supports ?slug=<page_slug> to filter audit commits
	// for a specific wiki/skill page (written by record_page_edit). These page
	// commits are scoped to the dataset and page file key, so route them through
	// the page-commit path instead of the folder-based ListCommits. This only
	// applies to the /datasets/{dataset_id}/commits route.
	if slug := c.Query("slug"); slug != "" {
		datasetID := c.Param("dataset_id")
		if datasetID == "" {
			common.ErrorWithCode(c, common.CodeArgumentError, "slug requires a dataset scope")
			return
		}
		pageType := c.Query("page_type")
		commits, total, err := h.commitService.ListPageCommits(ctx, datasetID, pageType, slug, page, pageSize)
		if err != nil {
			jsonInternalError(c, err)
			return
		}
		var commitList []entity.CommitResponse
		for _, commit := range commits {
			var ct int64
			if commit.CreateTime != nil {
				ct = *commit.CreateTime
			}
			commitList = append(commitList, entity.CommitResponse{
				ID:         commit.ID,
				FolderID:   commit.FolderID,
				ParentID:   commit.ParentID,
				Message:    commit.Message,
				AuthorID:   commit.AuthorID,
				FileCount:  commit.FileCount,
				CreateTime: &ct,
			})
		}
		common.SuccessWithData(c, gin.H{
			"total":     total,
			"page":      page,
			"page_size": pageSize,
			"commits":   commitList,
		}, common.CodeSuccess.Message())
		return
	}

	commits, total, err := h.commitService.ListCommits(ctx, folderID, page, pageSize, orderBy, desc)
	if err != nil {
		jsonInternalError(c, err)
		return
	}

	var commitList []entity.CommitResponse
	for _, commit := range commits {
		var ct int64
		if commit.CreateTime != nil {
			ct = *commit.CreateTime
		}
		commitList = append(commitList, entity.CommitResponse{
			ID:         commit.ID,
			FolderID:   commit.FolderID,
			ParentID:   commit.ParentID,
			Message:    commit.Message,
			AuthorID:   commit.AuthorID,
			FileCount:  commit.FileCount,
			CreateTime: &ct,
		})
	}

	common.SuccessWithData(c, gin.H{
		"total":     total,
		"page":      page,
		"page_size": pageSize,
		"commits":   commitList,
	}, common.CodeSuccess.Message())
}

// GetCommit gets details of a single commit
// @Summary Get Commit
// @Description Get details of a single commit including file changes
// @Tags file_commit
// @Accept json
// @Produce json
// @Param folder_id path string true "workspace folder ID"
// @Param commit_id path string true "commit ID"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/workspaces/{folder_id}/commits/{commit_id} [get]
func (h *FileCommitHandler) GetCommit(c *gin.Context) {
	_, errorCode, errorMessage := GetUser(c)
	if errorCode != common.CodeSuccess {
		common.ErrorWithCode(c, errorCode, errorMessage)
		return
	}

	folderID := c.Param("folder_id")
	commitID := c.Param("commit_id")
	if commitID == "" {
		common.ResponseWithCodeData(c, common.CodeParamError, nil, "commit_id is required")
		return
	}

	ctx := c.Request.Context()
	commit, err := h.commitService.GetCommit(ctx, commitID)
	if err != nil {
		common.ResponseWithCodeData(c, common.CodeNotFound, nil, "Commit not found")
		return
	}

	if commit.FolderID != folderID {
		common.ResponseWithCodeData(c, common.CodeNotFound, nil, "Commit not found in workspace")
		return
	}

	items, err := h.commitService.ListCommitFiles(ctx, commitID)
	if err != nil {
		items = []*entity.FileCommitItem{}
	}

	var ct int64
	if commit.CreateTime != nil {
		ct = *commit.CreateTime
	}

	common.SuccessWithData(c, gin.H{
		"id":          commit.ID,
		"folder_id":   commit.FolderID,
		"parent_id":   commit.ParentID,
		"message":     commit.Message,
		"author_id":   commit.AuthorID,
		"file_count":  commit.FileCount,
		"create_time": ct,
		"files":       items,
	}, common.CodeSuccess.Message())
}

// ListCommitFiles lists all file changes in a commit
// @Summary List Commit Files
// @Description List all file changes in a given commit
// @Tags file_commit
// @Accept json
// @Produce json
// @Param folder_id path string true "workspace folder ID"
// @Param commit_id path string true "commit ID"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/workspaces/{folder_id}/commits/{commit_id}/files [get]
func (h *FileCommitHandler) ListCommitFiles(c *gin.Context) {
	_, errorCode, errorMessage := GetUser(c)
	if errorCode != common.CodeSuccess {
		common.ErrorWithCode(c, errorCode, errorMessage)
		return
	}

	folderID := c.Param("folder_id")
	commitID := c.Param("commit_id")
	if commitID == "" {
		common.ResponseWithCodeData(c, common.CodeParamError, nil, "commit_id is required")
		return
	}

	ctx := c.Request.Context()
	commit, err := h.commitService.GetCommit(ctx, commitID)
	if err != nil {
		common.ResponseWithCodeData(c, common.CodeNotFound, nil, "Commit not found")
		return
	}
	if commit.FolderID != folderID {
		common.ResponseWithCodeData(c, common.CodeNotFound, nil, "Commit not found in workspace")
		return
	}

	items, err := h.commitService.ListCommitFiles(ctx, commitID)
	if err != nil {
		jsonInternalError(c, err)
		return
	}

	common.SuccessWithData(c, items, common.CodeSuccess.Message())
}

// DiffCommits compares two commits
// @Summary Diff Commits
// @Description Compare two commits and return the diff
// @Tags file_commit
// @Accept json
// @Produce json
// @Param folder_id path string true "workspace folder ID"
// @Param from query string true "from commit ID"
// @Param to query string true "to commit ID"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/workspaces/{folder_id}/commits/diff [get]
func (h *FileCommitHandler) DiffCommits(c *gin.Context) {
	_, errorCode, errorMessage := GetUser(c)
	if errorCode != common.CodeSuccess {
		common.ErrorWithCode(c, errorCode, errorMessage)
		return
	}

	folderID := c.Param("folder_id")
	fromID := c.Query("from")
	toID := c.Query("to")
	if fromID == "" || toID == "" {
		common.ResponseWithCodeData(c, common.CodeParamError, nil, "'from' and 'to' query parameters are required")
		return
	}

	ctx := c.Request.Context()
	fromCommit, err := h.commitService.GetCommit(ctx, fromID)
	if err != nil {
		common.ResponseWithCodeData(c, common.CodeNotFound, nil, "Commit not found")
		return
	}
	toCommit, err := h.commitService.GetCommit(ctx, toID)
	if err != nil {
		common.ResponseWithCodeData(c, common.CodeNotFound, nil, "Commit not found")
		return
	}
	if fromCommit.FolderID != folderID || toCommit.FolderID != folderID {
		common.ResponseWithCodeData(c, common.CodeNotFound, nil, "Commit not found in workspace")
		return
	}

	diff, err := h.commitService.DiffCommits(ctx, fromID, toID)
	if err != nil {
		jsonInternalError(c, err)
		return
	}

	common.SuccessWithData(c, diff, common.CodeSuccess.Message())
}

// GetUncommittedChanges gets uncommitted changes
// @Summary Get Uncommitted Changes
// @Description Get uncommitted changes for a workspace folder (like git status)
// @Tags file_commit
// @Accept json
// @Produce json
// @Param folder_id path string true "workspace folder ID"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/workspaces/{folder_id}/changes [get]
func (h *FileCommitHandler) GetUncommittedChanges(c *gin.Context) {
	_, errorCode, errorMessage := GetUser(c)
	if errorCode != common.CodeSuccess {
		common.ErrorWithCode(c, errorCode, errorMessage)
		return
	}

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

	ctx := c.Request.Context()
	changes, err := h.commitService.GetUncommittedChanges(ctx, folderID)
	if err != nil {
		jsonInternalError(c, err)
		return
	}

	common.SuccessWithData(c, changes, common.CodeSuccess.Message())
}

// GetCommitTree gets the folder tree snapshot for a commit
// @Summary Get Commit Tree
// @Description Get the folder tree snapshot for a given commit
// @Tags file_commit
// @Accept json
// @Produce json
// @Param folder_id path string true "workspace folder ID"
// @Param commit_id path string true "commit ID"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/workspaces/{folder_id}/commits/{commit_id}/tree [get]
func (h *FileCommitHandler) GetCommitTree(c *gin.Context) {
	_, errorCode, errorMessage := GetUser(c)
	if errorCode != common.CodeSuccess {
		common.ErrorWithCode(c, errorCode, errorMessage)
		return
	}

	folderID := c.Param("folder_id")
	commitID := c.Param("commit_id")
	if commitID == "" {
		common.ResponseWithCodeData(c, common.CodeParamError, nil, "commit_id is required")
		return
	}

	ctx := c.Request.Context()
	commit, err := h.commitService.GetCommit(ctx, commitID)
	if err != nil {
		common.ResponseWithCodeData(c, common.CodeNotFound, nil, "Commit not found")
		return
	}
	if commit.FolderID != folderID {
		common.ResponseWithCodeData(c, common.CodeNotFound, nil, "Commit not found in workspace")
		return
	}

	tree, err := h.commitService.GetCommitTree(ctx, commitID)
	if err != nil {
		jsonInternalError(c, err)
		return
	}

	common.SuccessWithData(c, tree, common.CodeSuccess.Message())
}

// GetCommitFileContent gets file content as it existed in a given commit
// @Summary Get Commit File Content
// @Description Get file content as it existed in a specific commit
// @Tags file_commit
// @Accept json
// @Produce json
// @Param folder_id path string true "workspace folder ID"
// @Param commit_id path string true "commit ID"
// @Param file_id path string true "file ID"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/workspaces/{folder_id}/commits/{commit_id}/files/{file_id}/content [get]
func (h *FileCommitHandler) GetCommitFileContent(c *gin.Context) {
	_, errorCode, errorMessage := GetUser(c)
	if errorCode != common.CodeSuccess {
		common.ErrorWithCode(c, errorCode, errorMessage)
		return
	}

	folderID := c.Param("folder_id")
	commitID := c.Param("commit_id")
	fileID := c.Param("file_id")

	if folderID == "" || commitID == "" || fileID == "" {
		common.ResponseWithCodeData(c, common.CodeParamError, nil, "folder_id, commit_id, and file_id are required")
		return
	}

	ctx := c.Request.Context()
	commit, err := h.commitService.GetCommit(ctx, commitID)
	if err != nil {
		common.ResponseWithCodeData(c, common.CodeNotFound, nil, "Commit not found")
		return
	}
	if commit.FolderID != folderID {
		common.ResponseWithCodeData(c, common.CodeNotFound, nil, "Commit not found in workspace")
		return
	}

	content, err := h.commitService.GetCommitFileContent(ctx, folderID, commitID, fileID)
	if err != nil {
		common.ResponseWithCodeData(c, common.CodeNotFound, nil, err.Error())
		return
	}

	common.SuccessWithData(c, gin.H{"content": string(content)}, common.CodeSuccess.Message())
}

// GetFileVersionHistory gets version history for a specific file
// @Summary Get File Version History
// @Description Get version history for a specific file across all commits
// @Tags file_commit
// @Accept json
// @Produce json
// @Param file_id path string true "file ID"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/files/{file_id}/versions [get]
func (h *FileCommitHandler) GetFileVersionHistory(c *gin.Context) {
	_, errorCode, errorMessage := GetUser(c)
	if errorCode != common.CodeSuccess {
		common.ErrorWithCode(c, errorCode, errorMessage)
		return
	}

	fileID := c.Param("id")
	if fileID == "" {
		common.ResponseWithCodeData(c, common.CodeParamError, nil, "file_id is required")
		return
	}

	ctx := c.Request.Context()
	versions, err := h.commitService.GetFileVersionHistory(ctx, fileID)
	if err != nil {
		jsonInternalError(c, err)
		return
	}

	common.SuccessWithData(c, versions, common.CodeSuccess.Message())
}
