package cmd

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"strconv"
	"strings"

	"github.com/tidbcloud/clinic-cli/internal/clinic"
)

const systemLogDatasourcesPath = "/data-proxy/api/v1/system-log-datasources"

type systemLogDatasource struct {
	UID      string `json:"uid"`
	Provider string `json:"provider"`
	Region   string `json:"region"`
}

type systemLogDatasourceCatalog struct {
	Status string                          `json:"status"`
	Data   *systemLogDatasourceCatalogData `json:"data"`
}

type systemLogDatasourceCatalogData struct {
	Datasources json.RawMessage `json:"datasources"`
}

type regionalLokiSelectorBoundary struct {
	start int
	end   int
}

type validatedRegionalLokiSelector struct {
	matchers       []string
	insertionPoint int
}

func isRegionalServerlessLokiTier(deployTypeV2 string) bool {
	switch normalizeDeployType(deployTypeV2) {
	case "starter", "essential":
		return true
	default:
		return false
	}
}

func resolveLokiQueryClusterReference(ctx *RuntimeContext, clusterID string) (clusterReference, error) {
	if profile, ok := selectedProfileClusterReferenceWithMetadataByID(ctx, clusterID); ok {
		if !isRegionalServerlessLokiTier(profile.DeployType()) {
			return profile, nil
		}

		refreshed, err := resolveClusterReferenceByIDFromAPI(ctx, clusterID)
		if err != nil {
			return clusterReference{}, fmt.Errorf("refresh Loki routing metadata for cluster %s: %w", clusterID, err)
		}
		if refreshed.ClusterID != clusterID {
			return clusterReference{}, fmt.Errorf("refreshed Loki routing metadata cluster ID %q does not match selected cluster %s", refreshed.ClusterID, clusterID)
		}
		if strings.TrimSpace(refreshed.DeployType()) == "" {
			return clusterReference{}, fmt.Errorf("refreshed Loki routing metadata for cluster %s is missing clusterDeployTypeV2", clusterID)
		}
		return refreshed, nil
	}

	clusterRef, err := resolveClusterReferenceByIDFromAPI(ctx, clusterID)
	if err != nil {
		return clusterReference{}, fmt.Errorf("resolve Loki routing metadata for cluster %s: %w", clusterID, err)
	}
	if clusterRef.ClusterID != clusterID {
		return clusterReference{}, fmt.Errorf("Loki routing metadata cluster ID %q does not match selected cluster %s", clusterRef.ClusterID, clusterID)
	}
	return clusterRef, nil
}

func validateRegionalLokiQuery(query string) (validatedRegionalLokiSelector, error) {
	boundaries, err := regionalLokiSelectorBoundaries(query)
	if err != nil {
		return validatedRegionalLokiSelector{}, err
	}
	if len(boundaries) != 1 {
		return validatedRegionalLokiSelector{}, fmt.Errorf("regional system-log queries require exactly one selector with a non-empty equality physical cluster=\"...\" matcher; found %d selectors", len(boundaries))
	}
	selector := inspectRegionalLokiSelector(query, boundaries[0])

	clusterMatchers := 0
	for _, matcher := range selector.matchers {
		label, operator, rawValue, ok := parseRegionalLokiLabelMatcher(strings.TrimSpace(matcher))
		if !ok || label != "cluster" {
			continue
		}
		clusterMatchers++
		if operator != "=" {
			return validatedRegionalLokiSelector{}, fmt.Errorf("regional system-log queries require a non-empty equality physical cluster=\"...\" matcher; cluster uses %q", operator)
		}

		rawValue = strings.TrimSpace(rawValue)
		if len(rawValue) < 2 || rawValue[0] != '"' || rawValue[len(rawValue)-1] != '"' {
			return validatedRegionalLokiSelector{}, errors.New("regional system-log queries require a quoted non-empty equality physical cluster=\"...\" matcher")
		}
		value, err := strconv.Unquote(rawValue)
		if err != nil || strings.TrimSpace(value) == "" {
			return validatedRegionalLokiSelector{}, errors.New("regional system-log queries require a valid non-empty equality physical cluster=\"...\" matcher")
		}
	}

	if clusterMatchers != 1 {
		return validatedRegionalLokiSelector{}, fmt.Errorf("regional system-log queries require exactly one non-empty equality physical cluster=\"...\" matcher; found %d", clusterMatchers)
	}
	return selector, nil
}

