package spec

import (
	"fmt"
	"strings"

	jsonschema "github.com/santhosh-tekuri/jsonschema/v6"
)

// ParseJSONPointer parses an RFC 6901 JSON Pointer into its decoded reference
// tokens. It is the single pointer parser shared by the Definition validator
// and (in a later stage) the runtime request binder, so both enforce identical
// pointer semantics.
//
// Semantics:
//   - The empty string "" addresses the whole document (the root). It yields
//     zero tokens and is always valid.
//   - A non-empty pointer must begin with "/"; each slash starts a reference
//     token.
//   - "~1" decodes to "/" and "~0" decodes to "~" (in that order). Any other
//     "~x" is an illegal escape and is rejected.
//   - A bare "/" addresses the property whose name is the empty string.
//
// Rejecting illegal escapes here is what stops "/bad~2escape" from being
// silently accepted as an unconstrained target.
func ParseJSONPointer(p string) ([]string, error) {
	if p == "" {
		return nil, nil
	}
	if !strings.HasPrefix(p, "/") {
		return nil, fmt.Errorf("json pointer %q must be empty (root) or start with /", p)
	}
	// strings.Split("/a/b", "/") == ["", "a", "b"]; drop the leading "".
	segments := strings.Split(p, "/")[1:]
	tokens := make([]string, 0, len(segments))
	for _, seg := range segments {
		tok, err := decodePointerToken(seg)
		if err != nil {
			return nil, fmt.Errorf("json pointer %q: %w", p, err)
		}
		tokens = append(tokens, tok)
	}
	return tokens, nil
}

// decodePointerToken applies the RFC 6901 "~0"/"~1" unescape and rejects any
// other tilde escape.
func decodePointerToken(seg string) (string, error) {
	if !strings.Contains(seg, "~") {
		return seg, nil
	}
	var b strings.Builder
	for i := 0; i < len(seg); i++ {
		c := seg[i]
		if c == '~' {
			if i+1 >= len(seg) {
				return "", fmt.Errorf("invalid escape: trailing ~ at %q", seg)
			}
			switch seg[i+1] {
			case '0':
				b.WriteByte('~')
			case '1':
				b.WriteByte('/')
			default:
				return "", fmt.Errorf("invalid escape sequence ~%c at %q", seg[i+1], seg)
			}
			i++
		} else {
			b.WriteByte(c)
		}
	}
	return b.String(), nil
}

// EncodeJSONPointer encodes reference tokens into an RFC 6901 JSON Pointer
// string. Each token has "~" replaced with "~0" and "/" replaced with "~1"
// (encoding "~" first so the round-trip is unambiguous), then tokens are joined
// with "/". The root pointer (zero tokens) yields the empty string "".
//
// This is the inverse of ParseJSONPointer: EncodeJSONPointer and ParseJSONPointer
// round-trip losslessly for all legal token sequences.
func EncodeJSONPointer(tokens []string) string {
	if len(tokens) == 0 {
		return ""
	}
	var b strings.Builder
	for _, tok := range tokens {
		b.WriteByte('/')
		b.WriteString(encodePointerToken(tok))
	}
	return b.String()
}

// encodePointerToken applies the RFC 6901 "~0"/"~1" escape to a single token.
func encodePointerToken(token string) string {
	if !strings.ContainsAny(token, "~/") {
		return token
	}
	var b strings.Builder
	for i := 0; i < len(token); i++ {
		switch token[i] {
		case '~':
			b.WriteString("~0")
		case '/':
			b.WriteString("~1")
		default:
			b.WriteByte(token[i])
		}
	}
	return b.String()
}

// pointerExists reports whether pointer addresses a property declared by the
// Draft 2020-12 schema. The root pointer ("") always exists. Object properties
// are traversed via "properties"; a leaf schema that is not an object still
// counts as present (the pointer resolves to a declared property).
func pointerExists(schema map[string]any, tokens []string) bool {
	if len(tokens) == 0 {
		return true // root document
	}
	current := schema
	for _, tok := range tokens {
		props, ok := current["properties"].(map[string]any)
		if !ok {
			return false
		}
		next, ok := props[tok]
		if !ok {
			return false
		}
		current, ok = next.(map[string]any)
		if !ok {
			return true // declared non-object leaf
		}
	}
	return true
}

// schemaRequires reports whether prop is listed in the schema's top-level
// "required" array.
func schemaRequires(schema map[string]any, prop string) bool {
	req, ok := schema["required"].([]any)
	if !ok {
		return false
	}
	for _, r := range req {
		if s, ok := r.(string); ok && s == prop {
			return true
		}
	}
	return false
}

// schemaAtPointer walks the schema along the pointer tokens and returns the
// leaf object schema. It returns (nil, false) if the pointer does not address
// an object node (e.g. a non-object leaf or a missing property).
func schemaAtPointer(schema map[string]any, tokens []string) (map[string]any, bool) {
	current := schema
	for _, tok := range tokens {
		props, ok := current["properties"].(map[string]any)
		if !ok {
			return nil, false
		}
		next, ok := props[tok]
		if !ok {
			return nil, false
		}
		current, ok = next.(map[string]any)
		if !ok {
			return nil, false // non-object leaf
		}
	}
	return current, true
}

// schemaRejectsNull reports whether the leaf schema definitively REJECTS a null
// value, using the COMPILED Draft 2020-12 schema (not hand-listed keywords). It
// is the §4.4 invariant check for non-body parameters (path/query/header) which
// cannot serialize null.
//
// Returns:
//   - (true, nil): null is rejected — the source is safe for a non-body param.
//   - (false, nil): null is accepted — the parameter cannot serialize null.
//   - (false, err): null-permissibility cannot be statically determined (e.g.
//     the leaf fails to compile standalone because of an unresolvable $ref).
//     The caller MUST fail-closed on this error.
//
// Compiling the schema (rather than hand-listing type/enum) is what catches
// compositions that admit null — anyOf/oneOf with a null branch, const:null,
// and an unconstrained (empty) schema that accepts every value including null.
func schemaRejectsNull(schema map[string]any) (bool, error) {
	compiler := jsonschema.NewCompiler()
	compiler.DefaultDraft(jsonschema.Draft2020)
	if err := compiler.AddResource("leaf.json", schema); err != nil {
		return false, fmt.Errorf("add leaf schema resource: %w", err)
	}
	compiled, err := compiler.Compile("leaf.json")
	if err != nil {
		return false, fmt.Errorf("compile leaf schema: %w", err)
	}
	if err := compiled.Validate(nil); err == nil {
		// null validates against the compiled schema → null is permissible.
		return false, nil
	}
	return true, nil
}
