package genaip

import (
	"strconv"
	"strings"

	"github.com/stoewer/go-strcase"
	"go.einride.tech/aip/reflect/aipreflect"
	"go.einride.tech/aip/resourcename"
	"google.golang.org/genproto/googleapis/api/annotations"
	"google.golang.org/protobuf/compiler/protogen"
	"google.golang.org/protobuf/reflect/protoregistry"
)

const (
	fmtPackage          = protogen.GoImportPath("fmt")
	resourcenamePackage = protogen.GoImportPath("go.einride.tech/aip/resourcename")
	stringsPackage      = protogen.GoImportPath("strings")
	encodingPackage     = protogen.GoImportPath("encoding")
)

type resourceNameCodeGenerator struct {
	resource *annotations.ResourceDescriptor
	file     *protogen.File
	files    *protoregistry.Files
}

func (r resourceNameCodeGenerator) GenerateCode(g *protogen.GeneratedFile) error {
	if len(r.resource.GetPattern()) == 0 {
		return nil
	}
	hasMultiPattern := len(r.resource.GetPattern()) > 1
	hasFutureMultiPattern := r.resource.GetHistory() == annotations.ResourceDescriptor_FUTURE_MULTI_PATTERN
	// Generate multi-pattern interface and parse methods if we have multiple patterns now or in the future.
	if hasMultiPattern || hasFutureMultiPattern {
		if err := r.generateMultiPatternInterface(g); err != nil {
			return err
		}
		if err := r.generateMultiPatternParseMethod(g); err != nil {
			return err
		}
	}

	// Generate the single-pattern struct unless we explicitly only want multi-patterns from the start.
	firstPattern := r.resource.GetPattern()[0]
	shouldGenerateSinglePatternStruct := !hasFutureMultiPattern
	firstSinglePatternStructName := r.SinglePatternStructName()
	if shouldGenerateSinglePatternStruct {
		if err := r.generatePatternStruct(
			g, firstPattern, firstSinglePatternStructName,
		); err != nil {
			return err
		}
	}
	// Generate the multi-pattern variant of the single-pattern struct if we need multi-pattern support.
	// If we've already generated single-pattern structs above, ignore top-level resources here as the
	// multi-pattern variant of the single-pattern struct will be identical to the single-pattern struct.
	firstMultiPatternStructName := r.MultiPatternStructName(firstPattern)
	equalMultiSinglePatternStructName := firstMultiPatternStructName == firstSinglePatternStructName
	avoidGeneratingSameSingleStruct := shouldGenerateSinglePatternStruct && equalMultiSinglePatternStructName
	if (hasMultiPattern || hasFutureMultiPattern) && !avoidGeneratingSameSingleStruct {
		if err := r.generatePatternStruct(
			g, firstPattern, firstMultiPatternStructName,
		); err != nil {
			return err
		}
	}
	// Generate multi-pattern structs for all but the first pattern.
	for _, pattern := range r.resource.GetPattern()[1:] {
		if err := r.generatePatternStruct(g, pattern, r.MultiPatternStructName(pattern)); err != nil {
			return err
		}
	}
	return nil
}

