package contracts

import (
	"regexp"
	"strings"
)

// argKind classifies the first argument passed to a wrapper call.
type argKind int

const (
	argUnknown   argKind = iota // non-literal, non-identifier (runtime expression)
	argLiteral                  // string/template literal — the caller's specific path
	argBareParam                // bare identifier (likely the caller's own path parameter)
)

// extractedArg describes the first argument at a wrapper call site
// plus any HTTP method found on a later argument's method: property.
type extractedArg struct {
	Kind   argKind
	Value  string // populated for argLiteral
	Method string // e.g. "POST"; empty means "unknown / default GET"
}

// extractFirstCallArg finds the call expression at the given 1-based
// line in src and returns the first argument's classification plus any
// method: property from a later argument. wrapperName is the short
// name of the function being called (e.g. "request"). lang is the
// caller's language ("typescript", "javascript", "go", "dart").
//
// The strategy is the same across languages because call syntax is
// uniform enough in practice: find wrapperName(, scan forward until
// the matching close-paren while tracking paren depth and string
// escapes, then parse the captured arg list. Works for:
//
//	request('/v1/users', getToken, { method: 'POST' })
//	request(`/v1/users/${id}`, getToken)
//	doFetch(path, token, options)
//	request("/v1/users", getToken, map[string]any{"method": "POST"})
//
// Language only matters for literal-delimiter set (Go doesn't use
// backticks for template literals; TS does; etc.) and for the
// identifier-name shape, which defaults to the same pattern across
// the languages we target.
func extractFirstCallArg(src []byte, line int, wrapperName, lang string) extractedArg {
	if len(src) == 0 || wrapperName == "" || line <= 0 {
		return extractedArg{}
	}

	lineStart, lineEnd := lineBounds(src, line)
	if lineStart < 0 {
		return extractedArg{}
	}

	// Search for the wrapper name on the target line only. An edge's
	// Line is the line where the call expression starts; if the call
	// isn't mentioned on that line, we shouldn't be picking up an
	// unrelated call lower in the file.
	nameRE := regexp.MustCompile(`\b` + regexp.QuoteMeta(wrapperName) + `\b\s*(?:<[^>]*>\s*)?\(`)
	lineSlice := src[lineStart:lineEnd]
	loc := nameRE.FindIndex(lineSlice)
	if loc == nil {
		return extractedArg{}
	}
	// Arg list starts just after the opening '('. Use offsets into the
	// full source so balancedCloseParen can scan past line breaks for
	// multi-line argument lists.
	argStart := lineStart + loc[1]
	end := balancedCloseParen(src, argStart-1)
	if end < 0 {
		return extractedArg{}
	}
	argsText := string(src[argStart:end])

	first, rest := splitFirstArg(argsText)
	return extractedArg{
		Kind:   classifyArg(first, lang),
		Value:  unquoteIfLiteral(first),
		Method: findMethodProperty(rest),
	}
}

// lineBounds returns the 0-based byte offsets of the start and end of
// the given 1-based line. Returns -1 for lineStart if the line is out
// of range.
func lineBounds(src []byte, line int) (int, int) {
	current := 1
	start := 0
	for i, b := range src {
		if current == line {
			// Find end of this line.
			end := len(src)
			for j := i; j < len(src); j++ {
				if src[j] == '\n' {
					end = j
					break
				}
			}
			return start, end
		}
		if b == '\n' {
			current++
			start = i + 1
		}
	}
	if current == line {
		return start, len(src)
	}
	return -1, -1
}

// balancedCloseParen returns the offset of the ")" that matches the
// "(" at openIdx, respecting nested parens, string/template literals
// and comments. Returns -1 if no match is found.
func balancedCloseParen(src []byte, openIdx int) int {
	if openIdx < 0 || openIdx >= len(src) || src[openIdx] != '(' {
		return -1
	}
	depth := 0
	i := openIdx
	for i < len(src) {
		c := src[i]
		switch c {
		case '(':
			depth++
			i++
		case ')':
			depth--
			if depth == 0 {
				return i
			}
			i++
		case '"', '\'', '`':
			// Skip until the matching close quote. Handles simple
			// escapes; template-literal ${...} interpolation is
			// treated as a string body (we don't recurse into it —
			// good enough for our call-site shapes).
			quote := c
			i++
			for i < len(src) && src[i] != quote {
				if src[i] == '\\' && i+1 < len(src) {
					i += 2
					continue
				}
				i++
			}
			if i < len(src) {
				i++ // consume close quote
			}
		case '/':
			// Skip // line comments and /* block comments.
			if i+1 < len(src) && src[i+1] == '/' {
				for i < len(src) && src[i] != '\n' {
					i++
				}
			} else if i+1 < len(src) && src[i+1] == '*' {
				i += 2
				for i+1 < len(src) && (src[i] != '*' || src[i+1] != '/') {
					i++
				}
				i += 2
			} else {
				i++
			}
		default:
			i++
		}
	}
	return -1
}

