package lib

import (
	"encoding/json"
	"fmt"
	"strings"

	"github.com/caddyserver/caddy/v2/modules/caddyhttp"

	"github.com/caddyserver/caddy/v2"
	"go.starlark.net/starlark"
)

// ResponderModule represents a module that satisfies the caddyhttp handler.
type ResponderModule struct {
	Name     string
	Cfg      json.RawMessage
	Instance caddyhttp.Handler
}

func (r ResponderModule) Freeze()               {}
func (r ResponderModule) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable: responder module") }
func (r ResponderModule) String() string        { return "responder module" }
func (r ResponderModule) Type() string          { return "responder module" }
func (r ResponderModule) Truth() starlark.Bool  { return true }

// Middleware represents a module that satisfies the starlark Value interface.
type Middleware struct {
	Name     string
	Cfg      json.RawMessage
	Instance caddyhttp.MiddlewareHandler
}

func (r Middleware) Freeze()               {}
func (r Middleware) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable: middleware") }
func (r Middleware) String() string        { return "middleware" }
func (r Middleware) Type() string          { return "middleware" }
func (r Middleware) Truth() starlark.Bool  { return true }

// LoadMiddleware represents the method exposed to starlark to load a Caddy module.
type LoadMiddleware struct {
	Middleware Middleware
	Ctx        caddy.Context
}

func (r LoadMiddleware) Freeze()               {}
func (r LoadMiddleware) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable: loadMiddleware") }
func (r LoadMiddleware) String() string        { return "loadMiddleware" }
func (r LoadMiddleware) Type() string          { return "function: loadMiddleware" }
func (r LoadMiddleware) Truth() starlark.Bool  { return true }

// Run is the method bound to the starlark loadMiddleware function.
func (r *LoadMiddleware) Run(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
	var name string
	var cfg *starlark.Dict
	err := starlark.UnpackPositionalArgs(fn.Name(), args, kwargs, 2, &name, &cfg)
	if err != nil {
		return starlark.None, fmt.Errorf("unpacking arguments: %v", err.Error())
	}

	js := json.RawMessage(cfg.String())

	if !strings.Contains(name, "http.handlers.") {
		name = fmt.Sprintf("http.handlers.%s", name)
	}

	inst, err := r.Ctx.LoadModuleByID(name, js)
	if err != nil {
		return starlark.None, err
	}

	mid, ok := inst.(caddyhttp.MiddlewareHandler)
	if !ok {
		return starlark.None, fmt.Errorf("could not assert as middleware handler")
	}

	m := Middleware{
		Name:     name,
		Cfg:      js,
		Instance: mid,
	}

	r.Middleware = m

	return m, nil
}

// LoadResponder represents the method exposed to starlark to load a Caddy middleware responder.
type LoadResponder struct {
	Module ResponderModule
	Ctx    caddy.Context
}

func (r LoadResponder) Freeze()               {}
func (r LoadResponder) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable: loadModule") }
func (r LoadResponder) String() string        { return "loadModule" }
func (r LoadResponder) Type() string          { return "function: loadModule" }
func (r LoadResponder) Truth() starlark.Bool  { return true }

// Run is the method bound to the starlark loadResponder function.
func (r *LoadResponder) Run(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
	var name string
	var cfg *starlark.Dict
	err := starlark.UnpackPositionalArgs(fn.Name(), args, kwargs, 2, &name, &cfg)
	if err != nil {
		return starlark.None, fmt.Errorf("unpacking arguments: %v", err.Error())
	}

	js := json.RawMessage(cfg.String())

	if !strings.Contains(name, "http.handlers.") {
		name = fmt.Sprintf("http.handlers.%s", name)
	}

	inst, err := r.Ctx.LoadModuleByID(name, js)
	if err != nil {
		return starlark.None, err
	}

	res, ok := inst.(caddyhttp.Handler)
	if !ok {
		return starlark.None, fmt.Errorf("could not assert as responder")
	}

	m := ResponderModule{
		Name:     name,
		Cfg:      js,
		Instance: res,
	}

	r.Module = m

	return m, nil
}

// Execute represents the method exposed to starlark to build a middleware chain.
type Execute struct {
	Modules []Middleware
}

func (r Execute) Freeze()               {}
func (r Execute) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable: execute") }
func (r Execute) String() string        { return "execute" }
func (r Execute) Type() string          { return "function: execute" }
func (r Execute) Truth() starlark.Bool  { return true }

// Run is the method bound to the starlark execute function.
func (r *Execute) Run(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
	var mids *starlark.List
	err := starlark.UnpackPositionalArgs(fn.Name(), args, kwargs, 1, &mids)
	if err != nil {
		return starlark.None, fmt.Errorf("unpacking arguments: %v", err.Error())
	}

	for i := 0; i < mids.Len(); i++ {
		val, ok := mids.Index(i).(Middleware)
		if !ok {
			return starlark.None, fmt.Errorf("cannot get module from execute")
		}

		r.Modules = append(r.Modules, val)
	}

	return starlark.None, nil
}