package cmd

import (
	"encoding/json"
	"fmt"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"time"
)

var (
	diagnoseBracketFieldPattern  = regexp.MustCompile(`\[(?P<key>[^\]=\s]+?)=(?P<value>[^\]]*)\]`)
	diagnoseKVFieldPattern       = regexp.MustCompile(`(?:^|[\s\[])([A-Za-z][A-Za-z0-9_]*?)=`)
	diagnoseTablePattern         = regexp.MustCompile(`\b(?:from|join|update|into)\s+([a-z0-9_.$]+)`)
	diagnoseFunctionFramePattern = regexp.MustCompile(`^[^\s=]*\.[^\s=]+$`)
	diagnoseGoFramePattern       = regexp.MustCompile(`(?i)\.go:\d+\b`)
	diagnoseErrorPattern         = regexp.MustCompile(`(?i)\b(?:panic:\s*.*|(?:[a-z0-9_.-]+\s+)?(?:error|failed|failure|fatal|timeout|timed out|exceeds?|exceeded)[^\n\r\[]*)`)
)

func normalizeDiagnoseEvidence(body []byte) ([]Evidence, error) {
	return normalizeDiagnoseEvidenceWithSeed(body, "")
}

func normalizeDiagnoseEvidenceWithSeed(body []byte, seed string) ([]Evidence, error) {
	var response lokiQueryRangeResponse
	if err := json.Unmarshal(body, &response); err != nil {
		return nil, fmt.Errorf("decode loki response: %w", err)
	}
	if response.Data.ResultType != "" && response.Data.ResultType != "streams" {
		return nil, fmt.Errorf("unexpected loki result type: %s", response.Data.ResultType)
	}

	dedup := make(map[string]Evidence)
	for _, stream := range response.Data.Result {
		streamLabels := copyStringMap(stream.Stream)
		streamKey := stableStreamLabelKey(streamLabels)
		for _, value := range stream.Values {
			if len(value) < 2 {
				continue
			}

			tsText := strings.TrimSpace(value[0])
			ns, err := strconv.ParseInt(tsText, 10, 64)
			if err != nil {
				continue
			}

			line := strings.TrimRight(value[1], "\r\n")
			fields := parseDiagnoseFields(line)
			evidence := Evidence{
				Time:   time.Unix(0, ns).UTC(),
				Line:   line,
				Fields: fields,
				Source: EvidenceSource{Stream: streamLabels},
			}
			populateDiagnoseComponentAndInstance(&evidence, streamLabels, fields)
			populateDiagnoseAnchors(&evidence, line, seed, fields)

			key := streamKey + "\x00" + tsText + "\x00" + line
			dedup[key] = evidence
		}
	}

	evidence := make([]Evidence, 0, len(dedup))
	for _, item := range dedup {
		evidence = append(evidence, item)
	}
	sort.Slice(evidence, func(i, j int) bool {
		leftKey := stableEvidenceSortKey(evidence[i])
		rightKey := stableEvidenceSortKey(evidence[j])
		if evidence[i].Time.Equal(evidence[j].Time) {
			if leftKey != rightKey {
				return leftKey < rightKey
			}
			return evidence[i].Line < evidence[j].Line
		}
		return evidence[i].Time.Before(evidence[j].Time)
	})
	for i := range evidence {
		evidence[i].ID = fmt.Sprintf("E%d", i+1)
	}

	return evidence, nil
}

func parseDiagnoseFields(line string) map[string]string {
	fields := make(map[string]string)
	bracketSpans := diagnoseBracketFieldPattern.FindAllStringSubmatchIndex(line, -1)

	for _, match := range diagnoseBracketFieldPattern.FindAllStringSubmatch(line, -1) {
		if len(match) != 3 {
			continue
		}
		key := strings.TrimSpace(match[1])
		value := normalizeDiagnoseFieldValue(key, match[2])
		if key != "" {
			fields[key] = value
		}
	}

	keyMatches := diagnoseKVFieldPattern.FindAllStringSubmatchIndex(line, -1)
	for i, match := range keyMatches {
		if len(match) < 4 {
			continue
		}
		if diagnoseIndexInAnySpan(match[0], bracketSpans) {
			continue
		}
		key := strings.TrimSpace(line[match[2]:match[3]])
		if key == "" {
			continue
		}

		valueStart := match[1]
		valueEnd := len(line)
		if i+1 < len(keyMatches) {
			valueEnd = keyMatches[i+1][0]
		}
		if valueStart > valueEnd {
			continue
		}
		value := strings.TrimRight(line[valueStart:valueEnd], " \t,;)]")
		value = normalizeDiagnoseFieldValue(key, value)
		if value != "" {
			fields[key] = value
		}
	}

	return fields
}

func diagnoseIndexInAnySpan(index int, spans [][]int) bool {
	for _, span := range spans {
		if len(span) < 2 {
			continue
		}
		if index >= span[0] && index < span[1] {
			return true
		}
	}
	return false
}

func normalizeDiagnoseFieldValue(key, value string) string {
	value = strings.TrimSpace(value)
	if !isDiagnoseScalarFieldKey(key) {
		return value
	}

	fields := strings.FieldsFunc(value, func(r rune) bool {
		return r == ' ' || r == '\t' || r == ',' || r == ';' || r == ')' || r == ']'
	})
	if len(fields) == 0 {
		return ""
	}
	return fields[0]
}