// splitFirstArg returns the first comma-separated argument and the
// remaining arg list as a single string, respecting nested parens,
// braces, brackets, and string literals.
func splitFirstArg(args string) (string, string) {
	depthParen, depthBrace, depthBracket := 0, 0, 0
	i := 0
	for i < len(args) {
		c := args[i]
		switch c {
		case '(':
			depthParen++
		case ')':
			depthParen--
		case '{':
			depthBrace++
		case '}':
			depthBrace--
		case '[':
			depthBracket++
		case ']':
			depthBracket--
		case '"', '\'', '`':
			quote := c
			i++
			for i < len(args) && args[i] != quote {
				if args[i] == '\\' && i+1 < len(args) {
					i += 2
					continue
				}
				i++
			}
		case ',':
			if depthParen == 0 && depthBrace == 0 && depthBracket == 0 {
				return strings.TrimSpace(args[:i]), strings.TrimSpace(args[i+1:])
			}
		}
		i++
	}
	return strings.TrimSpace(args), ""
}

// classifyArg decides whether an argument text is a literal string/
// template-literal (→ argLiteral), a bare identifier that looks like
// a function parameter (→ argBareParam), or something else.
func classifyArg(s, lang string) argKind {
	if s == "" {
		return argUnknown
	}
	// String literal (with or without quotes stripped earlier). The
	// caller strips nothing; we check here.
	if isQuoted(s) {
		// A template literal with interpolation is still a literal
		// for our purposes — NormalizeHTTPPath collapses ${id} to
		// {id}. But we exclude ones that are ENTIRELY a placeholder
		// ("`${url}`") because that's a wrapper forwarding the URL.
		inner := unquote(s)
		if isPureInterpolation(inner) {
			return argBareParam
		}
		return argLiteral
	}
	if isBareIdentifier(s) {
		return argBareParam
	}
	return argUnknown
}

// isQuoted reports whether s begins and ends with ", ', or `.
func isQuoted(s string) bool {
	if len(s) < 2 {
		return false
	}
	first, last := s[0], s[len(s)-1]
	return (first == '"' || first == '\'' || first == '`') && first == last
}

// unquote strips matching surrounding quotes.
func unquote(s string) string {
	if !isQuoted(s) {
		return s
	}
	return s[1 : len(s)-1]
}

// unquoteIfLiteral strips quotes when the arg looks like a literal,
// otherwise returns the arg unchanged. Non-literal returns are unused
// by the caller (the argKind guard skips them) but keeping the value
// debuggable is cheap.
func unquoteIfLiteral(s string) string {
	if isQuoted(s) {
		return unquote(s)
	}
	return s
}

// isPureInterpolation reports whether a template-literal body consists
// of a single ${ident} with nothing else. Those bodies mean "forward
// the URL parameter verbatim" — the caller is a wrapper, not a leaf.
var pureInterpRE = regexp.MustCompile(`^\$\{\s*[a-zA-Z_][a-zA-Z0-9_.]*\s*\}$`)

func isPureInterpolation(s string) bool {
	return pureInterpRE.MatchString(strings.TrimSpace(s))
}

// bareIdentRE accepts a single identifier possibly qualified with dots
// ("path", "opts.path") — Go / TS / JS / Dart all use the same shape.
var bareIdentRE = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*$`)

func isBareIdentifier(s string) bool {
	return bareIdentRE.MatchString(s)
}

// findMethodProperty scans the remaining args for an object literal
// whose method: property names the HTTP method. Handles:
//
//	{ method: 'POST', ... }
//	{method:"PATCH"}
//	map[string]any{"method": "DELETE"}  // go-ish
//
// Returns the method in uppercase, or "" if none found.
// methodPropRE accepts both the TS/JS shorthand ( method: "POST" ) and
// the Go/JSON style ( "method": "POST" ) where the key is quoted.
var methodPropRE = regexp.MustCompile(`(?i)['"` + "`" + `]?\bmethod\b['"` + "`" + `]?\s*:\s*['"` + "`" + `]([A-Za-z]+)['"` + "`" + `]`)

func findMethodProperty(rest string) string {
	m := methodPropRE.FindStringSubmatch(rest)
	if len(m) < 2 {
		return ""
	}
	return strings.ToUpper(m[1])
}
