package cmd

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

var (
	diagnoseLevelPattern       = regexp.MustCompile(`\[(TRACE|DEBUG|INFO|WARN|WARNING|ERROR|FATAL)\]`)
	diagnoseFileLinePattern    = regexp.MustCompile(`\[([A-Za-z0-9_./-]+\.go):([0-9]+)\]`)
	diagnoseLeadingTimePattern = regexp.MustCompile(`^\d{4}[/-]\d{2}[/-]\d{2}\s+\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:\s+(?:Z|[+-]\d{2}:?\d{2}))?\s*`)
)

func normalizeDiagnoseLogEntries(body []byte, seed string, queryIDs []string, location *time.Location) ([]DiagnoseLogEntry, []DiagnoseWarning, error) {
	if location == nil {
		location = time.Local
	}

	var response lokiQueryRangeResponse
	if err := json.Unmarshal(body, &response); err != nil {
		return nil, nil, fmt.Errorf("decode loki response: %w", err)
	}
	if response.Data.ResultType != "" && response.Data.ResultType != "streams" {
		return nil, nil, fmt.Errorf("unexpected loki result type: %s", response.Data.ResultType)
	}

	dedup := make(map[string]DiagnoseLogEntry)
	for _, stream := range response.Data.Result {
		streamLabels := copyStringMap(stream.Stream)
		if streamLabels == nil {
			streamLabels = map[string]string{}
		}
		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
			}
			rawLine := strings.TrimRight(value[1], "\r\n")
			entry := buildDiagnoseLogEntry(time.Unix(0, ns).UTC(), rawLine, seed, streamLabels, streamKey, queryIDs, location)
			key := diagnoseLogEntryDedupKey(ns, entry.Component, entry.Instance, streamKey, rawLine)
			if existing, ok := dedup[key]; ok {
				existing.QueryIDs = mergeDiagnoseQueryIDs(existing.QueryIDs, queryIDs)
				dedup[key] = existing
				continue
			}
			dedup[key] = entry
		}
	}

	entries := make([]DiagnoseLogEntry, 0, len(dedup))
	for _, entry := range dedup {
		entries = append(entries, entry)
	}
	sort.Slice(entries, func(i, j int) bool {
		if entries[i].TimeUTC != entries[j].TimeUTC {
			return entries[i].TimeUTC < entries[j].TimeUTC
		}
		if entries[i].Stream != entries[j].Stream {
			return entries[i].Stream < entries[j].Stream
		}
		return entries[i].Raw < entries[j].Raw
	})

	warnings := make([]DiagnoseWarning, 0)
	for i := range entries {
		entries[i].ID = fmt.Sprintf("log-%03d", i+1)
		if isDiagnoseLogParseFailure(entries[i]) {
			warnings = append(warnings, DiagnoseWarning{
				Code:    "parse_failure",
				Source:  entries[i].ID,
				QueryID: firstDiagnoseQueryID(entries[i].QueryIDs),
				Message: "log line preserved but structured fields could not be parsed",
			})
		}
		if len(entries[i].Redactions) > 0 {
			warnings = append(warnings, DiagnoseWarning{
				Code:    "redaction",
				Source:  entries[i].ID,
				QueryID: firstDiagnoseQueryID(entries[i].QueryIDs),
				Message: "log line contains redaction indicators",
			})
		}
	}

	return entries, warnings, nil
}

func buildDiagnoseLogEntry(ts time.Time, rawLine string, seed string, labels map[string]string, streamKey string, queryIDs []string, location *time.Location) DiagnoseLogEntry {
	fields := parseDiagnoseFields(rawLine)
	if fields == nil {
		fields = map[string]string{}
	}
	level, file, lineNo, message := parseDiagnoseLogMetadata(rawLine)
	anchors := extractDiagnoseLogAnchors(rawLine, seed, labels, fields)

	entry := DiagnoseLogEntry{
		Kind:       "log",
		TimeUTC:    ts.UTC().Format(time.RFC3339Nano),
		TimeLocal:  ts.In(location).Format(time.RFC3339Nano),
		Component:  firstNonEmpty(streamLabelValue(labels, "component"), diagnoseFieldValue(fields, "component")),
		Instance:   firstNonEmpty(streamLabelValue(labels, "instance"), diagnoseFieldValue(fields, "instance")),
		Node:       firstNonEmpty(streamLabelValue(labels, "node"), diagnoseFieldValue(fields, "node")),
		Stream:     streamKey,
		Level:      level,
		File:       file,
		LineNo:     lineNo,
		Message:    message,
		Raw:        rawLine,
		Labels:     copyStringMap(labels),
		Fields:     fields,
		Anchors:    anchors,
		Redactions: detectDiagnoseRedactions(rawLine),
		QueryIDs:   mergeDiagnoseQueryIDs(nil, queryIDs),
	}
	if entry.Labels == nil {
		entry.Labels = map[string]string{}
	}
	if entry.Anchors == nil {
		entry.Anchors = map[string]any{}
	}
	return entry
}

