// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package db

import (
	"context"
	"strings"
	"sync"

	"code.gitea.io/gitea/models/db"
	issue_model "code.gitea.io/gitea/models/issues"
	"code.gitea.io/gitea/modules/indexer"
	indexer_internal "code.gitea.io/gitea/modules/indexer/internal"
	inner_db "code.gitea.io/gitea/modules/indexer/internal/db"
	"code.gitea.io/gitea/modules/indexer/issues/internal"
	"code.gitea.io/gitea/modules/util"

	"xorm.io/builder"
)

var _ internal.Indexer = (*Indexer)(nil)

// Indexer implements Indexer interface to use database's like search
type Indexer struct {
	indexer_internal.Indexer
}

func (i *Indexer) SupportedSearchModes() []indexer.SearchMode {
	return indexer.SearchModesExactWords()
}

var GetIndexer = sync.OnceValue(func() *Indexer {
	return &Indexer{Indexer: &inner_db.Indexer{}}
})

// Index dummy function
func (i *Indexer) Index(_ context.Context, _ ...*internal.IndexerData) error {
	return nil
}

// Delete dummy function
func (i *Indexer) Delete(_ context.Context, _ ...int64) error {
	return nil
}

func buildMatchQuery(mode indexer.SearchModeType, colName, keyword string) builder.Cond {
	if mode == indexer.SearchModeExact {
		return db.BuildCaseInsensitiveLike(colName, keyword)
	}

	// match words
	cond := builder.NewCond()
	fields := strings.Fields(keyword)
	if len(fields) == 0 {
		return builder.Expr("1=1")
	}
	for _, field := range fields {
		if field == "" {
			continue
		}
		cond = cond.And(db.BuildCaseInsensitiveLike(colName, field))
	}
	return cond
}

// Search searches for issues
func (i *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (*internal.SearchResult, error) {
	// FIXME: I tried to avoid importing models here, but it seems to be impossible.
	//        We can provide a function to register the search function, so models/issues can register it.
	//        So models/issues will import modules/indexer/issues, it's OK because it's by design.
	//        But modules/indexer/issues has already imported models/issues to do UpdateRepoIndexer and UpdateIssueIndexer.
	//        And to avoid circular import, we have to move the functions to another package.
	//        I believe it should be services/indexer, sounds great!
	//        But the two functions are used in modules/notification/indexer, that means we will import services/indexer in modules/notification/indexer.
	//        So that's the root problem:
	//        The notification is defined in modules, but it's using lots of things should be in services.

	cond := builder.NewCond()

	if options.Keyword != "" {
		repoCond := builder.In("repo_id", options.RepoIDs)
		if len(options.RepoIDs) == 1 {
			repoCond = builder.Eq{"repo_id": options.RepoIDs[0]}
		}
		subQuery := builder.Select("id").From("issue").Where(repoCond)
		searchMode := util.IfZero(options.SearchMode, i.SupportedSearchModes()[0].ModeValue)
		cond = builder.Or(
			buildMatchQuery(searchMode, "issue.name", options.Keyword),
			buildMatchQuery(searchMode, "issue.content", options.Keyword),
			builder.In("issue.id", builder.Select("issue_id").
				From("comment").
				Where(builder.And(
					builder.Eq{"type": issue_model.CommentTypeComment},
					builder.In("issue_id", subQuery),
					buildMatchQuery(searchMode, "content", options.Keyword),
				)),
			),
		)

		if options.IsKeywordNumeric() {
			cond = cond.Or(
				builder.Eq{"`index`": options.Keyword},
			)
		}
	}

	opt, err := ToDBOptions(ctx, options)
	if err != nil {
		return nil, err
	}

	// If pagesize == 0, return total count only. It's a special case for search count.
	if options.Paginator != nil && options.Paginator.PageSize == 0 {
		total, err := issue_model.CountIssues(ctx, opt, cond)
		if err != nil {
			return nil, err
		}
		return &internal.SearchResult{
			Total: total,
		}, nil
	}

	return i.FindWithIssueOptions(ctx, opt, cond)
}

func (i *Indexer) FindWithIssueOptions(ctx context.Context, opt *issue_model.IssuesOptions, otherConds ...builder.Cond) (*internal.SearchResult, error) {
	ids, total, err := issue_model.IssueIDs(ctx, opt, otherConds...)
	if err != nil {
		return nil, err
	}

	hits := make([]internal.Match, 0, len(ids))
	for _, id := range ids {
		hits = append(hits, internal.Match{
			ID: id,
		})
	}
	return &internal.SearchResult{
		Total: total,
		Hits:  hits,
	}, nil
}
