package oauthroutes

import (
	"fmt"
	"strings"
)

// Family validates and canonicalizes OAuth route patterns under an app-blind
// route-family prefix (e.g. a versioned API root path). The prefix is supplied
// by the active product/dialect via the host's route-policy bus, not hardcoded
// in L0. An empty prefix means "no route family configured": every pattern is a
// non-family pass-through (validation returns nil, canonicalization is verbatim).
// The host fails closed when a product route family is active but supplies no
// prefix (see the serverruntime install).
type Family struct{ prefix string }

// NewFamily returns a route-pattern Family keyed on prefix.
func NewFamily(prefix string) Family { return Family{prefix: prefix} }

// CanonicalPattern normalizes concrete versioned route patterns under the family
// prefix to the shared ":version" placeholder used by runtime route coverage.
// Malformed version segments are left unchanged so they cannot alias a valid
// versioned route at runtime. An empty prefix passes every pattern through.
func (f Family) CanonicalPattern(pattern string) string {
	if f.prefix == "" || !strings.HasPrefix(pattern, f.prefix) {
		return pattern
	}
	segment, tail, ok := f.segment(pattern)
	if !ok {
		return pattern
	}
	if segment != ":version" && !IsAPIVersionSegment(segment) {
		return pattern
	}
	return f.prefix + ":version" + tail
}

// ValidatePattern rejects malformed <prefix><segment> route patterns that would
// otherwise be ambiguous with concrete versioned routes. The family is detected
// case-insensitively, but only the exact canonical lowercase prefix is accepted:
// a case-variant prefix matches no real lowercase Gin route, so it must fail
// closed rather than slip past as a non-family (and therefore unvalidated)
// pattern that issuance and CORS would still treat as an active scope. An empty
// prefix means no family is configured: nothing to validate.
func (f Family) ValidatePattern(pattern string) error {
	if f.prefix == "" {
		return nil
	}
	if !strings.HasPrefix(strings.ToLower(pattern), strings.ToLower(f.prefix)) {
		return nil
	}
	if !strings.HasPrefix(pattern, f.prefix) {
		return fmt.Errorf("route pattern %q must use the canonical lowercase %q prefix", pattern, f.prefix)
	}
	segment, _, ok := f.segment(pattern)
	if !ok {
		return nil
	}
	if segment == ":version" || IsAPIVersionSegment(segment) {
		return nil
	}
	return fmt.Errorf("route pattern %q has invalid version segment %q under route family %q", pattern, segment, f.prefix)
}

// ValidatePatterns returns the first error among the route patterns in raw, or
// nil if every pattern is valid. Every reader (issuance, route coverage, CORS)
// uses this so they fail closed on exactly the same malformed metadata, without
// per-reader tokenizing or case-folding that could disagree.
func (f Family) ValidatePatterns(raw string) error {
	for _, pattern := range SplitPatterns(raw) {
		if err := f.ValidatePattern(pattern); err != nil {
			return err
		}
	}
	return nil
}

func (f Family) segment(pattern string) (segment string, tail string, ok bool) {
	rest := strings.TrimPrefix(pattern, f.prefix)
	if rest == "" {
		return "", "", false
	}
	segment = rest
	if slash := strings.IndexByte(rest, '/'); slash >= 0 {
		segment = rest[:slash]
		tail = rest[slash:]
	}
	return segment, tail, true
}

// SplitPatterns splits a stored route-pattern field into individual patterns,
// using the same delimiters the scope readers tokenize on (whitespace and
// commas). Patterns are returned verbatim — case preserved — so every reader
// validates exactly the same representation.
func SplitPatterns(raw string) []string {
	return strings.FieldsFunc(raw, func(r rune) bool {
		switch r {
		case ',', '\n', '\r', '\t', ' ':
			return true
		default:
			return false
		}
	})
}

// IsAPIVersionSegment reports whether segment is an API version path segment,
// e.g. "v63.0": a 'v' followed by digits, a single '.', and more digits.
func IsAPIVersionSegment(segment string) bool {
	if len(segment) < 4 || segment[0] != 'v' {
		return false
	}
	dot := strings.IndexByte(segment, '.')
	if dot < 2 || dot == len(segment)-1 {
		return false
	}
	for i := 1; i < dot; i++ {
		if segment[i] < '0' || segment[i] > '9' {
			return false
		}
	}
	for i := dot + 1; i < len(segment); i++ {
		if segment[i] < '0' || segment[i] > '9' {
			return false
		}
	}
	return true
}