func regionalLokiSelectorBoundaries(query string) ([]regionalLokiSelectorBoundary, error) {
	boundaries := make([]regionalLokiSelectorBoundary, 0, 1)
	for i := 0; i < len(query); {
		switch query[i] {
		case '#':
			i = skipLokiLineComment(query, i)
			continue
		case '"':
			next, ok := skipQuotedLokiText(query, i)
			if !ok {
				return nil, errors.New("regional system-log query contains an unterminated quoted string")
			}
			i = next
			continue
		case '`':
			next, ok := skipBacktickLokiRawString(query, i)
			if !ok {
				return nil, errors.New("regional system-log query contains an unterminated backtick raw string")
			}
			i = next
			continue
		}
		if query[i] != '{' {
			i++
			continue
		}

		start := i
		i++
		for i < len(query) {
			switch query[i] {
			case '#':
				i = skipLokiLineComment(query, i)
			case '"':
				next, ok := skipQuotedLokiText(query, i)
				if !ok {
					return nil, errors.New("regional system-log selector contains an unterminated quoted string")
				}
				i = next
			case '`':
				next, ok := skipBacktickLokiRawString(query, i)
				if !ok {
					return nil, errors.New("regional system-log selector contains an unterminated backtick raw string")
				}
				i = next
			case '}':
				boundaries = append(boundaries, regionalLokiSelectorBoundary{start: start, end: i})
				i++
				goto selectorComplete
			case '{':
				return nil, errors.New("regional system-log query contains a malformed selector")
			default:
				i++
			}
		}
		return nil, errors.New("regional system-log query contains an unterminated selector")

	selectorComplete:
	}
	return boundaries, nil
}

func inspectRegionalLokiSelector(query string, boundary regionalLokiSelectorBoundary) validatedRegionalLokiSelector {
	matchers := make([]string, 0, 4)
	var matcher strings.Builder
	insertionPoint := boundary.start + 1

	for i := boundary.start + 1; i < boundary.end; {
		switch query[i] {
		case '#':
			matcher.WriteByte('\n')
			i = skipLokiLineComment(query, i)
		case '"':
			next, _ := skipQuotedLokiText(query, i)
			matcher.WriteString(query[i:next])
			insertionPoint = next
			i = next
		case '`':
			next, _ := skipBacktickLokiRawString(query, i)
			matcher.WriteString(query[i:next])
			insertionPoint = next
			i = next
		case ',':
			matchers = append(matchers, matcher.String())
			matcher.Reset()
			i++
		default:
			matcher.WriteByte(query[i])
			if !isLokiWhitespace(query[i]) {
				insertionPoint = i + 1
			}
			i++
		}
	}
	matchers = append(matchers, matcher.String())

	return validatedRegionalLokiSelector{
		matchers:       matchers,
		insertionPoint: insertionPoint,
	}
}

func applyDefaultRegionalLokiSelectorLabels(
	ctx *RuntimeContext,
	query string,
	selector validatedRegionalLokiSelector,
	includeNamespace bool,
) string {
	hasClusterEnv := regionalLokiSelectorHasMatcher(selector, "cluster_env")
	hasNamespace := regionalLokiSelectorHasMatcher(selector, "namespace")
	missingClusterEnv := !hasClusterEnv
	missingNamespace := includeNamespace && !hasNamespace
	if !missingClusterEnv && !missingNamespace {
		return query
	}

	missingLabels := make([]string, 0, 2)
	if missingClusterEnv {
		missingLabels = append(missingLabels, fmt.Sprintf(`cluster_env=%q`, buildDefaultLokiClusterEnv(ctx)))
	}
	if missingNamespace {
		missingLabels = append(missingLabels, `namespace="tidb-serverless"`)
	}

	insertion := ", " + strings.Join(missingLabels, ", ")
	return query[:selector.insertionPoint] + insertion + query[selector.insertionPoint:]
}

func regionalLokiSelectorHasMatcher(selector validatedRegionalLokiSelector, wantLabel string) bool {
	for _, matcher := range selector.matchers {
		label, _, _, ok := parseRegionalLokiLabelMatcher(strings.TrimSpace(matcher))
		if ok && label == wantLabel {
			return true
		}
	}
	return false
}

