/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 search_parser

import (
	"context"
	"fmt"
	"regexp"
	"strings"

	"github.com/apache/answer/internal/base/constant"

	"github.com/apache/answer/internal/schema"
	"github.com/apache/answer/internal/service/tag_common"
	usercommon "github.com/apache/answer/internal/service/user_common"
	"github.com/apache/answer/pkg/converter"
)

type SearchParser struct {
	tagCommonService *tag_common.TagCommonService
	userCommon       *usercommon.UserCommon
}

func NewSearchParser(tagCommonService *tag_common.TagCommonService, userCommon *usercommon.UserCommon) *SearchParser {
	return &SearchParser{
		tagCommonService: tagCommonService,
		userCommon:       userCommon,
	}
}

// ParseStructure parse search structure, maybe match one of type all/questions/answers,
// but if match two type, it will return false
func (sp *SearchParser) ParseStructure(ctx context.Context, dto *schema.SearchDTO) (cond *schema.SearchCondition) {
	cond = &schema.SearchCondition{}
	var (
		query      = dto.Query
		limitWords = 5
	)

	// match tags
	cond.Tags = sp.parseTags(ctx, &query)

	// match all
	cond.UserID = sp.parseUserID(ctx, &query, dto.UserID)
	cond.VoteAmount = sp.parseVotes(&query)
	cond.Words = sp.parseWithin(&query)

	// match questions
	cond.NotAccepted = sp.parseNotAccepted(&query)
	if cond.NotAccepted {
		cond.TargetType = constant.QuestionObjectType
	}
	cond.Views = sp.parseViews(&query)
	if cond.Views != -1 {
		cond.TargetType = constant.QuestionObjectType
	}
	cond.AnswerAmount = sp.parseAnswers(&query)
	if cond.AnswerAmount != -1 {
		cond.TargetType = constant.QuestionObjectType
	}

	// match answers
	cond.Accepted = sp.parseAccepted(&query)
	if cond.Accepted {
		cond.TargetType = constant.AnswerObjectType
	}
	cond.QuestionID = sp.parseQuestionID(&query)
	if cond.QuestionID != "" {
		cond.TargetType = constant.AnswerObjectType
	}

	if sp.parseIsQuestion(&query) {
		cond.TargetType = constant.QuestionObjectType
	}
	if sp.parseIsAnswer(&query) {
		cond.TargetType = constant.AnswerObjectType
	}

	if len(strings.TrimSpace(query)) > 0 {
		words := strings.Split(strings.TrimSpace(query), " ")
		cond.Words = append(cond.Words, words...)
	}

	// check limit words
	if len(cond.Words) > limitWords {
		cond.Words = cond.Words[:limitWords]
	}
	return
}

// parseTags parse search tags, return tag ids array
func (sp *SearchParser) parseTags(ctx context.Context, query *string) (tags [][]string) {
	var (
		// expire tag pattern
		exprTag = `\[(.*?)\]`
		q       = *query
		limit   = 5
	)

	re := regexp.MustCompile(exprTag)
	res := re.FindAllStringSubmatch(q, -1)
	if len(res) == 0 {
		return
	}

	tags = make([][]string, 0)
	for _, item := range res {
		tagGroup := make([]string, 0)
		tag, exists, err := sp.tagCommonService.GetTagBySlugName(ctx, item[1])
		if err != nil || !exists {
			continue
		}
		tagGroup = append(tagGroup, tag.ID)
		if tag.MainTagID > 0 {
			tagGroup = append(tagGroup, fmt.Sprintf("%d", tag.MainTagID))
		}
		synIDs, err := sp.tagCommonService.GetTagIDsByMainTagID(ctx, tag.ID)
		if err != nil || !exists {
			continue
		}
		tagGroup = append(tagGroup, tag.ID)
		tagGroup = append(tagGroup, synIDs...)
		tagGroup = converter.UniqueArray(tagGroup)
		tags = append(tags, tagGroup)
	}

	// limit maximum 5 tags
	if len(tags) > limit {
		tags = tags[:limit]
	}

	q = strings.TrimSpace(re.ReplaceAllString(q, ""))
	*query = q
	return
}

