package avro

import (
	
	
	
	
	

	
	jsoniter 
)

var (
	protocolReserved = []string{"doc", "types", "messages", "protocol", "namespace"}
	messageReserved  = []string{"doc", "response", "request", "errors", "one-way"}
)

type protocolConfig struct {
	doc   string
	props map[string]any
}

// ProtocolOption is a function that sets a protocol option.
type ProtocolOption func(*protocolConfig)

// WithProtoDoc sets the doc on a protocol.
func ( string) ProtocolOption {
	return func( *protocolConfig) {
		.doc = 
	}
}

// WithProtoProps sets the properties on a protocol.
func ( map[string]any) ProtocolOption {
	return func( *protocolConfig) {
		.props = 
	}
}

// Protocol is an Avro protocol.
type Protocol struct {
	name
	properties

	types    []NamedSchema
	messages map[string]*Message

	doc string

	hash string
}

// NewProtocol creates a protocol instance.
func (
	,  string,
	 []NamedSchema,
	 map[string]*Message,
	 ...ProtocolOption,
) (*Protocol, error) {
	var  protocolConfig
	for ,  := range  {
		(&)
	}

	,  := newName(, , nil)
	if  != nil {
		return nil, 
	}

	 := &Protocol{
		name:       ,
		properties: newProperties(.props, protocolReserved),
		types:      ,
		messages:   ,
		doc:        .doc,
	}

	 := md5.Sum([]byte(.String()))
	.hash = hex.EncodeToString([:])

	return , nil
}

// Message returns a message with the given name or nil.
func ( *Protocol) ( string) *Message {
	return .messages[]
}

// Doc returns the protocol doc.
func ( *Protocol) () string {
	return .doc
}

// Hash returns the MD5 hash of the protocol.
func ( *Protocol) () string {
	return .hash
}

// Types returns the types of the protocol.
func ( *Protocol) () []NamedSchema {
	return .types
}

// String returns the canonical form of the protocol.
func ( *Protocol) () string {
	 := ""
	for ,  := range .types {
		 += .String() + ","
	}
	if len() > 0 {
		 = [:len()-1]
	}

	 := ""
	for ,  := range .messages {
		 += `"` +  + `":` + .String() + ","
	}
	if len() > 0 {
		 = [:len()-1]
	}

	return `{"protocol":"` + .Name() +
		`","namespace":"` + .Namespace() +
		`","types":[` +  + `],"messages":{` +  + `}}`
}

// Message is an Avro protocol message.
type Message struct {
	properties

	req    *RecordSchema
	resp   Schema
	errs   *UnionSchema
	oneWay bool

	doc string
}

// NewMessage creates a protocol message instance.
func ( *RecordSchema,  Schema,  *UnionSchema,  bool,  ...ProtocolOption) *Message {
	var  protocolConfig
	for ,  := range  {
		(&)
	}

	return &Message{
		properties: newProperties(.props, messageReserved),
		req:        ,
		resp:       ,
		errs:       ,
		oneWay:     ,
		doc:        .doc,
	}
}

// Request returns the message request schema.
func ( *Message) () *RecordSchema {
	return .req
}

// Response returns the message response schema.
func ( *Message) () Schema {
	return .resp
}

// Errors returns the message errors union schema.
func ( *Message) () *UnionSchema {
	return .errs
}

// OneWay determines of the message is a one way message.
func ( *Message) () bool {
	return .oneWay
}

// Doc returns the message doc.
func ( *Message) () string {
	return .doc
}

// String returns the canonical form of the message.
func ( *Message) () string {
	 := ""
	for ,  := range .req.fields {
		 += .String() + ","
	}
	if len() > 0 {
		 = [:len()-1]
	}

	 := `{"request":[` +  + `]`
	if .resp != nil {
		 += `,"response":` + .resp.String()
	}
	if .errs != nil && len(.errs.Types()) > 1 {
		,  := NewUnionSchema(.errs.Types()[1:])
		 += `,"errors":` + .String()
	}
	 += "}"
	return 
}

// ParseProtocolFile parses an Avro protocol from a file.
func ( string) (*Protocol, error) {
	,  := os.ReadFile()
	if  != nil {
		return nil, 
	}

	return ParseProtocol(string())
}

