//
//  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 dao

import (
	"context"
	"errors"
	"ragflow/internal/entity"
	"strconv"

	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

// toInterfaceSlice converts a string slice into an []interface{} for use as
// parameterized query variables.
func toInterfaceSlice(ss []string) []interface{} {
	out := make([]interface{}, len(ss))
	for i := range ss {
		out[i] = ss[i]
	}
	return out
}

// FileCommitDAO file commit data access object
type FileCommitDAO struct{}

// NewFileCommitDAO create file commit DAO
func NewFileCommitDAO() *FileCommitDAO {
	return &FileCommitDAO{}
}

// GetByID gets a file commit by ID
func (dao *FileCommitDAO) GetByID(ctx context.Context, db *gorm.DB, id string) (*entity.FileCommit, error) {
	var commit entity.FileCommit
	err := db.WithContext(ctx).Where("id = ?", id).First(&commit).Error
	if err != nil {
		return nil, err
	}
	return &commit, nil
}

// Create creates a new file commit record
func (dao *FileCommitDAO) Create(ctx context.Context, db *gorm.DB, commit *entity.FileCommit) error {
	return db.WithContext(ctx).Create(commit).Error
}

// UpdateTreeState updates the tree_state field for a commit
func (dao *FileCommitDAO) UpdateTreeState(ctx context.Context, db *gorm.DB, id string, treeState string) error {
	return db.WithContext(ctx).Model(&entity.FileCommit{}).Where("id = ?", id).Update("tree_state", treeState).Error
}

// GetLatestByFolderID gets the latest (most recent) commit for a folder
func (dao *FileCommitDAO) GetLatestByFolderID(ctx context.Context, db *gorm.DB, folderID string) (*entity.FileCommit, error) {
	var commit entity.FileCommit
	err := db.WithContext(ctx).Where("folder_id = ?", folderID).
		Order("create_time DESC").
		First(&commit).Error
	if err != nil {
		return nil, err
	}
	return &commit, nil
}

// allowedFileCommitSorts is the whitelist of safe column names for
// ListByFolderID's orderBy parameter to prevent SQL injection.
var allowedFileCommitSorts = map[string]string{
	"create_time": "create_time",
	"create_date": "create_date",
	"update_time": "update_time",
	"update_date": "update_date",
}

// ListByFolderID lists commits for a folder with pagination
func (dao *FileCommitDAO) ListByFolderID(ctx context.Context, db *gorm.DB, folderID string, page, pageSize int, orderBy string, desc bool) ([]*entity.FileCommit, int64, error) {
	var commits []*entity.FileCommit
	var total int64

	query := db.WithContext(ctx).Model(&entity.FileCommit{}).Where("folder_id = ?", folderID)

	if err := query.Count(&total).Error; err != nil {
		return nil, 0, err
	}

	// Sanitize orderBy against whitelist; fall back to create_time.
	safeCol, ok := allowedFileCommitSorts[orderBy]
	if !ok {
		safeCol = "create_time"
	}

	orderDirection := "DESC"
	if !desc {
		orderDirection = "ASC"
	}

	orderClause := safeCol + " " + orderDirection

	if page > 0 && pageSize > 0 {
		offset := (page - 1) * pageSize
		if err := query.Order(orderClause).Offset(offset).Limit(pageSize).Find(&commits).Error; err != nil {
			return nil, 0, err
		}
	} else {
		if err := query.Order(orderClause).Find(&commits).Error; err != nil {
			return nil, 0, err
		}
	}

	return commits, total, nil
}

// ListByIDs lists commits whose IDs are in the given set, preserving the
// order of commitIDs (most-recent-first for page-edit history). Returns the
// total matched count for pagination.
func (dao *FileCommitDAO) ListByIDs(ctx context.Context, db *gorm.DB, commitIDs []string, page, pageSize int) ([]*entity.FileCommit, int64, error) {
	if len(commitIDs) == 0 {
		return []*entity.FileCommit{}, 0, nil
	}
	var commits []*entity.FileCommit
	var total int64
	query := db.WithContext(ctx).Model(&entity.FileCommit{}).Where("id IN ?", commitIDs)
	if err := query.Count(&total).Error; err != nil {
		return nil, 0, err
	}
	// Preserve the incoming (most-recent-first) order with a parameterized
	// CASE expression rather than a MySQL-specific FIELD() interpolation.
	orderExpr := "CASE id"
	for i := range commitIDs {
		orderExpr += " WHEN ? THEN " + strconv.Itoa(i)
	}
	orderExpr += " END"
	ordered := query.Clauses(clause.OrderBy{Expression: clause.Expr{
		SQL:  orderExpr,
		Vars: toInterfaceSlice(commitIDs),
	}})
	// Apply pagination in the query itself so long histories never load the
	// full set into memory before slicing.
	if page > 0 && pageSize > 0 {
		ordered = ordered.Offset((page - 1) * pageSize).Limit(pageSize)
	}
	if err := ordered.Find(&commits).Error; err != nil {
		return nil, 0, err
	}
	return commits, total, nil
}

// FileCommitItemDAO file commit item data access object
type FileCommitItemDAO struct{}

// NewFileCommitItemDAO create file commit item DAO
func NewFileCommitItemDAO() *FileCommitItemDAO {
	return &FileCommitItemDAO{}
}

// Create creates a new file commit item record
func (dao *FileCommitItemDAO) Create(ctx context.Context, db *gorm.DB, item *entity.FileCommitItem) error {
	return db.WithContext(ctx).Create(item).Error
}

// ListByCommitID lists all items for a commit
func (dao *FileCommitItemDAO) ListByCommitID(ctx context.Context, db *gorm.DB, commitID string) ([]*entity.FileCommitItem, error) {
	var items []*entity.FileCommitItem
	err := db.WithContext(ctx).Where("commit_id = ?", commitID).Order("create_time ASC").Find(&items).Error
	return items, err
}

// ListByFileID lists all commit items for a specific file (for version history)
func (dao *FileCommitItemDAO) ListByFileID(ctx context.Context, db *gorm.DB, fileID string) ([]*entity.FileCommitItem, error) {
	var items []*entity.FileCommitItem
	err := db.WithContext(ctx).Where("file_id = ?", fileID).Order("create_time DESC").Find(&items).Error
	return items, err
}

// GetByCommitIDAndFileID gets a single commit item by commit and file ID
func (dao *FileCommitItemDAO) GetByCommitIDAndFileID(ctx context.Context, db *gorm.DB, commitID, fileID string) (*entity.FileCommitItem, error) {
	var item entity.FileCommitItem
	err := db.WithContext(ctx).Where("commit_id = ? AND file_id = ?", commitID, fileID).First(&item).Error
	if err != nil {
		return nil, err
	}
	return &item, nil
}

// GetLatestCommitIDByFileID returns the most recent commit_id that modified the
// given wiki page file key, or "" if none. Used to build the parent chain for
// page-edit commits.
func (dao *FileCommitItemDAO) GetLatestCommitIDByFileID(ctx context.Context, db *gorm.DB, fileID string) (string, error) {
	var item entity.FileCommitItem
	err := db.WithContext(ctx).
		Where("file_id = ?", fileID).
		// Order by the monotonic auto-increment sequence so the most recently
		// inserted item is always selected even when multiple edits share the
		// same create_time millisecond.
		Order("seq DESC").
		First(&item).Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return "", nil
		}
		return "", err
	}
	return item.CommitID, nil
}
