package jennies

import (
	"bytes"
	"errors"
	"fmt"
	"go/format"
	"net/http"
	"path"
	"path/filepath"
	"regexp"
	"strings"

	"golang.org/x/tools/imports"

	"github.com/grafana/codejen"

	"github.com/grafana/grafana-app-sdk/codegen"
	"github.com/grafana/grafana-app-sdk/codegen/templates"
)

type CustomRouteGoTypesJenny struct {
	// AddKubernetesCodegen toggles whether kubernetes codegen comments are added to the go types generated by this jenny.
	// The codegen comments can then be used with the OpenAPI jenny, or with the kubernetes codegen tooling.
	AddKubernetesCodegen bool

	// GroupByKind determines whether kinds are grouped by GroupVersionKind or just GroupVersion.
	// If GroupByKind is true, generated paths are <kind>/<version>/<file>, instead of the default <version>/<file>.
	// When GroupByKind is false, subresource types (such as spec and status) are prefixed with the kind name,
	// i.e. generating FooSpec instead of Spec for kind.Name() = "Foo" and Depth=1
	GroupByKind bool

	// AnyAsInterface determines whether to use `interface{}` instead of `any` in generated go code.
	// If true, `interface{}` will be used instead of `any`.
	AnyAsInterface bool

	// OpenAPINamer will be called on each generated type's name to create an OpenAPIModelName() function
	// for the type to implement k8s.io/kube-openapi/pkg/util/OpenAPIModelNamer.
	// If nil, types will not implement OpenAPIModelNamer, which will cause problems with
	// apiservers if using the default apiserver.AppInstaller.
	// gvk.Kind may be empty when called if the custom route is a resource route without a kind.
	OpenAPINamer       func(OpenAPINamerInfo) string
	SkipImportsProcess bool
}

func (*CustomRouteGoTypesJenny) JennyName() string {
	return "CustomRouteTypes"
}

//nolint:gocognit
func (c *CustomRouteGoTypesJenny) Generate(appManifest codegen.AppManifest) (codejen.Files, error) {
	files := make(codejen.Files, 0, 1)
	for _, version := range appManifest.Versions() {
		for _, kind := range version.Kinds() {
			var openAPINamer func(string) string
			if c.OpenAPINamer != nil {
				openAPINamer = func(name string) string {
					return c.OpenAPINamer(OpenAPINamerInfo{
						TypeName:   name,
						FullGroup:  appManifest.Properties().FullGroup,
						ShortGroup: appManifest.Properties().Group,
						Version:    version.Name(),
						Kind:       kind.Kind,
					})
				}
			}
			for cpath, methods := range kind.Routes {
				for method, route := range methods {
					if route.Name == "" {
						route.Name = defaultRouteName(method, cpath)
					}
					generated, err := c.generateCustomRouteKinds(getGeneratedPathForKind(c.GroupByKind, appManifest.Properties().Group, kind, version.Name()), ToPackageName(version.Name()), strings.ToLower(kind.MachineName), route, openAPINamer)
					if err != nil {
						return nil, fmt.Errorf("failed to generate custom route types for route %s, kind %s: %w", route.Name, kind.Kind, err)
					}
					files = append(files, generated...)
				}
			}
		}
		var openAPINamer func(string) string
		if c.OpenAPINamer != nil {
			openAPINamer = func(name string) string {
				return c.OpenAPINamer(OpenAPINamerInfo{
					TypeName:   name,
					FullGroup:  appManifest.Properties().FullGroup,
					ShortGroup: appManifest.Properties().Group,
					Version:    version.Name(),
					Kind:       "",
				})
			}
		}
		for cpath, methods := range version.Routes().Namespaced {
			for method, route := range methods {
				if route.Name == "" {
					route.Name = defaultRouteName(method, cpath)
				}
				generated, err := c.generateCustomRouteKinds(filepath.Join(ToPackageName(appManifest.Properties().Group), ToPackageName(version.Name())), ToPackageName(version.Name()), "", route, openAPINamer)
				if err != nil {
					return nil, fmt.Errorf("failed to generate custom route types for route %s, version %s: %w", route.Name, version.Name(), err)
				}
				files = append(files, generated...)
			}
		}
		for cpath, methods := range version.Routes().Cluster {
			for method, route := range methods {
				if route.Name == "" {
					route.Name = defaultRouteName(method, cpath)
				}
				generated, err := c.generateCustomRouteKinds(filepath.Join(ToPackageName(appManifest.Properties().Group), ToPackageName(version.Name())), ToPackageName(version.Name()), "", route, openAPINamer)
				if err != nil {
					return nil, fmt.Errorf("failed to generate custom route types for route %s, version %s: %w", route.Name, version.Name(), err)
				}
				files = append(files, generated...)
			}
		}
	}
	return files, nil
}

