package contracts

import (
	"strings"

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

// applyBodyFactsToHTTPContract overrides Meta keys with AST-derived
// facts when they're more authoritative than the regex enricher's
// output. Called after enrichHTTPContract has run; AST output wins
// on the keys it can confidently produce.
//
// Phase 1 invariant: this function may not regress an existing
// Meta value to a strictly less informative one — i.e. if the
// regex pass set response_type but the AST can't, leave the regex
// value alone. The AST only writes when it has a confident answer.
//
// Both provider and consumer contracts are handled. The semantics
// differ by Role:
//   - Provider: WriteJSON/Encode → response_type; Decode/BodyParser/
//     ShouldBind*/Unmarshal → request_type
//   - Consumer: Marshal/MarshalIndent → request_type;
//     Decode/Unmarshal → response_type (response from server is
//     decoded into a typed variable)
func applyBodyFactsToHTTPContract(c *Contract, fileNodes []*graph.Node, tree *parser.ParseTree) {
	if c == nil || tree == nil {
		return
	}
	handler := findContractHandlerNode(c, fileNodes)
	if handler == nil {
		return
	}
	bf := MakeBodyFacts(tree, handler)
	if _, isNop := bf.(nopBodyFacts); isNop {
		return
	}

	switch c.Role {
	case RoleProvider:
		applyProviderFacts(c, fileNodes, bf)
	case RoleConsumer:
		applyConsumerFacts(c, fileNodes, bf)
	}
}

// applyProviderFacts handles the server-side enrichment.
func applyProviderFacts(c *Contract, fileNodes []*graph.Node, bf BodyFacts) {
	if writes := bf.StatusWrites(); len(writes) > 0 {
		mergeIntStrings(c.Meta, "status_codes", writes)
	}
	if reads := bf.QueryReads(); len(reads) > 0 {
		mergeStringList(c.Meta, "query_params", reads)
	}
	// Request type: take the first non-Marshal request binding
	// (Marshal is consumer-side, see applyConsumerFacts).
	for _, rb := range bf.RequestBindings() {
		if rb.Helper == "Marshal" || rb.Helper == "MarshalIndent" {
			continue
		}
		applyRequestBindingToContract(c, fileNodes, bf, rb)
		break
	}
	calls := bf.ResponseCalls()
	if len(calls) == 0 {
		return
	}
	rc := lastSuccessResponseCall(calls)
	applyResponseCallToContract(c, fileNodes, bf, rc)
}

// applyConsumerFacts handles the client-side enrichment.
//   - Marshal(req)   → request_type from VarBinding(req).TypeID
//   - Decode(&out)   → response_type from VarBinding(out).TypeID
//   - Unmarshal(b,&out) → response_type from VarBinding(out).TypeID
//   - var anonymous composite (Marshal(&Req{}))
//     → request_type from CompositeType
func applyConsumerFacts(c *Contract, fileNodes []*graph.Node, bf BodyFacts) {
	for _, rb := range bf.RequestBindings() {
		switch rb.Helper {
		case "Marshal", "MarshalIndent":
			applyConsumerBinding(c, fileNodes, bf, rb, "request")
		case "Decode", "Unmarshal":
			applyConsumerBinding(c, fileNodes, bf, rb, "response")
		}
	}
}

// applyConsumerBinding stamps either request_type or response_type
// (per kind) from a binding's variable lookup.
func applyConsumerBinding(c *Contract, fileNodes []*graph.Node, bf BodyFacts, rb RequestBinding, kind string) {
	typeKey := "request_type"
	exprKey := "request_expr"
	if kind == "response" {
		typeKey = "response_type"
		exprKey = "response_expr"
	}
	if existing, _ := c.Meta[typeKey].(string); existing != "" {
		return
	}
	resolved := ""
	repeated := false
	switch {
	case rb.CompositeType != "":
		resolved = resolveTypeInFile(rb.CompositeType, fileNodes)
	case rb.VarName != "":
		b := bf.VarBinding(rb.VarName)
		if b.TypeID != "" {
			resolved = resolveTypeInFile(b.TypeID, fileNodes)
			repeated = b.Repeated
		}
	}
	if resolved == "" {
		return
	}
	c.Meta[typeKey] = resolved
	if repeated {
		repeatedKey := "request_repeated"
		if kind == "response" {
			repeatedKey = "response_repeated"
		}
		c.Meta[repeatedKey] = true
	}
	c.Meta["schema_source"] = "extracted"
	delete(c.Meta, exprKey)
}

// lastSuccessResponseCall picks the response call that most likely
// represents the success path. Heuristic: the last call whose status
// is 2xx or unknown; falls back to the last call.
func lastSuccessResponseCall(calls []ResponseCall) ResponseCall {
	for i := len(calls) - 1; i >= 0; i-- {
		c := calls[i]
		if !c.StatusKnown {
			return c
		}
		if c.StatusCode >= 200 && c.StatusCode < 300 {
			return c
		}
	}
	return calls[len(calls)-1]
}

// applyResponseCallToContract turns one ResponseCall's value
// argument into Meta updates: response_type, response_envelope,
// response_repeated, schema_source.
func applyResponseCallToContract(c *Contract, fileNodes []*graph.Node, bf BodyFacts, rc ResponseCall) {
	if rc.ValueArg == nil {
		return
	}
	switch rc.ValueArg.Kind() {
	case "composite_literal":
		applyCompositeResponse(c, fileNodes, bf, rc.ValueArg)
	case "identifier":
		applyIdentResponse(c, fileNodes, bf, rc.ValueExpr)
	case "interpreted_string_literal", "raw_string_literal":
		applyLiteralResponse(c, "string")
	case "int_literal":
		applyLiteralResponse(c, "int")
	case "float_literal":
		applyLiteralResponse(c, "float64")
	case "true", "false":
		applyLiteralResponse(c, "bool")
	case "unary_expression":
		// `&Foo{...}` or `&value` — unwrap.
		inner := unaryOperand(rc.ValueArg)
		if inner == nil {
			return
		}
		switch inner.Kind() {
		case "composite_literal":
			applyCompositeResponse(c, fileNodes, bf, inner)
		case "identifier":
			applyIdentResponse(c, fileNodes, bf, inner.Text())
		}
	}
}

// applyLiteralResponse stamps a primitive response_type when the
// helper's value argument is a string / int / float / bool literal.
func applyLiteralResponse(c *Contract, typeName string) {
	c.Meta["response_type"] = typeName
	c.Meta["schema_source"] = "extracted"
	delete(c.Meta, "response_expr")
}

// applyRequestBindingToContract stamps request_type from one request
// binding. Resolution order:
//  1. CompositeType (Decode(&Foo{})) — direct, no var lookup needed
//  2. VarBinding(VarName).TypeID — chase the variable to its declared
//     type (e.g. `var req CreateRequest; Decode(&req)` → CreateRequest)
//  3. Leave c.Meta alone (regex pass might have set it, or it stays
//     unset)
func applyRequestBindingToContract(c *Contract, fileNodes []*graph.Node, bf BodyFacts, rb RequestBinding) {
	if rb.CompositeType != "" {
		c.Meta["request_type"] = resolveTypeInFile(rb.CompositeType, fileNodes)
		c.Meta["schema_source"] = "extracted"
		delete(c.Meta, "request_expr")
		return
	}
	if rb.VarName == "" {
		return
	}
	b := bf.VarBinding(rb.VarName)
	if b.TypeID == "" {
		return
	}
	c.Meta["request_type"] = resolveTypeInFile(b.TypeID, fileNodes)
	c.Meta["schema_source"] = "extracted"
	delete(c.Meta, "request_expr")
}

// applyCompositeResponse handles `WriteJSON(w, code, Foo{...})` and
// `WriteJSON(w, code, map[string]any{...})`. For map composites we
// walk the entries into an envelope.
func applyCompositeResponse(c *Contract, fileNodes []*graph.Node, bf BodyFacts, valueNode *Node) {
	if valueNode == nil || valueNode.Inner() == nil {
		return
	}
	typ := compositeTypeNode(valueNode.Inner())
	if typ == nil {
		return
	}
	switch typ.Type() {
	case "map_type":
		entries := bf.MapLiteralEntries(valueNode)
		if len(entries) == 0 {
			return
		}
		env := buildEnvelopeFromEntries(bf, fileNodes, entries)
		if len(env) == 0 {
			return
		}
		c.Meta["response_envelope"] = env
		if len(env) == 1 {
			if t, _ := env[0]["type"].(string); t != "" {
				c.Meta["response_type"] = t
			}
			if r, _ := env[0]["repeated"].(bool); r {
				c.Meta["response_repeated"] = true
			}
		}
		c.Meta["schema_source"] = "extracted"
		delete(c.Meta, "response_expr")
	case "slice_type", "array_type":
		// `[]Foo{...}` — record the element type as response, marked
		// repeated. Walk the slice element via bindingFromComposite.
		if goBF, ok := bf.(*goBodyFacts); ok {
			b := goBF.bindingFromComposite(valueNode.Inner())
			if b.TypeID != "" {
				c.Meta["response_type"] = resolveTypeInFile(b.TypeID, fileNodes)
				if b.Repeated {
					c.Meta["response_repeated"] = true
				}
				c.Meta["schema_source"] = "extracted"
				delete(c.Meta, "response_expr")
			}
		}
	default:
		// Named type: `WriteJSON(w, code, Foo{...})` → response = Foo.
		if goBF, ok := bf.(*goBodyFacts); ok {
			b := goBF.bindingFromComposite(valueNode.Inner())
			if b.TypeID != "" {
				c.Meta["response_type"] = resolveTypeInFile(b.TypeID, fileNodes)
				c.Meta["schema_source"] = "extracted"
				delete(c.Meta, "response_expr")
			}
		}
	}
}

// applyIdentResponse handles `WriteJSON(w, code, repos)`. Looks up
// repos's binding; if it resolves to a known type or a method call,
// stamps the response_type or leaves a clean response_expr for the
// post-pass to trace.
func applyIdentResponse(c *Contract, fileNodes []*graph.Node, bf BodyFacts, ident string) {
	ident = strings.TrimSpace(ident)
	if ident == "" {
		return
	}
	b := bf.VarBinding(ident)
	switch b.Kind {
	case BindingComposite, BindingSliceLit, BindingMakeSlice, BindingMakeMap:
		if b.TypeID != "" {
			c.Meta["response_type"] = resolveTypeInFile(b.TypeID, fileNodes)
			if b.Repeated {
				c.Meta["response_repeated"] = true
			}
			c.Meta["schema_source"] = "extracted"
			delete(c.Meta, "response_expr")
			return
		}
	case BindingStringLit, BindingIntLit, BindingFloatLit, BindingBoolLit,
		BindingPathValue, BindingFormValue, BindingHeaderValue, BindingQueryGet:
		c.Meta["response_type"] = b.TypeID
		c.Meta["schema_source"] = "extracted"
		delete(c.Meta, "response_expr")
		return
	case BindingMethodCall, BindingFuncCall:
		// Defer to the post-pass (resolveCallReturnTypes) which will
		// walk the graph from b.CallExpr to the method's return
		// type. We just clean up response_expr to be the bare ident
		// so the post-pass's `isLikelyIdentifier` branch matches.
		if rt, _ := c.Meta["response_type"].(string); rt == "" {
			c.Meta["response_expr"] = ident
		}
	}
}

// buildEnvelopeFromEntries converts AST map-literal entries into the
// `[]map[string]any` shape the dashboard renders. Each entry's value
// expression is matched against the body's bindings to resolve a
// type; when no fact is available the row stays with just an `expr`
// field for the post-pass to chase.
func buildEnvelopeFromEntries(bf BodyFacts, fileNodes []*graph.Node, entries []KeyValue) []map[string]any {
	out := make([]map[string]any, 0, len(entries))
	for _, e := range entries {
		row := map[string]any{"name": e.Key, "expr": e.ValueExpr}
		// Direct composite literals carry their type inline.
		if e.ValueNode != nil && e.ValueNode.Kind() == "composite_literal" {
			if goBF, ok := bf.(*goBodyFacts); ok {
				b := goBF.bindingFromComposite(e.ValueNode.Inner())
				if b.TypeID != "" {
					row["type"] = resolveTypeInFile(b.TypeID, fileNodes)
					if b.Repeated {
						row["repeated"] = true
					}
				}
			}
		} else {
			// Bare identifier: chase its binding.
			ident := strings.TrimLeft(e.ValueExpr, "&*")
			if isPlainIdent(ident) {
				b := bf.VarBinding(ident)
				if b.TypeID != "" {
					row["type"] = resolveTypeInFile(b.TypeID, fileNodes)
				}
				if b.Repeated {
					row["repeated"] = true
				}
			}
		}
		out = append(out, row)
	}
	return out
}

// findContractHandlerNode locates the handler graph node referenced
// by a contract's SymbolID.
func findContractHandlerNode(c *Contract, fileNodes []*graph.Node) *graph.Node {
	for _, n := range fileNodes {
		if n.ID == c.SymbolID {
			return n
		}
	}
	return nil
}

// unaryOperand returns the operand of a unary_expression Node.
func unaryOperand(n *Node) *Node {
	if n == nil || n.Inner() == nil {
		return nil
	}
	op := n.Inner().ChildByFieldName("operand")
	if op == nil {
		return nil
	}
	return NewNode(op, sourceOf(n))
}

// sourceOf retrieves the source bytes a Node was built with. Internal
// helper that reaches into the Node struct.
func sourceOf(n *Node) []byte {
	if n == nil {
		return nil
	}
	return n.src
}

// mergeIntStrings adds new ints into an existing []int meta field
// (or creates it) without duplicates. Preserves source order.
func mergeIntStrings(meta map[string]any, key string, additions []int) {
	if meta == nil {
		return
	}
	seen := map[int]bool{}
	out := []int{}
	if existing, ok := meta[key].([]int); ok {
		for _, v := range existing {
			if !seen[v] {
				seen[v] = true
				out = append(out, v)
			}
		}
	}
	for _, v := range additions {
		if !seen[v] {
			seen[v] = true
			out = append(out, v)
		}
	}
	if len(out) > 0 {
		meta[key] = out
	}
}

// mergeStringList adds strings to a []string Meta field, dedup,
// preserving source order.
func mergeStringList(meta map[string]any, key string, additions []string) {
	if meta == nil {
		return
	}
	seen := map[string]bool{}
	out := []string{}
	if existing, ok := meta[key].([]string); ok {
		for _, v := range existing {
			if !seen[v] {
				seen[v] = true
				out = append(out, v)
			}
		}
	}
	for _, v := range additions {
		if v == "" || seen[v] {
			continue
		}
		seen[v] = true
		out = append(out, v)
	}
	if len(out) > 0 {
		meta[key] = out
	}
}

// isPlainIdent reports whether s is a simple Go identifier (no dots,
// brackets, parens). Used to gate the var-binding lookup so a value
// expression like `len(repos)` isn't sent to BodyFacts.VarBinding.
func isPlainIdent(s string) bool {
	if s == "" {
		return false
	}
	for i, r := range s {
		if r == '_' ||
			(r >= 'a' && r <= 'z') ||
			(r >= 'A' && r <= 'Z') ||
			(i > 0 && r >= '0' && r <= '9') {
			continue
		}
		return false
	}
	return true
}
