package queryparser

import (
	"context"
	"fmt"

	"strings"

	"github.com/SigNoz/govaluate"
	"github.com/SigNoz/signoz/pkg/errors"
	"github.com/SigNoz/signoz/pkg/factory"
	"github.com/SigNoz/signoz/pkg/queryparser/queryfilterextractor"
	qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
)

type queryParserImpl struct {
	settings factory.ProviderSettings
}

// New creates a new implementation of the QueryParser service.
func New(settings factory.ProviderSettings) QueryParser {
	return &queryParserImpl{
		settings: settings,
	}
}

func (p *queryParserImpl) AnalyzeQueryFilter(ctx context.Context, queryType qbtypes.QueryType, query string) (result *queryfilterextractor.FilterResult, err error) {
	// the third-party clickhouse sql parser can panic on certain inputs, recover gracefully
	defer func() {
		if r := recover(); r != nil {
			result = nil
			err = errors.NewInternalf(errors.CodeInternal, "failed to analyze query filter: %s", fmt.Sprint(r))
		}
	}()

	var extractorType queryfilterextractor.ExtractorType
	switch queryType {
	case qbtypes.QueryTypePromQL:
		extractorType = queryfilterextractor.ExtractorTypePromQL
	case qbtypes.QueryTypeClickHouseSQL:
		extractorType = queryfilterextractor.ExtractorTypeClickHouseSQL
	default:
		return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported queryType: %s. Supported values are '%s' and '%s'", queryType, qbtypes.QueryTypePromQL, qbtypes.QueryTypeClickHouseSQL)
	}

	// Create extractor
	extractor, err := queryfilterextractor.NewExtractor(extractorType)
	if err != nil {
		return nil, err
	}
	return extractor.Extract(query)
}

func (p *queryParserImpl) AnalyzeQueryEnvelopes(ctx context.Context, queries []qbtypes.QueryEnvelope) (map[string]*queryfilterextractor.FilterResult, error) {
	results := make(map[string]*queryfilterextractor.FilterResult)

	// formulaQueries store the formula queries in the order they are defined
	formulaQueries := make(map[string]qbtypes.QueryBuilderFormula)

	// First pass: Process non-formula queries
	for _, query := range queries {
		result := &queryfilterextractor.FilterResult{
			MetricNames:    []string{},
			GroupByColumns: []queryfilterextractor.ColumnInfo{},
		}
		var queryName string

		switch query.Type {
		case qbtypes.QueryTypeBuilder:
			switch spec := query.Spec.(type) {
			case qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]:
				queryName = spec.Name
				// extract group by fields
				for _, groupBy := range spec.GroupBy {
					if groupBy.Name != "" {
						result.GroupByColumns = append(result.GroupByColumns, queryfilterextractor.ColumnInfo{Name: groupBy.Name, OriginExpr: groupBy.Name, OriginField: groupBy.Name, Alias: groupBy.Name})
					}
				}
				// extract metric names
				for _, aggregation := range spec.Aggregations {
					if aggregation.MetricName != "" {
						result.MetricNames = append(result.MetricNames, aggregation.MetricName)
					}
				}
			default:
				// TODO(abhishekhugetech): add support for Traces and Logs Aggregation types
				p.settings.Logger.WarnContext(ctx, "unsupported QueryBuilderQuery type: ", spec)
				// Skip result for this query
				continue
			}
		case qbtypes.QueryTypePromQL:
			spec, ok := query.Spec.(qbtypes.PromQuery)
			if !ok || spec.Query == "" {
				// Skip result for this query
				continue
			}
			queryName = spec.Name
			res, err := p.AnalyzeQueryFilter(ctx, qbtypes.QueryTypePromQL, spec.Query)
			if err != nil {
				return nil, err
			}
			result.MetricNames = append(result.MetricNames, res.MetricNames...)
			result.GroupByColumns = append(result.GroupByColumns, res.GroupByColumns...)
		case qbtypes.QueryTypeClickHouseSQL:
			spec, ok := query.Spec.(qbtypes.ClickHouseQuery)
			if !ok || spec.Query == "" {
				// Skip result for this query
				continue
			}
			queryName = spec.Name
			res, err := p.AnalyzeQueryFilter(ctx, qbtypes.QueryTypeClickHouseSQL, spec.Query)
			if err != nil {
				return nil, err
			}
			result.MetricNames = append(result.MetricNames, res.MetricNames...)
			result.GroupByColumns = append(result.GroupByColumns, res.GroupByColumns...)
		case qbtypes.QueryTypeFormula:
			spec, ok := query.Spec.(qbtypes.QueryBuilderFormula)
			if !ok {
				// Skip result for this query
				continue
			}
			formulaQueries[spec.Name] = spec
		default:
			return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported query type: %s", query.Type)
		}

		if queryName != "" {
			results[queryName] = result
		}
	}

	// Second pass: Process formula queries
	for _, query := range formulaQueries {
		result := &queryfilterextractor.FilterResult{
			MetricNames:    []string{},
			GroupByColumns: []queryfilterextractor.ColumnInfo{},
		}

		// Parse the expression to find used queries
		expression, err := govaluate.NewEvaluableExpressionWithFunctions(query.Expression, qbtypes.EvalFuncs())
		if err != nil {
			return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "failed to parse formula expression %s: %v", query.Name, err)
		}

		uniqueMetricNames := make(map[string]bool)
		uniqueGroupByColumns := make(map[string]bool)

		vars := expression.Vars()
		for _, v := range vars {
			// variables can be "A" or "A.0" or "A.alias" as per pkg/types/querybuildertypes/querybuildertypesv5/formula.go
			parts := strings.Split(v, ".")
			if len(parts) > 0 {
				refQueryName := parts[0]
				if refResult, exists := results[refQueryName]; exists {
					for _, metricName := range refResult.MetricNames {
						if !uniqueMetricNames[metricName] {
							uniqueMetricNames[metricName] = true
							result.MetricNames = append(result.MetricNames, metricName)
						}
					}
					for _, groupByColumn := range refResult.GroupByColumns {
						if !uniqueGroupByColumns[groupByColumn.Name] {
							uniqueGroupByColumns[groupByColumn.Name] = true
							result.GroupByColumns = append(result.GroupByColumns, groupByColumn)
						}
					}
				}
			}
		}

		// Add the formula query filter result to the results map
		results[query.Name] = result
	}

	return results, nil
}