func (r resourceNameCodeGenerator) generatePatternStruct(
	g *protogen.GeneratedFile,
	pattern string,
	typeName string,
) error {
	g.P()
	g.P("type ", typeName, " struct {")
	var sc resourcename.Scanner
	sc.Init(pattern)
	for sc.Scan() {
		if sc.Segment().IsVariable() {
			g.P(strcase.UpperCamelCase(string(sc.Segment().Literal())), " string")
		}
	}
	g.P("}")
	var parentConstructorsErr error
	aipreflect.RangeParentResourcesInPackage(
		r.files,
		r.file.Desc.Package(),
		pattern,
		func(parent *annotations.ResourceDescriptor) bool {
			if err := r.generateParentConstructorMethod(g, pattern, typeName, parent); err != nil {
				parentConstructorsErr = err
				return false
			}
			return true
		},
	)
	if parentConstructorsErr != nil {
		return parentConstructorsErr
	}
	if err := r.generateValidateMethod(g, pattern, typeName); err != nil {
		return err
	}
	if err := r.generateContainsWildcardMethod(g, pattern, typeName); err != nil {
		return err
	}
	if err := r.generateStringMethod(g, pattern, typeName); err != nil {
		return err
	}
	if err := r.generateMarshalStringMethod(g, typeName); err != nil {
		return err
	}
	if err := r.generateMarshalTextMethod(g, typeName); err != nil {
		return err
	}
	if err := r.generateUnmarshalStringMethod(g, pattern, typeName); err != nil {
		return err
	}
	if err := r.generateUnmarshalTextMethod(g, typeName); err != nil {
		return err
	}
	if err := r.generateTypeMethod(g, typeName); err != nil {
		return err
	}
	var parentErr error
	aipreflect.RangeParentResourcesInPackage(
		r.files,
		r.file.Desc.Package(),
		pattern,
		func(parent *annotations.ResourceDescriptor) bool {
			if err := r.generateParentMethod(g, pattern, typeName, parent); err != nil {
				parentErr = err
				return false
			}
			return true
		},
	)
	return parentErr
}

func (r *resourceNameCodeGenerator) generateParentConstructorMethod(
	g *protogen.GeneratedFile,
	pattern string,
	typeName string,
	parent *annotations.ResourceDescriptor,
) error {
	var parentPattern string
	for _, parentCandidate := range parent.GetPattern() {
		if resourcename.HasParent(pattern, parentCandidate) {
			parentPattern = parentCandidate
			break
		}
	}
	if parentPattern == "" {
		return nil
	}
	pg := resourceNameCodeGenerator{resource: parent, file: r.file, files: r.files}
	parentStruct := pg.StructName(parentPattern)
	g.P()
	g.P("func (n ", parentStruct, ") ", typeName, "(")
	var sc resourcename.Scanner
	sc.Init(strings.TrimPrefix(pattern, parentPattern))
	for sc.Scan() {
		if sc.Segment().IsVariable() {
			g.P(strcase.LowerCamelCase(string(sc.Segment().Literal())), " string,")
		}
	}
	g.P(") ", typeName, " {")
	g.P("return ", typeName, "{")
	sc.Init(parentPattern)
	for sc.Scan() {
		if sc.Segment().IsVariable() {
			g.P(
				strcase.UpperCamelCase(string(sc.Segment().Literal())),
				": ",
				"n.",
				strcase.UpperCamelCase(string(sc.Segment().Literal())),
				",",
			)
		}
	}
	sc.Init(strings.TrimPrefix(pattern, parentPattern))
	for sc.Scan() {
		if sc.Segment().IsVariable() {
			g.P(
				strcase.UpperCamelCase(string(sc.Segment().Literal())),
				": ",
				strcase.LowerCamelCase(string(sc.Segment().Literal())),
				",",
			)
		}
	}
	g.P("}")
	g.P("}")
	return nil
}

func (r *resourceNameCodeGenerator) generateParentMethod(
	g *protogen.GeneratedFile,
	pattern string,
	typeName string,
	parent *annotations.ResourceDescriptor,
) error {
	var parentPattern string
	for _, parentCandidate := range parent.GetPattern() {
		if resourcename.HasParent(pattern, parentCandidate) {
			parentPattern = parentCandidate
			break
		}
	}
	if parentPattern == "" {
		return nil
	}
	pg := resourceNameCodeGenerator{resource: parent, file: r.file, files: r.files}
	parentStruct := pg.StructName(parentPattern)
	g.P()
	g.P("func (n ", typeName, ") ", parentStruct, "() ", parentStruct, " {")
	g.P("return ", parentStruct, "{")
	var sc resourcename.Scanner
	sc.Init(parentPattern)
	for sc.Scan() {
		if sc.Segment().IsVariable() {
			fieldName := strcase.UpperCamelCase(string(sc.Segment().Literal()))
			g.P(fieldName, ": n.", fieldName, ",")
		}
	}
	g.P("}")
	g.P("}")
	return nil
}

