package apiserver

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"strings"

	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/conversion"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apiserver/pkg/endpoints/request"
	"k8s.io/apiserver/pkg/registry/rest"
	"k8s.io/kube-openapi/pkg/common"
	"k8s.io/kube-openapi/pkg/validation/spec"

	"github.com/grafana/grafana-app-sdk/app"
	"github.com/grafana/grafana-app-sdk/logging"
	"github.com/grafana/grafana-app-sdk/resource"
)

type CustomRoute struct {
	Path    string
	Handler func(context.Context, app.CustomRouteResponseWriter, *app.CustomRouteRequest) error
}

type ResourceCallOptions struct {
	metav1.TypeMeta

	// Path is the URL path to use for the current proxy request
	Path string
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ResourceCallOptions) DeepCopyInto(out *ResourceCallOptions) {
	*out = *in
	out.TypeMeta = in.TypeMeta
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceCallOptions.
func (in *ResourceCallOptions) DeepCopy() *ResourceCallOptions {
	if in == nil {
		return nil
	}
	out := new(ResourceCallOptions)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *ResourceCallOptions) DeepCopyObject() runtime.Object {
	c := in.DeepCopy()
	return c
}

var (
	_ = rest.Connecter(&SubresourceConnector{})
	_ = rest.StorageMetadata(&SubresourceConnector{})
)

type SubresourceConnectorResponseObject struct {
	Object    any
	MIMETypes []string
}
type SubresourceConnector struct {
	Route CustomRoute
	Kind  resource.Kind
	// Methods is a map of uppercase HTTP methods (ex. GET, PUT) to their response types
	Methods map[string]SubresourceConnectorResponseObject
}

func (*SubresourceConnector) New() runtime.Object {
	return &ResourceCallOptions{}
}

func (*SubresourceConnector) Destroy() {
}

func (r *SubresourceConnector) ConnectMethods() []string {
	methods := make([]string, 0, len(r.Methods))
	for method := range r.Methods {
		methods = append(methods, method)
	}
	return methods
}

func (*SubresourceConnector) NewConnectOptions() (runtime.Object, bool, string) {
	return &ResourceCallOptions{}, false, ""
}

func (r *SubresourceConnector) ProducesObject(verb string) any {
	resp := r.Methods[strings.ToUpper(verb)]
	return resp.Object
}

func (r *SubresourceConnector) ProducesMIMETypes(verb string) []string {
	resp := r.Methods[strings.ToUpper(verb)]
	if len(resp.MIMETypes) == 0 {
		return []string{"application/json"} // TODO: default to text/plain instead?
	}
	return resp.MIMETypes
}

func (r *SubresourceConnector) Connect(ctx context.Context, id string, opts runtime.Object, _ rest.Responder) (http.Handler, error) {
	resourceCallOpts, ok := opts.(*ResourceCallOptions)
	if !ok {
		return nil, fmt.Errorf("invalid options object: %#v", opts)
	}
	logging.FromContext(ctx).Debug("Creating subresource connector", "id", id, "opts", resourceCallOpts)
	// TODO: map instead?
	info, ok := request.RequestInfoFrom(ctx)
	if !ok {
		return nil, errors.New("unable to retrieve request info from context")
	}
	identifier := resource.FullIdentifier{
		Name:      info.Name,
		Namespace: info.Namespace,
		Group:     info.APIGroup,
		Version:   info.APIVersion,
		Kind:      r.Kind.Kind(),
	}
	return &handlerWrapper{
		id:      identifier,
		handler: r.Route.Handler,
		urlToPath: func(u *url.URL) string {
			path := u.Path
			// Index from GV
			idx := strings.Index(path, r.Kind.GroupVersionKind().GroupVersion().String())
			if idx == -1 {
				return path
			}
			path = path[idx+len(r.Kind.GroupVersionKind().GroupVersion().String())+1:]
			// Split up the path into segments
			parts := strings.Split(path, "/")
			// Check for namespaces/cluster
			if r.Kind.Scope() == resource.ClusterScope {
				if len(parts) <= 2 {
					return ""
				}
				return strings.Join(parts[2:], "/")
			}
			if len(parts) <= 4 {
				return ""
			}
			return strings.Join(parts[4:], "/")
		},
	}, nil
}

type handlerWrapper struct {
	id        resource.FullIdentifier
	handler   func(context.Context, app.CustomRouteResponseWriter, *app.CustomRouteRequest) error
	urlToPath func(u *url.URL) string
}

func (h *handlerWrapper) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	err := h.handler(req.Context(), w, &app.CustomRouteRequest{
		ResourceIdentifier: h.id,
		Path:               h.urlToPath(req.URL),
		URL:                req.URL,
		Method:             req.Method,
		Headers:            req.Header,
		Body:               req.Body,
	})
	if err != nil {
		j, e := json.Marshal(err)
		if e != nil {
			logging.FromContext(req.Context()).Error("unable to marshal error JSON", "error", err)
			w.WriteHeader(http.StatusInternalServerError)
		}
		if cast, ok := err.(apierrors.APIStatus); ok {
			w.WriteHeader(int(cast.Status().Code))
		} else {
			w.WriteHeader(http.StatusInternalServerError)
		}
		_, e = w.Write(j)
		if e != nil {
			sanitizedPath := strings.ReplaceAll(req.URL.Path, "\n", "")
			sanitizedPath = strings.ReplaceAll(sanitizedPath, "\r", "")
			logging.FromContext(req.Context()).Error("unable to write response", "error", err, "url", sanitizedPath)
		}
	}
}

// CovertURLValuesToResourceCallOptions reads from the input *url.Values and assigns fields in the out *ResourceCallOptions
func CovertURLValuesToResourceCallOptions(in *url.Values, out *ResourceCallOptions, s conversion.Scope) error {
	if values, ok := map[string][]string(*in)["path"]; ok && len(values) > 0 {
		if err := runtime.Convert_Slice_string_To_string(&values, &out.Path, s); err != nil {
			return err
		}
	} else {
		out.Path = ""
	}
	return nil
}

func GetResourceCallOptionsOpenAPIDefinition() map[string]common.OpenAPIDefinition {
	return map[string]common.OpenAPIDefinition{
		"github.com/grafana/grafana-app-sdk/k8s/apiserver.ResourceCallOptions": {
			Schema: spec.Schema{
				SchemaProps: spec.SchemaProps{
					Description: "ExternalNameFoo defines model for ExternalNameFoo.",
					Type:        []string{"object"},
					Properties: map[string]spec.Schema{
						"kind": {
							SchemaProps: spec.SchemaProps{
								Type: []string{"string"},
							},
						},
						"apiVersion": {
							SchemaProps: spec.SchemaProps{
								Type: []string{"string"},
							},
						},
						"path": {
							SchemaProps: spec.SchemaProps{
								Default: "",
								Type:    []string{"string"},
								Format:  "",
							},
						},
					},
				},
			},
		},
	}
}
