// Copyright 2020 Google LLC. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, softwis
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

package wellknown

import (
	v3 "github.com/google/gnostic/openapiv3"
	"google.golang.org/protobuf/reflect/protoreflect"
)

func NewStringSchema() *v3.SchemaOrReference {
	return &v3.SchemaOrReference{
		Oneof: &v3.SchemaOrReference_Schema{
			Schema: &v3.Schema{Type: "string"}}}
}

func NewBooleanSchema() *v3.SchemaOrReference {
	return &v3.SchemaOrReference{
		Oneof: &v3.SchemaOrReference_Schema{
			Schema: &v3.Schema{Type: "boolean"}}}
}

func NewBytesSchema() *v3.SchemaOrReference {
	return &v3.SchemaOrReference{
		Oneof: &v3.SchemaOrReference_Schema{
			Schema: &v3.Schema{Type: "string", Format: "bytes"}}}
}

func NewIntegerSchema(format string) *v3.SchemaOrReference {
	return &v3.SchemaOrReference{
		Oneof: &v3.SchemaOrReference_Schema{
			Schema: &v3.Schema{Type: "integer", Format: format}}}
}

func NewNumberSchema(format string) *v3.SchemaOrReference {
	return &v3.SchemaOrReference{
		Oneof: &v3.SchemaOrReference_Schema{
			Schema: &v3.Schema{Type: "number", Format: format}}}
}

func NewEnumSchema(enum_type *string, field protoreflect.FieldDescriptor) *v3.SchemaOrReference {
	schema := &v3.Schema{Format: "enum"}
	if enum_type != nil && *enum_type == "string" {
		schema.Type = "string"
		schema.Enum = make([]*v3.Any, 0, field.Enum().Values().Len())
		for i := 0; i < field.Enum().Values().Len(); i++ {
			schema.Enum = append(schema.Enum, &v3.Any{
				Yaml: string(field.Enum().Values().Get(i).Name()),
			})
		}
	} else {
		schema.Type = "integer"
	}
	return &v3.SchemaOrReference{
		Oneof: &v3.SchemaOrReference_Schema{
			Schema: schema}}
}

func NewListSchema(item_schema *v3.SchemaOrReference) *v3.SchemaOrReference {
	return &v3.SchemaOrReference{
		Oneof: &v3.SchemaOrReference_Schema{
			Schema: &v3.Schema{
				Type:  "array",
				Items: &v3.ItemsItem{SchemaOrReference: []*v3.SchemaOrReference{item_schema}},
			},
		},
	}
}

// google.api.HttpBody will contain POST body data
// This is based on how Envoy handles google.api.HttpBody
func NewGoogleApiHttpBodySchema() *v3.SchemaOrReference {
	return &v3.SchemaOrReference{
		Oneof: &v3.SchemaOrReference_Schema{
			Schema: &v3.Schema{Type: "string"}}}
}

// google.protobuf.Timestamp is serialized as a string
func NewGoogleProtobufTimestampSchema() *v3.SchemaOrReference {
	return &v3.SchemaOrReference{
		Oneof: &v3.SchemaOrReference_Schema{
			Schema: &v3.Schema{Type: "string", Format: "date-time"}}}
}

// google.protobuf.Duration is serialized as a string
func NewGoogleProtobufDurationSchema() *v3.SchemaOrReference {
	return &v3.SchemaOrReference{
		Oneof: &v3.SchemaOrReference_Schema{
			// From: https://github.com/protocolbuffers/protobuf/blob/ece5ef6b9b6fa66ef4638335612284379ee4548f/src/google/protobuf/duration.proto
			// In JSON format, the Duration type is encoded as a string rather than an
			// object, where the string ends in the suffix "s" (indicating seconds) and
			// is preceded by the number of seconds, with nanoseconds expressed as
			// fractional seconds. For example, 3 seconds with 0 nanoseconds should be
			// encoded in JSON format as "3s", while 3 seconds and 1 nanosecond should
			// be expressed in JSON format as "3.000000001s", and 3 seconds and 1
			// microsecond should be expressed in JSON format as "3.000001s".
			//
			// The fields of message google.protobuf.Duration are further described as:
			// "int64 seconds"
			// Signed seconds of the span of time. Must be from -315,576,000,000
			// to +315,576,000,000 inclusive. Note: these bounds are computed from:
			// 60 sec/min * 60 min/hr * 24 hr/day * 365.25 days/year * 10000 years
			// `int32 nanos`
			// Signed fractions of a second at nanosecond resolution of the span
			// of time. Durations less than one second are represented with a 0
			// `seconds` field and a positive or negative `nanos` field. For durations
			// of one second or more, a non-zero value for the `nanos` field must be
			// of the same sign as the `seconds` field. Must be from -999,999,999
			// to +999,999,999 inclusive.
			//
			// This leads to the regex below limiting range from -315.576,000,000s to 315,576,000,000s
			// allowing -0.999,999,999s to 0.999,999,999s in the floating precision range.
			// That full range cannot be expressed precisly in float64 as demonstrated in
			// the example at https://go.dev/play/p/XNtuhwdyu8Y for your reference.
			// So the well known type google.protobuf.Duration needs a string.
			//
			// Please note that JSON schemas duration format is NOT the same, as that uses
			// a different syntax starting with "P", supports daylight saving times and other
			// different features, so it is NOT compatible.
			Schema: &v3.Schema{
				Type:        "string",
				Pattern:     `^-?(?:0|[1-9][0-9]{0,11})(?:\.[0-9]{1,9})?s$`,
				Description: "Represents a a duration between -315,576,000,000s and 315,576,000,000s (around 10000 years). Precision is in nanoseconds. 1 nanosecond is represented as 0.000000001s",
			},
		},
	}
}