func (r resourceNameCodeGenerator) generateValidateMethod(
	g *protogen.GeneratedFile,
	pattern string,
	typeName string,
) error {
	stringsIndexByte := stringsPackage.Ident("IndexByte")
	fmtErrorf := fmtPackage.Ident("Errorf")
	g.P()
	g.P("func (n ", typeName, ") Validate() error {")
	var sc resourcename.Scanner
	sc.Init(pattern)
	for sc.Scan() {
		if !sc.Segment().IsVariable() {
			continue
		}
		value := string(sc.Segment().Literal())
		g.P("if n.", strcase.UpperCamelCase(value), " == ", strconv.Quote(""), "{")
		g.P("return ", fmtErrorf, `("`, value, `: empty")`)
		g.P("}")
		g.P("if ", stringsIndexByte, "(n.", strcase.UpperCamelCase(value), ", '/') != - 1 {")
		g.P("return ", fmtErrorf, `("`, value, `: contains illegal character '/'")`)
		g.P("}")
	}
	g.P("return nil")
	g.P("}")
	return nil
}

func (r resourceNameCodeGenerator) generateContainsWildcardMethod(
	g *protogen.GeneratedFile,
	pattern string,
	typeName string,
) error {
	g.P()
	g.P("func (n ", typeName, ") ContainsWildcard() bool {")
	var returnStatement strings.Builder
	returnStatement.WriteString("return false")
	var sc resourcename.Scanner
	sc.Init(pattern)
	for sc.Scan() {
		if !sc.Segment().IsVariable() {
			continue
		}
		returnStatement.WriteString("|| n.")
		returnStatement.WriteString(strcase.UpperCamelCase(string(sc.Segment().Literal())))
		returnStatement.WriteString(" == ")
		returnStatement.WriteString(strconv.Quote("-"))
	}
	g.P(returnStatement.String())
	g.P("}")
	return nil
}

func (r *resourceNameCodeGenerator) generateStringMethod(
	g *protogen.GeneratedFile,
	pattern string,
	typeName string,
) error {
	resourcenameSprint := resourcenamePackage.Ident("Sprint")
	g.P()
	g.P("func (n ", typeName, ") String() string {")
	g.P("return ", resourcenameSprint, "(")
	g.P(strconv.Quote(pattern), ",")
	var sc resourcename.Scanner
	sc.Init(pattern)
	for sc.Scan() {
		if sc.Segment().IsVariable() {
			g.P("n.", strcase.UpperCamelCase(string(sc.Segment().Literal())), ",")
		}
	}
	g.P(")")
	g.P("}")
	return nil
}

func (r resourceNameCodeGenerator) generateMarshalStringMethod(
	g *protogen.GeneratedFile,
	typeName string,
) error {
	g.P()
	g.P("func (n ", typeName, ") MarshalString() (string, error) {")
	g.P("if err := n.Validate(); err != nil {")
	g.P("return ", strconv.Quote(""), ", err")
	g.P("}")
	g.P("return n.String(), nil")
	g.P("}")
	return nil
}

func (r resourceNameCodeGenerator) generateUnmarshalStringMethod(
	g *protogen.GeneratedFile,
	pattern string,
	typeName string,
) error {
	resourcenameSscan := resourcenamePackage.Ident("Sscan")
	g.P()
	g.P("func (n *", typeName, ") UnmarshalString(name string) error {")
	g.P("err := ", resourcenameSscan, "(")
	g.P("name,")
	g.P(strconv.Quote(pattern), ",")
	var sc resourcename.Scanner
	sc.Init(pattern)
	for sc.Scan() {
		if sc.Segment().IsVariable() {
			g.P("&n.", strcase.UpperCamelCase(string(sc.Segment().Literal())), ",")
		}
	}
	g.P(")")
	g.P("if err != nil {")
	g.P("return err")
	g.P("}")
	g.P("return n.Validate()")
	g.P("}")
	return nil
}

