package languages

import (
	"strings"

	sitter "github.com/zzet/gortex/internal/parser/tsitter"

	"github.com/zzet/gortex/internal/graph"
	"github.com/zzet/gortex/internal/parser"
)

// goLogMethods is the set of method names that, when called as a
// selector (`slog.Info("…")`, `logger.Error("…")`), are treated as
// log emissions for the observability coverage domain. The list is
// intentionally cross-library — slog, zap, zerolog, logrus, log15
// and most internal wrappers all share the same naming, so the
// heuristic catches a wide range without per-provider plumbing.
//
// False positives are possible (a domain function named Info that
// takes a string), but in practice the cost is a few extraneous
// KindEvent nodes — the gate strips them when the user disables
// observability.
var goLogMethods = map[string]string{
	"Debug":   "log",
	"Info":    "log",
	"Warn":    "log",
	"Warning": "log",
	"Error":   "log",
	"Fatal":   "log",
	"Panic":   "log",
	"Trace":   "log",
}

// goObservabilityEvent is a deferred record of one log call. The
// event name is the call's first string-literal argument; calls
// whose first arg is not a literal (e.g. dynamic format strings)
// are simply not recorded — agents can grep for those if they need
// to.
type goObservabilityEvent struct {
	method string // matched log method
	name   string // event name, taken from the first string-literal arg
	line   int    // 1-based line of the call expression
}

// detectGoLogEvent inspects a callm.expr capture and, when it
// matches the log heuristic, returns the resolved event name and
// the log-method classification. ok=false when the call is not a
// log emission.
//
// The check is deliberately strict: method must be in goLogMethods,
// and the first argument must be a string literal. Variadic /
// formatted overloads (`slog.Info(ctx, msg, ...)`) still match as
// long as the *first* string-literal argument is found anywhere in
// the leading arg list — slog passes `msg` as the first string
// after a context, while package-level slog accepts msg directly.
func detectGoLogEvent(callExpr *sitter.Node, method string, src []byte) (string, bool) {
	if callExpr == nil {
		return "", false
	}
	if _, ok := goLogMethods[method]; !ok {
		return "", false
	}
	args := callExpr.ChildByFieldName("arguments")
	if args == nil {
		return "", false
	}
	for i := 0; i < int(args.NamedChildCount()); i++ {
		c := args.NamedChild(i)
		if c == nil {
			continue
		}
		if c.Type() != "interpreted_string_literal" && c.Type() != "raw_string_literal" {
			continue
		}
		text := strings.Trim(c.Content(src), "\"`")
		if text == "" {
			return "", false
		}
		return text, true
	}
	return "", false
}

// emitGoObservabilityEvents emits one KindEvent per deferred event
// plus the EdgeEmits edges from the enclosing function to that
// event. Event nodes share IDs across files in a repo — the same
// "user.signup" event name produces a single node that all emitters
// link to, which is the intended graph topology for the
// observability domain.
//
// callerLookup maps a 1-based line to the enclosing function ID.
func emitGoObservabilityEvents(events []goObservabilityEvent, callerLookup func(line int) string, filePath string, result *parser.ExtractionResult) {
	if len(events) == 0 {
		return
	}
	seen := make(map[string]struct{}, len(events))
	seenStringNodes := make(map[string]struct{}, len(events))
	seenStringEdges := make(map[string]struct{}, len(events))
	for _, e := range events {
		callerID := callerLookup(e.line)
		if callerID == "" {
			continue
		}
		eventID := goEventNodeID(e.name)
		if _, ok := seen[eventID]; !ok {
			seen[eventID] = struct{}{}
			result.Nodes = append(result.Nodes, &graph.Node{
				ID:       eventID,
				Kind:     graph.KindEvent,
				Name:     e.name,
				FilePath: filePath, // first sighting; not authoritative
				Language: "go",
				Meta: map[string]any{
					"event_kind": "log",
					"name":       e.name,
				},
			})
		}
		result.Edges = append(result.Edges, &graph.Edge{
			From:     callerID,
			To:       eventID,
			Kind:     graph.EdgeEmits,
			FilePath: filePath,
			Line:     e.line,
			Origin:   graph.OriginASTInferred,
			Meta: map[string]any{
				"method": e.method,
			},
		})
		// Anchor a KindString context="log_message" registry node so
		// the log_events analyzer can aggregate emitters by raw
		// literal (richer than the canonical KindEvent ID).
		// Meta["level"] is the matched method's severity bucket
		// (every entry in goLogMethods today resolves to "log", but
		// the explicit field future-proofs a per-severity split —
		// Debug→debug, Error→error). Meta["event"] back-links to the
		// KindEvent ID so the registry view and the event view stay
		// round-trippable.
		strID := goStringNodeID(stringCtxLogMessage, e.name)
		if _, dup := seenStringNodes[strID]; !dup {
			seenStringNodes[strID] = struct{}{}
			result.Nodes = append(result.Nodes, &graph.Node{
				ID:       strID,
				Kind:     graph.KindString,
				Name:     e.name,
				FilePath: filePath,
				Language: "go",
				Meta: map[string]any{
					"context": string(stringCtxLogMessage),
					"value":   e.name,
					"level":   goLogMethods[e.method],
					"event":   eventID,
				},
			})
		}
		edgeKey := callerID + "->" + strID
		if _, dup := seenStringEdges[edgeKey]; !dup {
			seenStringEdges[edgeKey] = struct{}{}
			result.Edges = append(result.Edges, &graph.Edge{
				From:     callerID,
				To:       strID,
				Kind:     graph.EdgeEmits,
				FilePath: filePath,
				Line:     e.line,
				Origin:   graph.OriginASTInferred,
				Meta: map[string]any{
					"context": string(stringCtxLogMessage),
					"method":  e.method,
					"level":   goLogMethods[e.method],
				},
			})
		}
	}
}

// goEventNodeID is the canonical ID for a log/metric/trace event
// node. The "event::" prefix matches the synthetic-ID convention
// the exporter recognises (alongside `module::`, `external::`,
// `annotation::`, `unresolved::`), so cross-file events
// de-duplicate even without a real source-file backing.
func goEventNodeID(name string) string {
	return "event::log::" + name
}