// google.type.Date is serialized as a string
func NewGoogleTypeDateSchema() *v3.SchemaOrReference {
	return &v3.SchemaOrReference{
		Oneof: &v3.SchemaOrReference_Schema{
			Schema: &v3.Schema{Type: "string", Format: "date"}}}
}

// google.type.DateTime is serialized as a string
func NewGoogleTypeDateTimeSchema() *v3.SchemaOrReference {
	return &v3.SchemaOrReference{
		Oneof: &v3.SchemaOrReference_Schema{
			Schema: &v3.Schema{Type: "string", Format: "date-time"}}}
}

// google.protobuf.FieldMask masks is serialized as a string
func NewGoogleProtobufFieldMaskSchema() *v3.SchemaOrReference {
	return &v3.SchemaOrReference{
		Oneof: &v3.SchemaOrReference_Schema{
			Schema: &v3.Schema{Type: "string", Format: "field-mask"}}}
}

// google.protobuf.Struct is equivalent to a JSON object
func NewGoogleProtobufStructSchema() *v3.SchemaOrReference {
	return &v3.SchemaOrReference{
		Oneof: &v3.SchemaOrReference_Schema{
			Schema: &v3.Schema{Type: "object"}}}
}

// google.protobuf.Value is handled specially
// See here for the details on the JSON mapping:
//
//	https://developers.google.com/protocol-buffers/docs/proto3#json
//
// and here:
//
//	https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#google.protobuf.Value
func NewGoogleProtobufValueSchema(name string) *v3.NamedSchemaOrReference {
	return &v3.NamedSchemaOrReference{
		Name: name,
		Value: &v3.SchemaOrReference{
			Oneof: &v3.SchemaOrReference_Schema{
				Schema: &v3.Schema{
					Description: "Represents a dynamically typed value which can be either null, a number, a string, a boolean, a recursive struct value, or a list of values.",
				},
			},
		},
	}
}

// google.protobuf.Any is handled specially
// See here for the details on the JSON mapping:
//
//	https://developers.google.com/protocol-buffers/docs/proto3#json
func NewGoogleProtobufAnySchema(name string) *v3.NamedSchemaOrReference {
	return &v3.NamedSchemaOrReference{
		Name: name,
		Value: &v3.SchemaOrReference{
			Oneof: &v3.SchemaOrReference_Schema{
				Schema: &v3.Schema{
					Type:        "object",
					Description: "Contains an arbitrary serialized message along with a @type that describes the type of the serialized message.",
					Properties: &v3.Properties{
						AdditionalProperties: []*v3.NamedSchemaOrReference{
							{
								Name: "@type",
								Value: &v3.SchemaOrReference{
									Oneof: &v3.SchemaOrReference_Schema{
										Schema: &v3.Schema{
											Type:        "string",
											Description: "The type of the serialized message.",
										},
									},
								},
							},
						},
					},
					AdditionalProperties: &v3.AdditionalPropertiesItem{
						Oneof: &v3.AdditionalPropertiesItem_Boolean{
							Boolean: true,
						},
					},
				},
			},
		},
	}
}

// google.rpc.Status is handled specially
func NewGoogleRpcStatusSchema(name string, any_name string) *v3.NamedSchemaOrReference {
	return &v3.NamedSchemaOrReference{
		Name: name,
		Value: &v3.SchemaOrReference{
			Oneof: &v3.SchemaOrReference_Schema{
				Schema: &v3.Schema{
					Type:        "object",
					Description: "The `Status` type defines a logical error model that is suitable for different programming environments, including REST APIs and RPC APIs. It is used by [gRPC](https://github.com/grpc). Each `Status` message contains three pieces of data: error code, error message, and error details. You can find out more about this error model and how to work with it in the [API Design Guide](https://cloud.google.com/apis/design/errors).",
					Properties: &v3.Properties{
						AdditionalProperties: []*v3.NamedSchemaOrReference{
							{
								Name: "code",
								Value: &v3.SchemaOrReference{
									Oneof: &v3.SchemaOrReference_Schema{
										Schema: &v3.Schema{
											Type:        "integer",
											Format:      "int32",
											Description: "The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code].",
										},
									},
								},
							},
							{
								Name: "message",
								Value: &v3.SchemaOrReference{
									Oneof: &v3.SchemaOrReference_Schema{
										Schema: &v3.Schema{
											Type:        "string",
											Description: "A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client.",
										},
									},
								},
							},
							{
								Name: "details",
								Value: &v3.SchemaOrReference{
									Oneof: &v3.SchemaOrReference_Schema{
										Schema: &v3.Schema{
											Type: "array",
											Items: &v3.ItemsItem{
												SchemaOrReference: []*v3.SchemaOrReference{
													{
														Oneof: &v3.SchemaOrReference_Reference{
															Reference: &v3.Reference{
																XRef: "#/components/schemas/" + any_name,
															},
														},
													},
												},
											},
											Description: "A list of messages that carry the error details.  There is a common set of message types for APIs to use.",
										},
									},
								},
							},
						},
					},
				},
			},
		},
	}
}

func NewGoogleProtobufMapFieldEntrySchema(value_field_schema *v3.SchemaOrReference) *v3.SchemaOrReference {
	return &v3.SchemaOrReference{
		Oneof: &v3.SchemaOrReference_Schema{
			Schema: &v3.Schema{Type: "object",
				AdditionalProperties: &v3.AdditionalPropertiesItem{
					Oneof: &v3.AdditionalPropertiesItem_SchemaOrReference{
						SchemaOrReference: value_field_schema,
					},
				},
			},
		},
	}
}