func (r resourceNameCodeGenerator) generateMarshalTextMethod(
	g *protogen.GeneratedFile,
	typeName string,
) error {
	g.P()
	g.P("// MarshalText implements the encoding.TextMarshaler interface.")
	g.P("func (n ", typeName, ") MarshalText() ([]byte, error) {")
	g.P("if err := n.Validate(); err != nil {")
	g.P("return nil, err")
	g.P("}")
	g.P("return []byte(n.String()), nil")
	g.P("}")
	return nil
}

func (r resourceNameCodeGenerator) generateUnmarshalTextMethod(
	g *protogen.GeneratedFile,
	typeName string,
) error {
	g.P()
	g.P("// UnmarshalText implements the encoding.TextUnmarshaler interface.")
	g.P("func (n *", typeName, ") UnmarshalText(text []byte) error {")
	g.P("return n.UnmarshalString(string(text))")
	g.P("}")
	return nil
}

func (r resourceNameCodeGenerator) generateTypeMethod(
	g *protogen.GeneratedFile,
	typeName string,
) error {
	g.P()
	g.P("func (n ", typeName, ") Type() string {")
	g.P("return ", strconv.Quote(r.resource.GetType()))
	g.P("}")
	return nil
}

func (r resourceNameCodeGenerator) generateMultiPatternInterface(g *protogen.GeneratedFile) error {
	fmtStringer := fmtPackage.Ident("Stringer")
	textMarshaler := encodingPackage.Ident("TextMarshaler")
	g.P()
	g.P("type ", r.MultiPatternInterfaceName(), " interface {")
	g.P(fmtStringer)
	g.P(textMarshaler)
	g.P("MarshalString() (string, error)")
	g.P("ContainsWildcard() bool")
	g.P("}")
	return nil
}

func (r *resourceNameCodeGenerator) generateMultiPatternParseMethod(g *protogen.GeneratedFile) error {
	resourcenameMatch := resourcenamePackage.Ident("Match")
	fmtErrorf := fmtPackage.Ident("Errorf")
	g.P()
	g.P("func Parse", r.MultiPatternInterfaceName(), "(name string) (", r.MultiPatternInterfaceName(), ", error) {")
	g.P("switch {")
	for _, pattern := range r.resource.GetPattern() {
		g.P("case ", resourcenameMatch, "(", strconv.Quote(pattern), ", name):")
		g.P("var result ", r.MultiPatternStructName(pattern))
		g.P("return &result, result.UnmarshalString(name)")
	}
	g.P("default:")
	g.P("return nil, ", fmtErrorf, `("no matching pattern")`)
	g.P("}")
	g.P("}")
	return nil
}

func (r *resourceNameCodeGenerator) SinglePatternStructName() string {
	return aipreflect.ResourceType(r.resource.GetType()).Type() + "ResourceName"
}

func (r *resourceNameCodeGenerator) StructName(pattern string) string {
	if r.resource.GetHistory() == annotations.ResourceDescriptor_FUTURE_MULTI_PATTERN || len(r.resource.GetPattern()) > 1 {
		return r.MultiPatternStructName(pattern)
	}
	if r.resource.GetPattern()[0] == pattern {
		return r.SinglePatternStructName()
	}
	return r.MultiPatternStructName(pattern)
}

func (r *resourceNameCodeGenerator) MultiPatternStructName(pattern string) string {
	var result strings.Builder
	var sc resourcename.Scanner
	sc.Init(pattern)
	for sc.Scan() {
		if !sc.Segment().IsVariable() && string(sc.Segment().Literal()) != r.resource.GetPlural() {
			_, _ = result.WriteString(strcase.UpperCamelCase(string(sc.Segment().Literal())))
		}
	}
	_, _ = result.WriteString(r.SinglePatternStructName())
	return result.String()
}

func (r *resourceNameCodeGenerator) MultiPatternInterfaceName() string {
	return aipreflect.ResourceType(r.resource.GetType()).Type() + "MultiPatternResourceName"
}