func skipLokiLineComment(value string, hashIndex int) int {
	newline := strings.IndexByte(value[hashIndex+1:], '\n')
	if newline < 0 {
		return len(value)
	}
	return hashIndex + newline + 2
}

func isLokiWhitespace(value byte) bool {
	switch value {
	case ' ', '\t', '\n', '\r', '\f':
		return true
	default:
		return false
	}
}

func skipQuotedLokiText(value string, quoteIndex int) (int, bool) {
	escaped := false
	for i := quoteIndex + 1; i < len(value); i++ {
		if escaped {
			escaped = false
			continue
		}
		switch value[i] {
		case '\\':
			escaped = true
		case '"':
			return i + 1, true
		}
	}
	return len(value), false
}

func skipBacktickLokiRawString(value string, quoteIndex int) (int, bool) {
	for i := quoteIndex + 1; i < len(value); i++ {
		if value[i] == '`' {
			return i + 1, true
		}
	}
	return len(value), false
}

func validateRegionalLokiMetadata(clusterRef clusterReference) error {
	clusterID := clusterRef.ClusterID
	if strings.TrimSpace(clusterRef.ClusterProviderName) == "" {
		return fmt.Errorf("cluster %s regional system-log routing metadata is missing clusterProviderName", clusterID)
	}
	if strings.TrimSpace(clusterRef.ClusterRegionName) == "" {
		return fmt.Errorf("cluster %s regional system-log routing metadata is missing clusterRegionName", clusterID)
	}
	return nil
}

func resolveSystemLogDatasource(ctx *RuntimeContext, clusterID, provider, region string) (string, error) {
	var catalog systemLogDatasourceCatalog
	if err := doClinicRequest(ctx, clinic.Request{
		Method: http.MethodGet,
		Path:   systemLogDatasourcesPath,
	}, &catalog); err != nil {
		var syntaxErr *json.SyntaxError
		var typeErr *json.UnmarshalTypeError
		if errors.As(err, &syntaxErr) || errors.As(err, &typeErr) {
			return "", fmt.Errorf("cluster %s system-log datasource catalog contract could not be decoded: %w", clusterID, err)
		}
		return "", fmt.Errorf("fetch system-log datasource catalog for cluster %s provider %q region %q: %w", clusterID, provider, region, err)
	}
	if catalog.Status == "" {
		return "", fmt.Errorf("cluster %s system-log datasource catalog contract is missing status", clusterID)
	}
	if catalog.Status != "success" {
		return "", fmt.Errorf("cluster %s system-log datasource catalog contract returned non-success status %q", clusterID, catalog.Status)
	}
	if catalog.Data == nil || len(catalog.Data.Datasources) == 0 || string(catalog.Data.Datasources) == "null" {
		return "", fmt.Errorf("cluster %s system-log datasource catalog contract is missing data.datasources", clusterID)
	}

	var datasources []systemLogDatasource
	if err := json.Unmarshal(catalog.Data.Datasources, &datasources); err != nil {
		return "", fmt.Errorf("cluster %s system-log datasource catalog contract has invalid data.datasources: %w", clusterID, err)
	}

	matches := make([]systemLogDatasource, 0, 1)
	for _, datasource := range datasources {
		if strings.EqualFold(strings.TrimSpace(datasource.Provider), strings.TrimSpace(provider)) &&
			strings.EqualFold(strings.TrimSpace(datasource.Region), strings.TrimSpace(region)) {
			matches = append(matches, datasource)
		}
	}

	switch len(matches) {
	case 0:
		return "", fmt.Errorf("cluster %s has no system-log datasource for provider %q region %q", clusterID, provider, region)
	case 1:
		uid := strings.TrimSpace(matches[0].UID)
		if uid == "" {
			return "", fmt.Errorf("cluster %s system-log datasource for provider %q region %q has an empty uid", clusterID, provider, region)
		}
		return uid, nil
	default:
		return "", fmt.Errorf("cluster %s has multiple system-log datasources for provider %q region %q", clusterID, provider, region)
	}
}

func systemLogQueryRangeRequest(datasourceUID string, query url.Values) clinic.Request {
	const prefix = systemLogDatasourcesPath + "/"
	const suffix = "/logs/query-range"
	return clinic.Request{
		Method:  http.MethodGet,
		Path:    prefix + datasourceUID + suffix,
		RawPath: prefix + url.PathEscape(datasourceUID) + suffix,
		Query:   query,
	}
}