func parseDiagnoseLogMetadata(line string) (string, string, int, string) {
	level := ""
	if match := diagnoseLevelPattern.FindStringSubmatch(line); len(match) == 2 {
		level = match[1]
		if level == "WARNING" {
			level = "WARN"
		}
	}

	file := ""
	lineNo := 0
	if match := diagnoseFileLinePattern.FindStringSubmatch(line); len(match) == 3 {
		file = match[1]
		parsed, err := strconv.Atoi(match[2])
		if err == nil {
			lineNo = parsed
		}
	}

	message := diagnoseLeadingTimePattern.ReplaceAllString(line, "")
	message = diagnoseLevelPattern.ReplaceAllString(message, "")
	message = diagnoseFileLinePattern.ReplaceAllString(message, "")
	message = diagnoseBracketFieldPattern.ReplaceAllString(message, "")
	message = strings.Join(strings.Fields(message), " ")
	return level, file, lineNo, message
}

func extractDiagnoseLogAnchors(line string, seed string, labels, fields map[string]string) map[string]any {
	anchors := make(map[string]any)
	addDiagnoseStringAnchor(anchors, "conn", firstAnchorValue(fields, labels, "conn", "connID", "conn_id"))
	addDiagnoseStringAnchor(anchors, "conn_id", firstAnchorValue(fields, labels, "conn_id", "connID", "conn"))
	addDiagnoseStringAnchor(anchors, "txn_start_ts", firstAnchorValue(fields, labels, "txn_start_ts", "txnStartTS"))
	addDiagnoseStringAnchor(anchors, "timestamp_or_start_ts", firstAnchorValue(fields, labels, "timestamp_or_start_ts", "timestamp", "start_ts", "startTS"))
	addDiagnoseStringAnchor(anchors, "region_id", firstAnchorValue(fields, labels, "region_id", "regionID", "region"))
	addDiagnoseStringAnchor(anchors, "region_version", firstAnchorValue(fields, labels, "region_version", "regionVersion"))
	addDiagnoseStringAnchor(anchors, "store_addr", firstAnchorValue(fields, labels, "store_addr", "storeAddr"))
	addDiagnoseStringAnchor(anchors, "peer_addr", firstAnchorValue(fields, labels, "peer_addr", "peerAddr"))
	addDiagnoseStringAnchor(anchors, "keyspace", firstAnchorValue(fields, labels, "keyspace", "keyspaceName"))
	addDiagnoseStringAnchor(anchors, "cluster_id", firstAnchorValue(fields, labels, "cluster_id", "clusterID"))
	addDiagnoseStringAnchor(anchors, "tenant_id", firstAnchorValue(fields, labels, "tenant_id", "tenantID"))
	addDiagnoseStringAnchor(anchors, "namespace", firstAnchorValue(fields, labels, "namespace"))
	addDiagnoseStringAnchor(anchors, "instance", firstAnchorValue(fields, labels, "instance"))
	addDiagnoseStringAnchor(anchors, "node", firstAnchorValue(fields, labels, "node"))
	addDiagnoseStringAnchor(anchors, "component", firstAnchorValue(fields, labels, "component"))
	addDiagnoseStringAnchor(anchors, "sql", firstAnchorValue(fields, labels, "sql"))
	addDiagnoseStringAnchor(anchors, "sql_digest", firstAnchorValue(fields, labels, "sql_digest", "sqlDigest"))
	addDiagnoseStringAnchor(anchors, "database", firstAnchorValue(fields, labels, "database", "db"))
	addDiagnoseStringSliceAnchor(anchors, "table", collectDiagnoseTables(fields, labels))
	addDiagnoseStringAnchor(anchors, "job_id", firstAnchorValue(fields, labels, "job_id", "jobID"))
	addDiagnoseStringAnchor(anchors, "task_id", firstAnchorValue(fields, labels, "task_id", "taskID"))
	addDiagnoseStringAnchor(anchors, "subtask_id", firstAnchorValue(fields, labels, "subtask_id", "subtaskID"))
	addDiagnoseStringAnchor(anchors, "request_id", firstAnchorValue(fields, labels, "request_id", "requestID"))
	addDiagnoseStringAnchor(anchors, "trace_id", firstAnchorValue(fields, labels, "trace_id", "traceID"))
	addDiagnoseStringSliceAnchor(anchors, "stack_frames", extractDiagnoseStackFrames(line))
	addDiagnoseStringAnchor(anchors, "error_text", extractDiagnoseErrorText(line, seed))
	return anchors
}