// parseUserID return user id or current login user id
func (sp *SearchParser) parseUserID(ctx context.Context, query *string, currentUserID string) (userID string) {
	var (
		exprUsername = `user:(\S+)`
		exprMe       = "user:me"
		q            = *query
	)

	re := regexp.MustCompile(exprUsername)
	res := re.FindStringSubmatch(q)
	if strings.Contains(q, exprMe) {
		userID = currentUserID
		q = strings.ReplaceAll(q, exprMe, "")
	} else if len(res) > 1 {
		name := res[1]
		user, has, err := sp.userCommon.GetUserBasicInfoByUserName(ctx, name)
		if err == nil && has {
			userID = user.ID
			q = re.ReplaceAllString(q, "")
		}
	}
	*query = strings.TrimSpace(q)
	return
}

// parseVotes return the votes of search query
func (sp *SearchParser) parseVotes(query *string) (votes int) {
	var (
		expr = `score:(\d+)`
		q    = *query
	)
	votes = -1

	re := regexp.MustCompile(expr)
	res := re.FindStringSubmatch(q)
	if len(res) > 1 {
		votes = converter.StringToInt(res[1])
		q = re.ReplaceAllString(q, "")
	}

	*query = strings.TrimSpace(q)
	return
}

// parseWithin parse quotes within words like: "hello world"
func (sp *SearchParser) parseWithin(query *string) (words []string) {
	var (
		q    = *query
		expr = `(?U)(".+")`
	)
	re := regexp.MustCompile(expr)
	matches := re.FindAllStringSubmatch(q, -1)
	words = []string{}
	for _, match := range matches {
		if len(match[1]) == 0 {
			continue
		}
		words = append(words, match[1])
	}
	q = re.ReplaceAllString(q, "")
	*query = strings.TrimSpace(q)
	return
}

// parseNotAccepted return the question has not accepted the answer
func (sp *SearchParser) parseNotAccepted(query *string) (notAccepted bool) {
	var (
		q    = *query
		expr = `hasaccepted:no`
	)

	if strings.Contains(q, expr) {
		q = strings.ReplaceAll(q, expr, "")
		notAccepted = true
	}

	*query = strings.TrimSpace(q)
	return
}

// parseIsQuestion check the result if only limit question or not
func (sp *SearchParser) parseIsQuestion(query *string) (isQuestion bool) {
	var (
		q    = *query
		expr = `is:question`
	)

	if strings.Contains(q, expr) {
		q = strings.ReplaceAll(q, expr, "")
		isQuestion = true
	}

	*query = strings.TrimSpace(q)
	return
}

// parseViews check search has views or not
func (sp *SearchParser) parseViews(query *string) (views int) {
	var (
		q    = *query
		expr = `views:(\d+)`
	)
	views = -1

	re := regexp.MustCompile(expr)
	res := re.FindStringSubmatch(q)
	if len(res) > 1 {
		views = converter.StringToInt(res[1])
		q = re.ReplaceAllString(q, "")
	}
	*query = strings.TrimSpace(q)
	return
}

// parseAnswers check whether specified answer count for question
func (sp *SearchParser) parseAnswers(query *string) (answers int) {
	var (
		q    = *query
		expr = `answers:(\d+)`
	)
	answers = -1

	re := regexp.MustCompile(expr)
	res := re.FindStringSubmatch(q)
	if len(res) > 1 {
		answers = converter.StringToInt(res[1])
		q = re.ReplaceAllString(q, "")
	}

	*query = strings.TrimSpace(q)
	return
}

// parseAccepted check the search is limit accepted answer or not
func (sp *SearchParser) parseAccepted(query *string) (accepted bool) {
	var (
		q    = *query
		expr = `isaccepted:yes`
	)

	if strings.Contains(q, expr) {
		accepted = true
		q = strings.ReplaceAll(q, expr, "")
	}

	*query = strings.TrimSpace(q)
	return
}

// parseQuestionID check whether specified question's id
func (sp *SearchParser) parseQuestionID(query *string) (questionID string) {
	var (
		q    = *query
		expr = `inquestion:(\d+)`
	)

	re := regexp.MustCompile(expr)
	res := re.FindStringSubmatch(q)
	if len(res) == 2 {
		questionID = res[1]
		q = re.ReplaceAllString(q, "")
	}

	*query = strings.TrimSpace(q)
	return
}

// parseIsAnswer check the result if only limit answer or not
func (sp *SearchParser) parseIsAnswer(query *string) (isAnswer bool) {
	var (
		q    = *query
		expr = `is:answer`
	)

	if strings.Contains(q, expr) {
		isAnswer = true
		q = strings.ReplaceAll(q, expr, "")
	}

	*query = strings.TrimSpace(q)
	return
}
