package server

import (
	"net/http"

	"github.com/rancher/steve/pkg/schemaserver/builtin"
	"github.com/rancher/steve/pkg/schemaserver/handlers"
	"github.com/rancher/steve/pkg/schemaserver/parse"
	"github.com/rancher/steve/pkg/schemaserver/subscribe"
	"github.com/rancher/steve/pkg/schemaserver/types"
	"github.com/rancher/steve/pkg/schemaserver/writer"
	"github.com/rancher/wrangler/pkg/schemas/validation"
)

type RequestHandler interface {
	http.Handler

	GetSchemas() *types.APISchemas
	Handle(apiOp *types.APIRequest)
}

type Server struct {
	ResponseWriters map[string]types.ResponseWriter
	Schemas         *types.APISchemas
	Defaults        Defaults
	AccessControl   types.AccessControl
	Parser          parse.Parser
	URLParser       parse.URLParser
}

type Defaults struct {
	ByIDHandler   types.RequestHandler
	ListHandler   types.RequestListHandler
	CreateHandler types.RequestHandler
	DeleteHandler types.RequestHandler
	UpdateHandler types.RequestHandler
	ErrorHandler  types.ErrorHandler
}

func DefaultAPIServer() *Server {
	s := &Server{
		Schemas: types.EmptyAPISchemas().MustAddSchemas(builtin.Schemas),
		ResponseWriters: map[string]types.ResponseWriter{
			"json": &writer.GzipWriter{
				ResponseWriter: &writer.EncodingResponseWriter{
					ContentType: "application/json",
					Encoder:     types.JSONEncoder,
				},
			},
			"html": &writer.GzipWriter{
				ResponseWriter: &writer.HTMLResponseWriter{
					EncodingResponseWriter: writer.EncodingResponseWriter{
						Encoder:     types.JSONEncoder,
						ContentType: "application/json",
					},
				},
			},
			"yaml": &writer.GzipWriter{
				ResponseWriter: &writer.EncodingResponseWriter{
					ContentType: "application/yaml",
					Encoder:     types.YAMLEncoder,
				},
			},
		},
		AccessControl: &SchemaBasedAccess{},
		Defaults: Defaults{
			ByIDHandler:   handlers.ByIDHandler,
			CreateHandler: handlers.CreateHandler,
			DeleteHandler: handlers.DeleteHandler,
			UpdateHandler: handlers.UpdateHandler,
			ListHandler:   handlers.ListHandler,
			ErrorHandler:  handlers.ErrorHandler,
		},
		Parser:    parse.Parse,
		URLParser: parse.MuxURLParser,
	}

	subscribe.Register(s.Schemas)
	return s
}

func (s *Server) setDefaults(ctx *types.APIRequest) {
	if ctx.ResponseWriter == nil {
		ctx.ResponseWriter = s.ResponseWriters[ctx.ResponseFormat]
		if ctx.ResponseWriter == nil {
			ctx.ResponseWriter = s.ResponseWriters["json"]
		}
	}

	ctx.AccessControl = s.AccessControl

	if ctx.Schemas == nil {
		ctx.Schemas = s.Schemas
	}
}

func (s *Server) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
	s.Handle(&types.APIRequest{
		Request:  req,
		Response: rw,
	})
}

func (s *Server) Handle(apiOp *types.APIRequest) {
	s.handle(apiOp, s.Parser)
}

func (s *Server) handle(apiOp *types.APIRequest, parser parse.Parser) {
	if apiOp.Schemas == nil {
		apiOp.Schemas = s.Schemas
	}

	if err := parser(apiOp, parse.MuxURLParser); err != nil {
		// ensure defaults set so writer is assigned
		s.setDefaults(apiOp)
		s.handleError(apiOp, err)
		return
	}

	s.setDefaults(apiOp)

	if code, data, err := s.handleOp(apiOp); err != nil {
		s.handleError(apiOp, err)
	} else if obj, ok := data.(types.APIObject); ok {
		apiOp.WriteResponse(code, obj)
	} else if list, ok := data.(types.APIObjectList); ok {
		apiOp.WriteResponseList(code, list)
	} else if code > http.StatusOK {
		apiOp.Response.WriteHeader(code)
	}
}

func (s *Server) handleOp(apiOp *types.APIRequest) (int, interface{}, error) {
	if err := CheckCSRF(apiOp); err != nil {
		return 0, nil, err
	}

	action, err := ValidateAction(apiOp)
	if err != nil {
		return 0, nil, err
	}

	if apiOp.Schema == nil {
		return http.StatusNotFound, nil, nil
	}

	if action != nil {
		return http.StatusOK, nil, handleAction(apiOp)
	}

	switch apiOp.Method {
	case http.MethodGet:
		if apiOp.Name == "" {
			data, err := handleList(apiOp, apiOp.Schema.ListHandler, s.Defaults.ListHandler)
			return http.StatusOK, data, err
		}
		data, err := handle(apiOp, apiOp.Schema.ByIDHandler, s.Defaults.ByIDHandler)
		return http.StatusOK, data, err
	case http.MethodPatch:
		fallthrough
	case http.MethodPut:
		data, err := handle(apiOp, apiOp.Schema.UpdateHandler, s.Defaults.UpdateHandler)
		return http.StatusOK, data, err
	case http.MethodPost:
		data, err := handle(apiOp, apiOp.Schema.CreateHandler, s.Defaults.CreateHandler)
		return http.StatusCreated, data, err
	case http.MethodDelete:
		data, err := handle(apiOp, apiOp.Schema.DeleteHandler, s.Defaults.DeleteHandler)
		return http.StatusOK, data, err
	}

	return http.StatusNotFound, nil, nil
}

func handleList(apiOp *types.APIRequest, custom types.RequestListHandler, handler types.RequestListHandler) (types.APIObjectList, error) {
	if custom != nil {
		return custom(apiOp)
	}
	return handler(apiOp)
}

func handle(apiOp *types.APIRequest, custom types.RequestHandler, handler types.RequestHandler) (types.APIObject, error) {
	if custom != nil {
		return custom(apiOp)
	}
	return handler(apiOp)
}

func handleAction(context *types.APIRequest) error {
	if err := context.AccessControl.CanAction(context, context.Schema, context.Action); err != nil {
		return err
	}
	if handler, ok := context.Schema.ActionHandlers[context.Action]; ok {
		handler.ServeHTTP(context.Response, context.Request)
		return validation.ErrComplete
	}
	return nil
}

func (s *Server) handleError(apiOp *types.APIRequest, err error) {
	if apiOp.Schema != nil && apiOp.Schema.ErrorHandler != nil {
		apiOp.Schema.ErrorHandler(apiOp, err)
	} else if s.Defaults.ErrorHandler != nil {
		s.Defaults.ErrorHandler(apiOp, err)
	}
}

func (s *Server) CustomAPIUIResponseWriter(cssURL, jsURL, version writer.StringGetter) {
	wi, ok := s.ResponseWriters["html"]
	if !ok {
		return
	}
	w, ok := wi.(*writer.HTMLResponseWriter)
	if !ok {
		return
	}
	w.CSSURL = cssURL
	w.JSURL = jsURL
	w.APIUIVersion = version
}