func firstAnchorValue(fields, labels map[string]string, aliases ...string) string {
	if value := diagnoseFieldValue(fields, aliases...); value != "" {
		return value
	}
	for _, alias := range aliases {
		if value := streamLabelValue(labels, alias); value != "" {
			return value
		}
	}
	return ""
}

func collectDiagnoseTables(fields, labels map[string]string) []string {
	seen := make(map[string]struct{})
	tables := make([]string, 0)
	for _, value := range []string{
		diagnoseFieldValue(fields, "table", "tableName"),
		streamLabelValue(labels, "table"),
		streamLabelValue(labels, "tableName"),
	} {
		addDiagnoseTableValues(&tables, seen, value)
	}
	if sql := diagnoseFieldValue(fields, "sql"); sql != "" {
		addDiagnoseTableValues(&tables, seen, strings.Join(extractDiagnoseSQLTableNames(normalizeDiagnoseSQL(sql)), ","))
	}
	return tables
}

func addDiagnoseTableValues(tables *[]string, seen map[string]struct{}, raw string) {
	for _, part := range strings.FieldsFunc(raw, func(r rune) bool {
		return r == ',' || r == ' ' || r == '\t'
	}) {
		table := strings.TrimSpace(part)
		if table == "" {
			continue
		}
		if _, ok := seen[table]; ok {
			continue
		}
		seen[table] = struct{}{}
		*tables = append(*tables, table)
	}
}

func addDiagnoseStringAnchor(anchors map[string]any, key string, value string) {
	value = strings.TrimSpace(value)
	if value != "" {
		anchors[key] = value
	}
}

func addDiagnoseStringSliceAnchor(anchors map[string]any, key string, values []string) {
	if len(values) > 0 {
		anchors[key] = values
	}
}

func detectDiagnoseRedactions(line string) []string {
	indicators := []string{`"[?,?]"`, "[?, ?)", "[?, ?]", "[?,?)", "[?,?]", "?"}
	found := make([]string, 0)
	for _, indicator := range indicators {
		if strings.Contains(line, indicator) {
			found = append(found, indicator)
		}
	}
	return found
}

func diagnoseLogEntryDedupKey(ns int64, component, instance, streamKey, rawLine string) string {
	sum := sha256.Sum256([]byte(rawLine))
	return strings.Join([]string{
		strconv.FormatInt(ns, 10),
		component,
		instance,
		streamKey,
		hex.EncodeToString(sum[:]),
	}, "\x00")
}

func mergeDiagnoseQueryIDs(existing []string, additions []string) []string {
	seen := make(map[string]struct{}, len(existing)+len(additions))
	merged := make([]string, 0, len(existing)+len(additions))
	for _, id := range append(existing, additions...) {
		id = strings.TrimSpace(id)
		if id == "" {
			continue
		}
		if _, ok := seen[id]; ok {
			continue
		}
		seen[id] = struct{}{}
		merged = append(merged, id)
	}
	return merged
}

func firstDiagnoseQueryID(queryIDs []string) string {
	if len(queryIDs) == 0 {
		return ""
	}
	return queryIDs[0]
}

func isDiagnoseLogParseFailure(entry DiagnoseLogEntry) bool {
	return len(entry.Fields) == 0 && entry.Level == "" && entry.File == "" && entry.Message == strings.TrimSpace(entry.Raw)
}

func firstNonEmpty(values ...string) string {
	for _, value := range values {
		if strings.TrimSpace(value) != "" {
			return strings.TrimSpace(value)
		}
	}
	return ""
}
