package httproute

import (
	"bytes"
	"regexp"
	"strings"

	"github.com/pkg/errors"
)

// The following comment is copied from google/api/http.proto. It describes
// the bidirectional mapping between protobuf RPCs and HTTP
// requests/responses. The original file is available at the following URL:
//
// https://github.com/googleapis/googleapis/blob/master/google/api/http.proto

// # Rules for HTTP mapping
//
// The rules for mapping HTTP path, query parameters, and body fields
// to the request message are as follows:
//
// 1. The `body` field specifies either `*` or a field path, or is
//    omitted. If omitted, it assumes there is no HTTP body.
// 2. Leaf fields (recursive expansion of nested messages in the
//    request) can be classified into three types:
//     (a) Matched in the URL template.
//     (b) Covered by body (if body is `*`, everything except (a) fields;
//         else everything under the body field)
//     (c) All other fields.
// 3. URL query parameters found in the HTTP request are mapped to (c) fields.
// 4. Any body sent with an HTTP request can contain only (b) fields.
//
// The syntax of the path template is as follows:
//
//     Template = "/" Segments [ Verb ] ;
//     Segments = Segment { "/" Segment } ;
//     Segment  = "*" | "**" | LITERAL | Variable ;
//     Variable = "{" FieldPath [ "=" Segments ] "}" ;
//     FieldPath = IDENT { "." IDENT } ;
//     Verb     = ":" LITERAL ;
//
// The syntax `*` matches a single path segment. It follows the semantics of
// [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String
// Expansion.
//
// The syntax `**` matches zero or more path segments. It follows the semantics
// of [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.3 Reserved
// Expansion. NOTE: it must be the last segment in the path except the Verb.
//
// The syntax `LITERAL` matches literal text in the URL path.
//
// The syntax `Variable` matches the entire path as specified by its template;
// this nested template must not contain further variables. If a variable
// matches a single path segment, its template may be omitted, e.g. `{var}`
// is equivalent to `{var=*}`.
//
// NOTE: the field paths in variables and in the `body` must not refer to
// repeated fields or map fields.
//
// Use CustomHttpPattern to specify any HTTP method that is not included in the
// `pattern` field, such as HEAD, or "*" to leave the HTTP method unspecified for
// a given URL path rule. The wild-card rule is useful for services that provide
// content to Web (HTML) clients.

type (
	// Template is a URI template as may be found in a google.api.http
	// protobuf annotation.
	Template struct {
		segments tmplSegments
		verb     tmplVerb
	}
	// tmplSegments is a list of elements that make up the path of a URI
	// template.
	tmplSegments []tmplSegment
	// tmplSegment is a single literal path segment in a URI template, or a
	// single variable segment.
	tmplSegment struct {
		literal  string
		variable *tmplVariable
		// set when a variable segment doesn't specify a pattern to match
		implicit bool
	}
	// tmplVariable is a single variable segment. The pattern specified by its
	// segments field may match multiple slash-delimited path segments.
	tmplVariable struct {
		fieldPath tmplFieldPath
		segments  tmplSegments
	}
	// tmplFieldPath is a list of field identifiers pointing to a (nested)
	// field on a protobuf message.
	tmplFieldPath []string
	// tmplVerb is a literal string attached to the end of a URI after a ':'.
	// It appears to be part of how Google defines their external GCP APIs.
	tmplVerb string
)

func (t *Template) String() string {
	return "/" + t.segments.String() + t.verb.String()
}

func (t tmplSegments) String() string {
	var buf bytes.Buffer
	for i, seg := range t {
		if i > 0 {
			buf.WriteString("/")
		}
		buf.WriteString(seg.String())
	}
	return buf.String()
}

func (t tmplSegment) String() string {
	if t.variable == nil {
		return t.literal
	}
	return t.variable.String()
}

func (t tmplVariable) String() string {
	segs := ""
	if len(t.segments) > 0 && !t.segments[0].implicit {
		segs = "=" + t.segments.String()
	}
	return "{" + t.fieldPath.String() + segs + "}"
}

func (t tmplFieldPath) String() string {
	return strings.Join([]string(t), ".")
}

func (t tmplVerb) String() string {
	if t == "" {
		return ""
	}
	return ":" + string(t)
}

const (
	// The documentation for google.api.http annotations describes most of the
	// input language clearly, but the acceptable form of LITERAL is somewhat
	// underspecified, as "matches literal text in the URL path". We reserve
	// '/' as an obvious path separator, along with ';' and ',', per RFC 3986
	// §3.3. The syntax for google.api.http "verb" segments requires that we
	// reserve ':', at least in the last path segment. We'll reserve it
	// outright. Since path segments of "*" and "**" have special meaning, we
	// also reserve '*' outright. Curly braces are used for the variable
	// syntax, placing some restrictions on their appearance as well. Again in
	// the name of simplicity (and so typographical errors in/near variable
	// declarations are more likely to be quickly detected), we reserve both
	// '{' and '}' outright.
	//
	// APIs that need these reserved characters to appear in their URI paths
	// may use percent-encoding in the template.
	literal = `[^/;,:\*{}]+`

	ident       = `[A-Za-z][A-Za-z0-9_]*` // https://developers.google.com/protocol-buffers/docs/reference/proto3-spec
	fieldPath   = ident + `(\.` + ident + `)*`
	varSegment  = `(` + literal + `|\*\*?)` // No nesting allowed within variables
	varSegments = varSegment + `(/` + varSegment + `)*`
	variable    = `{(?P<field_path>` + fieldPath + `)(=(?P<var_segments>` + varSegments + `))?}`
	segment     = `((?P<literal>` + literal + `)|(?P<stars>\*\*?)|(?P<variable>` + variable + `))`
	segments    = `(?P<segment>` + segment + `)(?P<more_segment>(/` + segment + `)*)`
	template    = `(?P<segments>/` + segments + `)(?P<verb>:` + literal + `)?`
)

