package searchstore

import (
	"bytes"
	"fmt"
	"strings"

	"github.com/grafana/grafana/pkg/services/featuremgmt"
	"github.com/grafana/grafana/pkg/services/search/model"
	"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
)

// Builder defaults to returning a SQL query to get a list of all dashboards
// in default order, but can be modified by applying filters.
type Builder struct {
	// List of FilterWhere/FilterGroupBy/FilterOrderBy/FilterLeftJoin
	// to modify the query.
	Filters  []any
	Dialect  migrator.Dialect
	Features featuremgmt.FeatureToggles

	params []any
	sql    bytes.Buffer
}

// ToSQL builds the SQL query and returns it as a string, together with the SQL parameters.
func (b *Builder) ToSQL(limit, page int64) (string, []any) {
	b.params = make([]any, 0)
	b.sql = bytes.Buffer{}

	b.buildSelect()

	b.sql.WriteString("( ")
	orderQuery := b.applyFilters()

	b.sql.WriteString(b.Dialect.LimitOffset(limit, (page-1)*limit) + `) AS ids
		INNER JOIN dashboard ON ids.id = dashboard.id`)
	b.sql.WriteString("\n")

	// covered by UQE_folder_org_id_uid
	b.sql.WriteString(
		`LEFT OUTER JOIN folder ON folder.uid = dashboard.folder_uid AND folder.org_id = dashboard.org_id`)
	b.sql.WriteString(`
	LEFT OUTER JOIN dashboard_tag ON dashboard.id = dashboard_tag.dashboard_id`)
	b.sql.WriteString("\n")
	b.sql.WriteString(orderQuery)

	return b.sql.String(), b.params
}

func (b *Builder) buildSelect() {
	var recQuery string
	var recQueryParams []any

	b.sql.WriteString(
		`SELECT
			dashboard.id,
			dashboard.org_id,
			dashboard.uid,
			dashboard.title,
			dashboard.slug,
			dashboard_tag.term,
			dashboard.is_folder,
			dashboard.folder_id,
			dashboard.deleted,
			folder.uid AS folder_uid,
			folder.title AS folder_slug,
			folder.title AS folder_title 
		`)
	for _, f := range b.Filters {
		if f, ok := f.(model.FilterSelect); ok {
			b.sql.WriteString(fmt.Sprintf(", %s", f.Select()))
		}

		if f, ok := f.(model.FilterWith); ok {
			recQuery, recQueryParams = f.With()
		}
	}

	b.sql.WriteString(` FROM `)

	if recQuery == "" {
		return
	}

	// prepend recursive queries
	var bf bytes.Buffer
	bf.WriteString(recQuery)
	bf.WriteString(b.sql.String())

	b.sql = bf
	b.params = append(recQueryParams, b.params...)
}

func (b *Builder) applyFilters() (ordering string) {
	joins := []string{}
	orderJoins := []string{}

	wheres := []string{}
	whereParams := []any{}

	groups := []string{}
	groupParams := []any{}

	orders := []string{}

	for _, f := range b.Filters {
		if f, ok := f.(model.FilterLeftJoin); ok {
			s := f.LeftJoin()
			if s != "" {
				joins = append(joins, fmt.Sprintf(" LEFT OUTER JOIN %s ", s))
			}
		}

		if f, ok := f.(model.FilterWhere); ok {
			sql, params := f.Where()
			if sql != "" {
				wheres = append(wheres, sql)
				whereParams = append(whereParams, params...)
			}
		}

		if f, ok := f.(model.FilterGroupBy); ok {
			sql, params := f.GroupBy()
			if sql != "" {
				groups = append(groups, sql)
				groupParams = append(groupParams, params...)
			}
		}

		if f, ok := f.(model.FilterOrderBy); ok {
			if f, ok := f.(model.FilterLeftJoin); ok {
				orderJoins = append(orderJoins, fmt.Sprintf(" LEFT OUTER JOIN %s ", f.LeftJoin()))
			}
			orders = append(orders, f.OrderBy())
		}
	}

	forceIndex := ""
	if b.Dialect.DriverName() == migrator.MySQL {
		forceIndex = " FORCE INDEX (IDX_dashboard_title) "
	}

	b.sql.WriteString(fmt.Sprintf("SELECT dashboard.id FROM dashboard %s", forceIndex))
	b.sql.WriteString(strings.Join(joins, ""))

	if len(wheres) > 0 {
		b.sql.WriteString(fmt.Sprintf(" WHERE %s", strings.Join(wheres, " AND ")))
		b.params = append(b.params, whereParams...)
	}

	if len(orders) < 1 {
		orders = append(orders, TitleSorter{}.OrderBy())
	}

	if len(groups) > 0 {
		cols := make([]string, 0, len(orders)+len(groups))
		for _, o := range orders {
			o := strings.TrimSuffix(o, " DESC")
			o = strings.TrimSuffix(o, " ASC")
			exists := false
			for _, g := range groups {
				if g == o {
					exists = true
					break
				}
			}
			if !exists {
				cols = append(cols, o)
			}
		}
		cols = append(cols, groups...)
		b.sql.WriteString(fmt.Sprintf(" GROUP BY %s", strings.Join(cols, ", ")))
		b.params = append(b.params, groupParams...)
	}

	orderByCols := []string{}
	for _, o := range orders {
		orderByCols = append(orderByCols, b.Dialect.OrderBy(o))
	}

	orderBy := fmt.Sprintf(" ORDER BY %s", strings.Join(orderByCols, ", "))
	b.sql.WriteString(orderBy)

	order := strings.Join(orderJoins, "")
	order += orderBy
	return order
}
