/*
	Copyright (C) 2025  snoutie
	Authors: snoutie (copyright@achtarmig.org)
	This program is free software: you can redistribute it and/or modify
	it under the terms of the GNU Affero General Public License as published
	by the Free Software Foundation, either version 3 of the License, or
	(at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU Affero General Public License for more details.

	You should have received a copy of the GNU Affero General Public License
	along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/

package search

import (
	"api-cds-search/cmd/database/table"
	"api-cds-search/cmd/model"
	"errors"
	"fmt"
	"net/http"
	"slices"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/lithammer/fuzzysearch/fuzzy"
)

type fuzzyResult struct {
	CDSViewTechnicalName table.TechnicalName
	hits                 int
	perfectHits          int
	averageDistance      float32
	score                float64
}

type resultBuffer struct {
	createdAt time.Time
	hits      int
	mu        sync.Mutex
	results   []fuzzyResult
}

const pageStep = 12

var (
	ErrNoBuffer  = errors.New("no buffer")
	ErrNoResults = errors.New("no results")
)

var searchTargets map[table.TechnicalName][]string = make(map[table.TechnicalName][]string)
var resultsBuffer map[string]*resultBuffer = make(map[string]*resultBuffer)

func Load() error {
	keywords, err := model.GetKeywordsModel()
	if err != nil {
		return err
	}
	for _, keyword := range *keywords {
		table := searchTargets[keyword.CDSViewTechnicalName]
		table = append(table, keyword.Keyword)
		searchTargets[keyword.CDSViewTechnicalName] = table
	}

	ticker := time.NewTicker(5 * time.Minute)

	go func() {
		for {
			select {
			case <-ticker.C:
				ClearOldBuffers()
			}
		}
	}()

	return nil
}

func GetSearchTerm(q string) string {
	return strings.ToLower(q)
}

func GetPage(r *http.Request) int {
	page, _ := strconv.Atoi(r.URL.Query().Get("p"))
	if page < 0 {
		page = 0
	}
	return page
}

func GetBuffer(searchTerm string) (*resultBuffer, error) {
	buffer, exists := resultsBuffer[searchTerm]

	if !exists {
		return nil, ErrNoBuffer
	}

	return buffer, nil
}

func BuildBuffer(searchTerm string) error {
	if searchTerm == "" {
		return ErrNoResults
	}

	splitSearchTerms := strings.Split(searchTerm, " ")

	if len(splitSearchTerms) > 1 {
		splitSearchTerms = append(splitSearchTerms, searchTerm)
		splitSearchTerms = append(splitSearchTerms, strings.ReplaceAll(searchTerm, " ", ""))
	}

	var fuzzyResults []fuzzyResult

	for viewName, targets := range searchTargets {
		var (
			totalHits, perfectHits int
			totalDistance          float32
		)

		totalDistance = 1
		for i, splitSearchTerm := range splitSearchTerms {
			if splitSearchTerm == "" {
				continue
			}

			ranks := fuzzy.RankFindFold(splitSearchTerm, targets)

			hits := len(ranks)
			totalHits += hits

			if hits == 0 {
				totalDistance += 100
				continue
			}

			var averageDistance float32 = 0
			for _, rank := range ranks {
				averageDistance += float32(rank.Distance)
				if i >= len(splitSearchTerms)-2 && rank.Distance == 0 {
					perfectHits += 1
				}
			}

			totalDistance += averageDistance / float32(hits)
		}

		if totalHits > 0 {
			fuzzyResults = append(fuzzyResults, fuzzyResult{
				CDSViewTechnicalName: viewName,
				hits:                 totalHits,
				averageDistance:      totalDistance,
				perfectHits:          perfectHits,
				score:                (float64(totalDistance) / float64(totalHits)) * (float64(len(targets)) / float64(totalHits)),
			})
		}
	}

	hits := len(fuzzyResults)
	if hits == 0 {
		return ErrNoResults
	}

	slices.SortFunc(fuzzyResults, func(a, b fuzzyResult) int {
		if a.perfectHits < b.perfectHits {
			return 1
		} else if a.perfectHits > b.perfectHits {
			return -1
		}
		if a.score > b.score {
			return 1
		} else if a.score < b.score {
			return -1
		}
		if a.CDSViewTechnicalName > b.CDSViewTechnicalName {
			return 1
		} else if a.CDSViewTechnicalName < b.CDSViewTechnicalName {
			return -1
		}
		return 0
	})

	resultsBuffer[searchTerm] = &resultBuffer{
		createdAt: time.Now(),
		hits:      hits,
		results:   fuzzyResults,
	}

	return nil
}

func GetPages(buffer *resultBuffer, p int) (int, int) {
	var maxPage int
	if buffer.hits <= pageStep {
		maxPage = 0
	} else {
		maxPage = (buffer.hits / pageStep)
	}

	if p+1 > maxPage {
		return maxPage, maxPage
	} else if p < 0 {
		return 0, maxPage
	}
	return p, maxPage
}

func Search(buffer *resultBuffer, page int, maxPage int) *model.ResultsModel {
	buffer.mu.Lock()
	defer buffer.mu.Unlock()

	resultsModel := model.NewResultsModel()
	for i := page * pageStep; i < page*pageStep+pageStep; i++ {
		if i > len(buffer.results)-1 {
			break
		}
		result := buffer.results[i]
		cdsView, err := model.GetCDSViewModel(result.CDSViewTechnicalName)
		if err != nil {
			fmt.Println(err)
			continue
		}

		resultsModel.AppendResultsModelViews(cdsView, result.score)
	}

	resultsModel.CurrentPage = page
	resultsModel.MaxPage = maxPage

	return resultsModel
}

/*
	func HandleSearch(w http.ResponseWriter, r *http.Request) {
		searchTerm := GetSearchTerm(r)

		buffer, err := GetBuffer(searchTerm)
		if err != nil {
			err := BuildBuffer(searchTerm)
			if err != nil {
				err := ui.Template.ExecuteTemplate(w, "search-placeholder-no-result", nil)
				if err != nil {
					fmt.Println(err)
				}
				return
			}
			buffer, _ = GetBuffer(searchTerm)
		}

		buffer.mu.Lock()
		defer buffer.mu.Unlock()

		page := GetPage(r)
		var maxPage int
		if buffer.hits <= pageStep {
			maxPage = 0
		} else {
			maxPage = (buffer.hits / pageStep)
		}

		if page+1 > maxPage {
			page = maxPage
		}

		resultsModel := model.NewResultsModel()
		for i := page * pageStep; i < page*pageStep+pageStep; i++ {
			if i > len(buffer.results)-1 {
				break
			}
			result := buffer.results[i]
			cdsView, err := model.GetCDSViewModel(result.CDSViewTechnicalName)
			if err != nil {
				fmt.Println(err)
				continue
			}

			resultsModel.AppendResultsModelViews(cdsView, result.score)
		}

		resultsModel.SearchTerm = searchTerm
		resultsModel.CurrentPage = page
		resultsModel.MaxPage = maxPage

		err = ui.Template.ExecuteTemplate(w, "results", resultsModel)
		if err != nil {
			fmt.Println(err)
			return
		}
	}
*/
func ClearOldBuffers() {
	for key, buffer := range resultsBuffer {
		if time.Since(buffer.createdAt) >= time.Minute*10 {
			buffer.mu.Lock()
			delete(resultsBuffer, key)
			buffer.mu.Unlock()
		}
	}
}