func (c *CustomRouteGoTypesJenny) generateCustomRouteKinds(basePath string, packageName string, filenamePrefix string, customRoute codegen.CustomRoute, openAPINamer func(string) string) (codejen.Files, error) {
	files := make(codejen.Files, 0, 1)
	if !customRoute.Response.Exists() {
		return nil, errors.New("custom route response is required")
	}
	if !customRoute.ResponseMetadata.TypeMeta && (customRoute.ResponseMetadata.ListMeta || customRoute.ResponseMetadata.ObjectMeta) {
		return nil, errors.New("custom route response metadata must have TypeMeta if ListMeta or ObjectMeta are present")
	}
	if filenamePrefix != "" {
		filenamePrefix = fmt.Sprintf("%s_", filenamePrefix)
	}
	// Response
	typeName := exportField(customRoute.Name)
	responseFiles, err := c.generateResponseTypes(customRoute, typeName, packageName, filenamePrefix, basePath, openAPINamer)
	if err != nil {
		return nil, err
	}
	files = append(files, responseFiles...)

	// Request
	requestTypeName := fmt.Sprintf("%sRequest", typeName)
	requestParamsTypeName := fmt.Sprintf("%sParams", requestTypeName)

	// Request Params
	if customRoute.Request.Query.Exists() {
		requestQueryType, err := GoTypesFromCUE(customRoute.Request.Query, CUEGoConfig{
			PackageName: packageName,
			Name:        "Params",
			NamePrefix:  requestTypeName,
		}, 0, openAPINamer)
		if err != nil {
			return nil, err
		}
		files = append(files, codejen.File{
			Data:         requestQueryType,
			RelativePath: fmt.Sprintf(path.Join(basePath, "%s%s_request_params_types_gen.go"), filenamePrefix, strings.ToLower(customRoute.Name)),
			From:         []codejen.NamedJenny{c},
		})

		wrapperTypeName := fmt.Sprintf("%sObject", requestParamsTypeName)
		wrapperOpenAPIName := ""
		if openAPINamer != nil {
			wrapperOpenAPIName = openAPINamer(wrapperTypeName)
		}
		requestQueryObjectType := bytes.Buffer{}
		err = templates.WriteRuntimeObjectWrapper(templates.RuntimeObjectWrapperMetadata{
			PackageName:      packageName,
			WrapperTypeName:  wrapperTypeName,
			TypeName:         requestParamsTypeName,
			OpenAPIModelName: wrapperOpenAPIName,
		}, &requestQueryObjectType)
		if err != nil {
			return nil, err
		}
		files = append(files, codejen.File{
			Data:         requestQueryObjectType.Bytes(),
			RelativePath: fmt.Sprintf(path.Join(basePath, "%s%s_request_params_object_gen.go"), filenamePrefix, strings.ToLower(customRoute.Name)),
			From:         []codejen.NamedJenny{c},
		})
	}

	// Request Body
	if customRoute.Request.Body.Exists() {
		requestBodyType, err := GoTypesFromCUE(customRoute.Request.Body, CUEGoConfig{
			PackageName: packageName,
			Name:        "Body",
			NamePrefix:  requestTypeName,
		}, 0, openAPINamer)
		if err != nil {
			return nil, err
		}
		files = append(files, codejen.File{
			Data:         requestBodyType,
			RelativePath: fmt.Sprintf(path.Join(basePath, "%s%s_request_body_types_gen.go"), filenamePrefix, strings.ToLower(customRoute.Name)),
			From:         []codejen.NamedJenny{c},
		})
	}
	return files, nil
}