func isDiagnoseScalarFieldKey(key string) bool {
	switch strings.ToLower(strings.TrimSpace(key)) {
	case "conn", "connid", "conn_id",
		"txnstartts", "txn_start_ts",
		"regionid", "region_id", "region",
		"storeaddr", "store_addr",
		"keyspacename", "keyspace":
		return true
	default:
		return false
	}
}

func populateDiagnoseComponentAndInstance(evidence *Evidence, streamLabels, fields map[string]string) {
	if evidence == nil {
		return
	}

	if evidence.Component == "" {
		if component := streamLabelValue(streamLabels, "component"); component != "" {
			evidence.Component = component
		} else if component := diagnoseFieldValue(fields, "component"); component != "" {
			evidence.Component = component
		}
	}

	if evidence.Instance == "" {
		if instance := streamLabelValue(streamLabels, "instance"); instance != "" {
			evidence.Instance = instance
		} else if instance := diagnoseFieldValue(fields, "instance"); instance != "" {
			evidence.Instance = instance
		}
	}
}

func populateDiagnoseAnchors(evidence *Evidence, line string, seed string, fields map[string]string) {
	if evidence == nil {
		return
	}

	evidence.Anchors.ConnID = diagnoseFieldValue(fields, "conn", "connID", "conn_id")
	evidence.Anchors.TxnStartTS = diagnoseFieldValue(fields, "txnStartTS", "txn_start_ts")
	if region := diagnoseFieldValue(fields, "regionID", "region_id", "region"); isNumericString(region) {
		evidence.Anchors.RegionID = region
	}
	evidence.Anchors.StoreAddr = diagnoseFieldValue(fields, "storeAddr", "store_addr")
	evidence.Anchors.Keyspace = diagnoseFieldValue(fields, "keyspaceName", "keyspace")

	if sql := diagnoseFieldValue(fields, "sql"); sql != "" {
		normalizedSQL := normalizeDiagnoseSQL(sql)
		evidence.Anchors.SQL = normalizedSQL
		evidence.Anchors.TableNames = extractDiagnoseSQLTableNames(normalizedSQL)
	}

	if text := extractDiagnoseErrorText(line, seed); text != "" {
		evidence.Anchors.ErrorText = text
	}

	evidence.Anchors.StackFrames = extractDiagnoseStackFrames(line)
}

func diagnoseFieldValue(fields map[string]string, aliases ...string) string {
	for _, alias := range aliases {
		for key, value := range fields {
			if strings.EqualFold(key, alias) {
				return strings.TrimSpace(value)
			}
		}
	}
	return ""
}

func normalizeDiagnoseSQL(sql string) string {
	return strings.ToLower(strings.Join(strings.Fields(sql), " "))
}

func extractDiagnoseSQLTableNames(sql string) []string {
	if sql == "" {
		return nil
	}

	seen := make(map[string]struct{})
	tables := make([]string, 0)
	for _, match := range diagnoseTablePattern.FindAllStringSubmatch(sql, -1) {
		if len(match) < 2 {
			continue
		}
		table := strings.TrimSpace(match[1])
		if table == "" {
			continue
		}
		if _, exists := seen[table]; exists {
			continue
		}
		seen[table] = struct{}{}
		tables = append(tables, table)
	}
	return tables
}

func extractDiagnoseErrorText(line string, seed string) string {
	if line == "" {
		return ""
	}

	normalizedSeed := normalizeDiagnoseErrorText(seed)
	if normalizedSeed != "" && strings.Contains(strings.ToLower(line), strings.ToLower(normalizedSeed)) {
		return normalizedSeed
	}

	if match := diagnoseErrorPattern.FindString(line); match != "" {
		return normalizeDiagnoseErrorText(match)
	}
	return ""
}

func normalizeDiagnoseErrorText(text string) string {
	return strings.Join(strings.Fields(strings.TrimSpace(text)), " ")
}

func extractDiagnoseStackFrames(line string) []string {
	if line == "" {
		return nil
	}

	seen := make(map[string]struct{})
	frames := make([]string, 0)
	for _, segment := range strings.Split(line, "\n") {
		frame := strings.TrimSpace(segment)
		if frame == "" {
			continue
		}

		if diagnoseGoFramePattern.MatchString(frame) || diagnoseFunctionFramePattern.MatchString(frame) {
			if _, exists := seen[frame]; exists {
				continue
			}
			seen[frame] = struct{}{}
			frames = append(frames, frame)
		}
	}

	return frames
}

func stableEvidenceSortKey(evidence Evidence) string {
	return stableStreamLabelKey(evidence.Source.Stream)
}

func stableStreamLabelKey(labels map[string]string) string {
	if len(labels) == 0 {
		return ""
	}

	keys := make([]string, 0, len(labels))
	for key := range labels {
		keys = append(keys, key)
	}
	sort.Strings(keys)

	parts := make([]string, 0, len(keys))
	for _, key := range keys {
		parts = append(parts, key+"="+labels[key])
	}
	return strings.Join(parts, "\x1f")
}

func copyStringMap(source map[string]string) map[string]string {
	if len(source) == 0 {
		return nil
	}

	copy := make(map[string]string, len(source))
	for key, value := range source {
		copy[key] = value
	}
	return copy
}

func streamLabelValue(labels map[string]string, key string) string {
	for candidate, value := range labels {
		if strings.EqualFold(candidate, key) {
			return strings.TrimSpace(value)
		}
	}
	return ""
}

func isNumericString(value string) bool {
	if value == "" {
		return false
	}
	for _, r := range value {
		if r < '0' || r > '9' {
			return false
		}
	}
	return true
}