// MustParseProtocol parses an Avro protocol, panicing if there is an error.
func ( string) *Protocol {
	,  := ParseProtocol()
	if  != nil {
		panic()
	}

	return 
}

// ParseProtocol parses an Avro protocol.
func ( string) (*Protocol, error) {
	 := &SchemaCache{}

	var  map[string]any
	if  := jsoniter.Unmarshal([]byte(), &);  != nil {
		return nil, 
	}

	 := seenCache{}
	return parseProtocol(, , )
}

type protocol struct {
	Protocol  string                    `mapstructure:"protocol"`
	Namespace string                    `mapstructure:"namespace"`
	Doc       string                    `mapstructure:"doc"`
	Types     []any                     `mapstructure:"types"`
	Messages  map[string]map[string]any `mapstructure:"messages"`
	Props     map[string]any            `mapstructure:",remain"`
}

func parseProtocol( map[string]any,  seenCache,  *SchemaCache) (*Protocol, error) {
	var (
		    protocol
		 mapstructure.Metadata
	)
	if  := decodeMap(, &, &);  != nil {
		return nil, fmt.Errorf("avro: error decoding protocol: %w", )
	}

	if  := checkParsedName(.Protocol);  != nil {
		return nil, 
	}

	var (
		 []NamedSchema
		   error
	)
	if len(.Types) > 0 {
		,  = parseProtocolTypes(.Namespace, .Types, , )
		if  != nil {
			return nil, 
		}
	}

	 := map[string]*Message{}
	if len(.Messages) > 0 {
		for ,  := range .Messages {
			,  := parseMessage(.Namespace, , , )
			if  != nil {
				return nil, 
			}

			[] = 
		}
	}

	return NewProtocol(.Protocol, .Namespace, , , WithProtoDoc(.Doc), WithProtoProps(.Props))
}

func parseProtocolTypes( string,  []any,  seenCache,  *SchemaCache) ([]NamedSchema, error) {
	 := make([]NamedSchema, len())
	for ,  := range  {
		,  := parseType(, , , )
		if  != nil {
			return nil, 
		}

		,  := .(NamedSchema)
		if ! {
			return nil, errors.New("avro: protocol types must be named schemas")
		}

		[] = 
	}

	return , nil
}

type message struct {
	Doc      string           `mapstructure:"doc"`
	Request  []map[string]any `mapstructure:"request"`
	Response any              `mapstructure:"response"`
	Errors   []any            `mapstructure:"errors"`
	OneWay   bool             `mapstructure:"one-way"`
	Props    map[string]any   `mapstructure:",remain"`
}

func parseMessage( string,  map[string]any,  seenCache,  *SchemaCache) (*Message, error) {
	var (
		  message
		 mapstructure.Metadata
	)
	if  := decodeMap(, &, &);  != nil {
		return nil, fmt.Errorf("avro: error decoding message: %w", )
	}

	 := make([]*Field, len(.Request))
	for ,  := range .Request {
		,  := parseField(, , , )
		if  != nil {
			return nil, 
		}
		[] = 
	}
	 := &RecordSchema{
		name:       name{},
		properties: properties{},
		fields:     ,
	}

	var  Schema
	if .Response != nil {
		,  := parseType(, .Response, , )
		if  != nil {
			return nil, 
		}

		if .Type() != Null {
			 = 
		}
	}

	 := []Schema{NewPrimitiveSchema(String, nil)}
	if len(.Errors) > 0 {
		for ,  := range .Errors {
			,  := parseType(, , , )
			if  != nil {
				return nil, 
			}

			if ,  := .(*RecordSchema);  && !.IsError() {
				return nil, errors.New("avro: errors record schema must be of type error")
			}

			 = append(, )
		}
	}
	,  := NewUnionSchema()
	if  != nil {
		return nil, 
	}

	 := .OneWay
	if hasKey(.Keys, "one-way") &&  && (len(.Types()) > 1 ||  != nil) {
		return nil, errors.New("avro: one-way messages cannot not have a response or errors")
	}
	if ! && len(.Types()) <= 1 &&  == nil {
		 = true
	}

	return NewMessage(, , , , WithProtoDoc(.Doc), WithProtoProps(.Props)), nil
}