func (c *CustomRouteGoTypesJenny) generateResponseTypes(customRoute codegen.CustomRoute, typeName, packageName, filenamePrefix, fileBasePath string, openAPINamer func(string) string) (codejen.Files, error) {
	if !customRoute.ResponseMetadata.TypeMeta && (customRoute.ResponseMetadata.ListMeta || customRoute.ResponseMetadata.ObjectMeta) {
		return nil, errors.New("TypeMeta must be true if ObjectMeta or ListMeta is true")
	}
	files := make(codejen.Files, 0, 1)
	bodyName := typeName + "Body"
	goGenTypeName := "Response"
	if customRoute.ResponseMetadata.ListMeta || customRoute.ResponseMetadata.TypeMeta || customRoute.ResponseMetadata.ObjectMeta {
		goGenTypeName = "Body"
	}
	responseTypes, err := GoTypesFromCUE(customRoute.Response, CUEGoConfig{
		PackageName:                    packageName,
		Name:                           goGenTypeName,
		NamePrefix:                     toExportedFieldName(typeName), // TODO: not sure if we want this set
		AddKubernetesOpenAPIGenComment: c.AddKubernetesCodegen,
		AnyAsInterface:                 c.AnyAsInterface,
	}, 0, openAPINamer)
	if err != nil {
		return nil, err
	}
	typeName += "Response"
	body := ""
	if customRoute.ResponseMetadata.TypeMeta {
		body = "body_"
	}
	formattedResponseTypes, err := c.formatGoBytes(responseTypes)
	if err != nil {
		return nil, fmt.Errorf("failed to format custom route types: %w", err)
	}
	files = append(files, codejen.File{
		Data:         formattedResponseTypes,
		RelativePath: fmt.Sprintf(path.Join(fileBasePath, "%s%s_response_%stypes_gen.go"), filenamePrefix, strings.ToLower(customRoute.Name), body),
		From:         []codejen.NamedJenny{c},
	})
	if customRoute.ResponseMetadata.TypeMeta {
		wrapperOpenAPIName := ""
		if openAPINamer != nil {
			wrapperOpenAPIName = openAPINamer(typeName)
		}
		buf := bytes.Buffer{}
		err = templates.WriteRuntimeObjectWrapper(templates.RuntimeObjectWrapperMetadata{
			PackageName:               packageName,
			TypeName:                  toExportedFieldName(bodyName),
			WrapperTypeName:           typeName,
			HasObjectMeta:             customRoute.ResponseMetadata.ObjectMeta,
			HasListMeta:               customRoute.ResponseMetadata.ListMeta,
			OpenAPIModelName:          wrapperOpenAPIName,
			AddDeepCopyForTypeName:    true,
			KubernetesCodegenComments: true,
		}, &buf)
		if err != nil {
			return nil, fmt.Errorf("failed to write runtime.Object for custom route types: %w", err)
		}
		formatted, err := c.formatGoBytes(buf.Bytes())
		if err != nil {
			return nil, fmt.Errorf("failed to format custom route types: %w", err)
		}
		files = append(files, codejen.File{
			Data:         formatted,
			RelativePath: fmt.Sprintf(path.Join(fileBasePath, "%s%s_response_object_types_gen.go"), strings.ToLower(filenamePrefix), strings.ToLower(customRoute.Name)),
			From:         []codejen.NamedJenny{c},
		})
	}
	return files, nil
}

var httpMethodToK8sVerb = map[string]string{
	http.MethodGet:     "get",
	http.MethodPost:    "create",
	http.MethodPut:     "replace",
	http.MethodPatch:   "patch",
	http.MethodDelete:  "delete",
	http.MethodConnect: "connect",
	http.MethodOptions: "connect", // No real equivalent to options and head
	http.MethodHead:    "connect",
}

func defaultRouteName(method string, route string) string {
	for len(route) > 1 && route[0] == '/' {
		route = route[1:]
	}
	nonAlphanumeric := regexp.MustCompile("[^A-Za-z0-9_]")
	segments := strings.Split(route, "/")
	var sb strings.Builder
	for _, seg := range segments {
		seg = nonAlphanumeric.ReplaceAllString(seg, "")
		if len(seg) > 1 {
			_, _ = sb.WriteString(strings.ToUpper(seg[:1]) + seg[1:])
		} else {
			_, _ = sb.WriteString(strings.ToUpper(seg))
		}
	}
	return fmt.Sprintf("%s%s", httpMethodToK8sVerb[strings.ToUpper(method)], sb.String())
}

func toExportedFieldName(name string) string {
	sanitized := regexp.MustCompile("[^A-Za-z0-9_]").ReplaceAllString(name, "")
	if len(sanitized) > 1 {
		return strings.ToUpper(sanitized[:1]) + sanitized[1:]
	}
	return strings.ToUpper(sanitized)
}

func (c *CustomRouteGoTypesJenny) formatGoBytes(b []byte) ([]byte, error) {
	formatted, err := format.Source(b)
	if err != nil {
		return nil, err
	}

	if !c.SkipImportsProcess {
		formatted, err = imports.Process("", formatted, &imports.Options{
			Comments: true,
		})
		if err != nil {
			return nil, err
		}
	}
	return formatted, nil
}