var (
	reSegments = regexp.MustCompile(`^` + segments + `$`)
	reSegment  = regexp.MustCompile(`^` + segment + `$`)
	reTemplate = regexp.MustCompile(`^` + template + `$`)
	reVariable = regexp.MustCompile(`^` + variable + `$`)
)

func splitSegment(str string) (first, rest string) {
	re := reSegments
	for i, val := range re.FindStringSubmatch(str) {
		switch re.SubexpNames()[i] {
		case "segment":
			first = val
		case "more_segment":
			rest = strings.TrimPrefix(val, "/")
		}
	}
	return first, rest
}

func readSegment(str string) (literal, variable string) {
	re := reSegment
	for i, val := range re.FindStringSubmatch(str) {
		switch re.SubexpNames()[i] {
		case "literal", "stars":
			if val != "" {
				literal = val
			}
		case "variable":
			variable = val
		}
	}
	return literal, variable
}

// ParseTemplate parses a template and returns, if successful, a Template
// value that may be used to create URI paths that match the template.
func ParseTemplate(tmpl string) (*Template, error) {
	// Although the template language is expressed as BNF productions --
	// implying that the language is context-free -- nested structures are
	// specifically prohibited, meaning that the language is in fact regular.
	// This means it can be recognized with a DFA, or a regular expression.

	var t Template

	// A reminder of the path template language:
	//
	//     Template = "/" Segments [ Verb ] ;
	//     Segments = Segment { "/" Segment } ;
	//     Segment  = "*" | "**" | LITERAL | Variable ;
	//     Variable = "{" FieldPath [ "=" Segments ] "}" ;
	//     FieldPath = IDENT { "." IDENT } ;
	//     Verb     = ":" LITERAL ;

	re := reTemplate
	matches := re.FindStringSubmatch(tmpl)
	if matches == nil {
		return nil, errors.Errorf("invalid template")
	}

	for i, val := range matches {
		switch re.SubexpNames()[i] {
		case "verb":
			t.verb = tmplVerb(strings.TrimPrefix(val, ":"))
		case "segments":
			tmpl = strings.TrimPrefix(val, "/")
		}
	}

	for tmpl != "" {
		first, rest := splitSegment(tmpl)
		tmpl = rest

		var seg tmplSegment
		l, v := readSegment(first)
		seg.literal = l
		if v != "" {
			seg.variable = parseVariable(v)
		}

		t.segments = append(t.segments, seg)
	}

	return &t, nil
}

func parseVariable(v string) *tmplVariable {
	re := reVariable
	matches := re.FindStringSubmatch(v)
	var fieldPath, segments string
	for i, val := range matches {
		switch re.SubexpNames()[i] {
		case "field_path":
			fieldPath = val
		case "var_segments":
			segments = val
		}
	}
	var varSegs tmplSegments
	for segments != "" {
		first, rest := splitSegment(segments)
		segments = rest
		// Since the top-level regular expression has already
		// recognized the input string, we know here that the segments
		// within the variable do not contain nested variables. We can
		// immediately store any segment as a literal.
		varSegs = append(varSegs, tmplSegment{literal: first})
	}
	if len(varSegs) == 0 {
		// If the variable template is unspecified, it defaults to
		// matching a single path segment.
		varSegs = append(varSegs, tmplSegment{literal: "*", implicit: true})
	}

	return &tmplVariable{
		fieldPath: strings.Split(fieldPath, "."),
		segments:  varSegs,
	}
}

func (t *Template) pathSegments() []string {
	segs := make([]string, 0)

	for _, seg := range t.segments {
		if seg.variable == nil {
			segs = append(segs, seg.literal)
			continue
		}
		for _, seg := range seg.variable.segments {
			segs = append(segs, seg.literal)
		}
	}

	return segs
}

// Validate checks whether the Template is valid, returning any error
// encountered.
func (t *Template) Validate() error {
	// Nested variable declarations are forbidden by the regular expression.
	// We'll re-check here to protect against future refactors.
	//
	// If "**" appears as a path segment, it must be the last path segment
	// (aside from the colon-delimited "verb").
	//
	// Zero-length URI path segments (unless they're in a variable declaration
	// where they're assumed to be "*") and field path segments are forbidden.

	// TODO: check zero-length segments

	for i, seg := range t.segments {
		if seg.variable == nil {
			if seg.literal == "**" && i != len(t.segments)-1 {
				return errors.Errorf("expansion token ** appears at path segment %d, rather than only at the end", i)
			}
			continue
		}
		for _, seg := range seg.variable.segments {
			if seg.variable != nil {
				return errors.Errorf("nested variable declaration at path segment %d", i)
			}
		}
	}
	return nil
}
