package main

import (
	"fmt"
	"os"
	"reflect"
	"strconv"
	"strings"
	"text/template"

	"github.com/alecthomas/errors"

	"github.com/block/ftl/common/strcase"
)

var _ fmt.Stringer

var go2protoTmpl = template.Must(template.New("go2proto.to.go.tmpl").
	Funcs(template.FuncMap{
		"typeof": func(t any) Kind { return Kind(reflect.Indirect(reflect.ValueOf(t)).Type().Name()) },
		// Return true if the type is a builtin proto type.
		"isBuiltin": func(t Field) bool {
			switch t.OriginType {
			case "int", "uint", "int32", "int64", "uint32", "uint64", "float32", "float64", "bool", "string":
				return true
			}
			return false
		},
		"protoName": protoName,
		"goProtoImport": func(g Go2ProtoContext) (string, error) {
			unquoted, err := strconv.Unquote(g.Options["go_package"])
			if err != nil {
				return "", errors.Wrap(err, "go_package must be a quoted string")
			}
			parts := strings.Split(unquoted, ";")
			return parts[0], nil
		},
		"sumTypeVariantName": func(s string, v string) string {
			return protoName(strings.TrimPrefix(v, s))
		},
		"toLower":      strings.ToLower,
		"toUpper":      strings.ToUpper,
		"toLowerCamel": strcase.ToLowerCamel,
		"toUpperCamel": strcase.ToUpperCamel,
		"toLowerSnake": strcase.ToLowerSnake,
		"toUpperSnake": strcase.ToUpperSnake,
		"trimPrefix":   strings.TrimPrefix,
	}).
	Parse(`// Code generated by go2proto. DO NOT EDIT.

package {{ .GoPackage }}

import "fmt"
import "encoding"
import destpb "{{ . | goProtoImport }}"
import "google.golang.org/protobuf/types/known/timestamppb"
import "google.golang.org/protobuf/types/known/durationpb"
import "github.com/alecthomas/errors"
import "github.com/alecthomas/types/optional"
import "github.com/alecthomas/types/result"
{{ range .GoImports }}
import "{{ . }}"
{{ end }}

var _ fmt.Stringer
var _ = timestamppb.Timestamp{}
var _ = durationpb.Duration{}

// protoSlice converts a slice of values to a slice of protobuf values.
func protoSlice[P any, T interface{ ToProto() P }](values []T) []P {
	out := make([]P, len(values))
	for i, v := range values {
		out[i] = v.ToProto()
	}
	return out
}

func protoMust[T any](v T, err error) T {
	if err != nil {
		panic(err)
	}
	return v
}

func sliceMap[T any, U any](values []T, f func(T) U) []U {
	out := make([]U, len(values))
	for i, v := range values {
		out[i] = f(v)
	}
	return out
}

func sliceMapR[T any, U any](values []T, f func(T) result.Result[U]) result.Result[[]U] {
	if len(values) == 0 {
		return result.Ok[[]U](nil)
	}
	out := make([]U, len(values))
	for i, v := range values {
		r := f(v)
		if r.Err() != nil {
			return result.Err[[]U](r.Err())
		}
		out[i], _ = r.Get()
	}
	return result.Ok[[]U](out)
}

func mapValues[K comparable, V, U any](m map[K]V, f func(V) U) map[K]U {
	out := make(map[K]U, len(m))
	for k, v := range m {
		out[k] = f(v)
	}
	return out
}

func mapValuesR[K comparable, V, U any](m map[K]V, f func(V) result.Result[U]) result.Result[map[K]U] {
	out := make(map[K]U, len(m))
	for k, v := range m {
		r := f(v)
		if r.Err() != nil {
			return result.Err[map[K]U](r.Err())
		}
		val, _ := r.Get()
		out[k] = val
	}
	return result.Ok[map[K]U](out)
}

func orZero[T any](v *T) T {
	if v == nil {
		return *new(T)
	}
	return *v
}

func orZeroR[T any](v result.Result[*T]) result.Result[T] {
		if v.Err() != nil {
		return result.Err[T](v.Err())
	}
	r, _ := v.Get()
	return result.Ok[T](orZero(r))
}

func ptr[T any](o T) *T {
	return &o
}

func ptrR[T any](o result.Result[T]) result.Result[*T] {
	if o.Err() != nil {
		return result.Err[*T](o.Err())
	}
	r, _ := o.Get()
	return result.Ok[*T](ptr(r))
}

func fromPtr[T any](v *T) T {
	if v == nil {
		return *new(T)
	}
	return *v
}

func fromPtrR[T any](v result.Result[*T]) result.Result[T] {
	if v.Err() != nil {
		return result.Err[T](v.Err())
	}
	r, _ := v.Get()
	return result.Ok[T](fromPtr(r))
}

func optionalR[T any](r result.Result[*T]) result.Result[optional.Option[T]] {
	if r.Err() != nil {
		return result.Err[optional.Option[T]](r.Err())
	}
	v, _ := r.Get()
	return result.Ok[optional.Option[T]](optional.Ptr(v))
}

func optionalRPtr[T any](r result.Result[*T]) result.Result[optional.Option[*T]] {
	if r.Err() != nil {
		return result.Err[optional.Option[*T]](r.Err())
	}
	v, _ := r.Get()
	return result.Ok[optional.Option[*T]](optional.Zero(v))
}

func setNil[T, O any](v *T, o *O) *T {
	if o == nil {
		return nil
	}
	return v
}

func setNilR[T, O any](v result.Result[*T], o *O) result.Result[*T] {
	if v.Err() != nil {
		return v
	}
	r, _ := v.Get()
	return result.Ok[*T](setNil(r, o))
}

type binaryUnmarshallable[T any] interface {
	*T
	encoding.BinaryUnmarshaler
}

type textUnmarshallable[T any] interface {
	*T
	encoding.TextUnmarshaler
}

func unmarshallBinary[T any, TPtr binaryUnmarshallable[T]](v []byte, f TPtr) result.Result[*T] {
	var to T
	toptr := (TPtr)(&to)

	err := toptr.UnmarshalBinary(v)
	if err != nil {
		return result.Err[*T](err)
	}
	return result.Ok[*T](&to)
}

func unmarshallText[T any, TPtr textUnmarshallable[T]](v []byte, f TPtr) result.Result[*T] {
	var to T
	toptr := (TPtr)(&to)

	err := toptr.UnmarshalText(v)
	if err != nil {
		return result.Err[*T](err)
	}
	return result.Ok[*T](&to)
}

{{range $decl := .OrderedDecls }}
{{- if eq (typeof $decl) "Message" }}
func (x *{{ .Name }}) ToProto() *destpb.{{ .Name }} {
	if x == nil {
		return nil
	}
	return &destpb.{{ .Name }}{
{{- range $field := .Fields }}
		{{ $field.EscapedName }}: {{ $field.ToProto }},
{{- end}}
	}
}

func {{ .Name }}FromProto(v *destpb.{{ .Name }}) (out *{{ .Name }}, err error) {
	if v == nil {
		return nil, nil
	}

	out = &{{ .Name }}{}

{{- range $field := .Fields }}
	if out.{{ $field.Name }}, err = {{ $field.FromProto }}.Result(); err != nil {
		return nil, errors.Wrap(err, "{{ $field.Name }}")
	}
{{- end}}
{{- if .Validator }}
	if err := out.Validate(); err != nil {
		return nil, err
	}
{{- end}}
	return out, nil
}

{{- else if eq (typeof $decl) "Enum" }}
func (x {{ .Name }}) ToProto() destpb.{{ .Name }} {
	return destpb.{{ .Name }}(x)
}

func {{ .Name }}FromProto(v destpb.{{ .Name }}) ({{ .Name }}, error) {
	// TODO: Check if the value is valid.
	return {{ .Name }}(v), nil
}
{{- else if eq (typeof $decl) "SumType" }}
{{- $sumtype := . }}
// {{ .Name }}ToProto converts a {{ .Name }} sum type to a protobuf message.
func {{ .Name }}ToProto(value {{ .Name }}) *destpb.{{ .Name }} {
	switch value := value.(type) {
	case nil:
		return nil
	{{- range $variant, $id := .Variants }}
	case *{{ $variant }}:
		return &destpb.{{ $sumtype.Name }}{
			Value: &destpb.{{$variant | $sumtype.TypeName }}{value.ToProto()},
		}
	{{- end }}
	default:
		panic(fmt.Sprintf("unknown variant: %T", value))
	}
}

func {{ $sumtype.Name }}FromProto(v *destpb.{{ $sumtype.Name }}) ({{ $sumtype.Name }}, error) {
	if v == nil {
		return nil, nil
	}
	switch v.Value.(type) {
	{{- range $variant, $id := .Variants }}
	case *destpb.{{ $sumtype.TypeName $variant }}:
		return {{ $variant }}FromProto(v.{{ $variant | $sumtype.Getter }}())
	{{- end }}
	default:
		panic(fmt.Sprintf("unknown variant: %T", v.Value))
	}
}
{{- end}}
{{ end}}
		`))

type Go2ProtoContext struct {
	PackageDirectives
	File
	GoImports []string
}

func renderToProto(out *os.File, directives PackageDirectives, file File, goImports []string) error {
	err := go2protoTmpl.Execute(out, Go2ProtoContext{
		PackageDirectives: directives,
		File:              file,
		GoImports:         goImports,
	})
	if err != nil {
		return errors.Wrap(err, "template error")
	}
	return nil
}
