package api

import (
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"reflect"

	"gordenko.dev/dima/fixme"
	"gordenko.dev/dima/web"
)

var errorInterface = reflect.TypeFor[error]()

type APIRequest interface {
	FuncName() string
	Data() json.RawMessage
	AuthToken() (string, error) // SID or AccessToken
}

type funcData struct {
	Func            reflect.Value
	ArgumentType    reflect.Type
	ReturnParamType reflect.Type
	HasArgument     bool
	HasReturnParam  bool
	HasExtra        bool
	IsPublic        bool
	ExtraFunc       reflect.Value
	ExtraType       reflect.Type
}

type Options struct {
	Logger         *log.Logger
	ExtractRequest func(*web.State) (APIRequest, error)
	WriteResponse  func(*web.Reply, any, error)
	Authenticate   func(string, []int) (int64, string, error) // (authToken, apps) => (userID, tokenID, error)
	App            int
}

type API struct {
	funcMap        map[string]funcData
	logger         *log.Logger
	extractRequest func(*web.State) (APIRequest, error)
	writeResponse  func(*web.Reply, any, error)               // (reply, result, error)
	authenticate   func(string, []int) (int64, string, error) // (authToken) => (userID, tokenID, error)
	app            int
}

func NewAPI(opt Options) (*API, error) {
	if opt.Logger == nil {
		return nil, errors.New("Logger option is required")
	}
	s := &API{
		funcMap:        make(map[string]funcData),
		logger:         opt.Logger,
		authenticate:   opt.Authenticate,
		app:            opt.App,
		extractRequest: ExtractRPCRequest,
		writeResponse:  WriteRPCResponse,
	}
	if opt.ExtractRequest != nil {
		s.extractRequest = opt.ExtractRequest
	}
	if opt.WriteResponse != nil {
		s.writeResponse = opt.WriteResponse
	}
	return s, nil
}

func (s *API) getFuncT(fd *funcData, funcName string, f any) reflect.Type {
	if funcName == "" {
		log.Fatalln("funcName is empty")
	}
	_, ok := s.funcMap[funcName]
	if ok {
		log.Fatalf("%s: already registered\n", funcName)
	}
	// проверка что - API метод это вообще функция
	funcT := reflect.TypeOf(f)
	if funcT.Kind() != reflect.Func {
		log.Fatalf("%s: not func\n", funcName)
	}
	fd.Func = reflect.ValueOf(f)
	return funcT
}

func (s *API) check1stAsInt64(funcName string, funcT reflect.Type) {
	p1 := funcT.In(0)
	iface := reflect.New(p1).Elem().Interface()
	_, ok := iface.(int64)
	if !ok {
		log.Fatalf("%s: 1st argument type must be int64\n", funcName)
	}
}

func (s *API) checkExtraFunc(fd *funcData, funcName string, stateToExtras any) {
	// проверка что - extra функция - это вообще функция
	extraFuncT := reflect.TypeOf(stateToExtras)
	if extraFuncT.Kind() != reflect.Func {
		log.Fatalf("%s: stateToExtras not func\n", funcName)
	}
	if extraFuncT.NumIn() != 1 {
		log.Fatalf("%s: stateToExtras must have 1 argument", funcName)
	}
	if extraFuncT.In(0) != reflect.TypeOf((*web.State)(nil)) {
		log.Fatalf("%s: stateToExtras must have 1 argument of type *web.State",
			funcName)
	}
	switch extraFuncT.NumOut() {
	case 1:
	case 2:
		mustErr := extraFuncT.Out(1)
		if !mustErr.Implements(errorInterface) {
			log.Fatalf("%s: 2nd return value of stateToExtras not implements error interface\n",
				funcName)
		}
	default:
		log.Fatalf("%s: stateToExtras must have 1 or 2 return value", funcName)
	}
	fd.HasExtra = true
	fd.ExtraFunc = reflect.ValueOf(stateToExtras)
	// тип, возвращаемый extra-функцией
	fd.ExtraType = extraFuncT.Out(0)
}

func (s *API) checkReturns(fd *funcData, funcName string, funcT reflect.Type) {
	returnsNumber := funcT.NumOut()
	switch returnsNumber {
	case 1:
		mustErr := funcT.Out(0)
		if !mustErr.Implements(errorInterface) {
			log.Fatalf("%s: 1st return value not implements error interface\n",
				funcName)
		}
	case 2:
		mustErr := funcT.Out(1)
		if !mustErr.Implements(errorInterface) {
			log.Fatalf("%s: 2nd return value not implements error interface\n",
				funcName)
		}
		fd.ReturnParamType = funcT.Out(0)
		fd.HasReturnParam = true
	default:
		log.Fatalf("%s: has wrong return values qty %d\n", funcName, returnsNumber)
	}
}

func (s *API) PrivateFunc(funcName string, f any) {
	if s.authenticate == nil {
		panic("PrivateFunc: authenticate func not set")
	}
	if s.app <= 0 {
		panic("PrivateFunc: app not set")
	}
	var fd funcData
	funcT := s.getFuncT(&fd, funcName, f)
	// проверяем чтобы первый аргумент был userID
	s.check1stAsInt64(funcName, funcT)
	// проверяю аргументы
	argsNumber := funcT.NumIn()
	switch argsNumber {
	case 1:
		// userID
	case 2:
		// userID, request
		fd.ArgumentType = funcT.In(1)
		fd.HasArgument = true
	default:
		log.Fatalf("%s: has wrong arguments qty %d\n", funcName, argsNumber)
	}
	// проверяю возвращаемые значения
	s.checkReturns(&fd, funcName, funcT)
	s.funcMap[funcName] = fd
}

// stateToExtras - функция с сигнатурой func(*web.State) T, которая на вход
// получает State объект, а возвращает извлеченные данные, например IP адрес.
func (s *API) ExtraPrivateFunc(funcName string, f any, stateToExtras any) {
	if s.authenticate == nil {
		panic("ExtraPrivateFunc: authenticate func not set")
	}
	if s.app <= 0 {
		panic("ExtraPrivateFunc: app not set")
	}
	var fd funcData
	funcT := s.getFuncT(&fd, funcName, f)
	// проверяю extra функцию
	s.checkExtraFunc(&fd, funcName, stateToExtras)
	// проверяем чтобы первый аргумент был userID
	s.check1stAsInt64(funcName, funcT)
	// проверяю аргументы
	argsNumber := funcT.NumIn()
	switch argsNumber {
	case 2:
		// userID, extras
		if funcT.In(1) != fd.ExtraType {
			log.Fatalf("%s: 2nd argument type must be %s\n",
				funcName, fd.ExtraType.String())
		}
	case 3:
		// userID, request, extras
		if funcT.In(2) != fd.ExtraType {
			log.Fatalf("%s: 3rd argument type must be %s\n",
				funcName, fd.ExtraType.String())
		}
		fd.ArgumentType = funcT.In(1)
		fd.HasArgument = true
	default:
		log.Fatalf("%s: has wrong arguments qty %d\n", funcName, argsNumber)
	}
	// проверяю возвращаемые значения
	s.checkReturns(&fd, funcName, funcT)
	s.funcMap[funcName] = fd
}

func (s *API) PublicFunc(funcName string, f any) {
	fd := funcData{
		IsPublic: true,
	}
	funcT := s.getFuncT(&fd, funcName, f)
	paramsNumber := funcT.NumIn()
	switch paramsNumber {
	case 0:
		// ok
	case 1:
		fd.ArgumentType = funcT.In(0)
		fd.HasArgument = true
	default:
		log.Fatalf("%s: has wrong arguments qty %d\n", funcName, paramsNumber)
	}
	// проверяю возвращаемые значения
	s.checkReturns(&fd, funcName, funcT)
	s.funcMap[funcName] = fd
}

// stateToExtras - функция с сигнатурой func(*web.State) T, которая на вход
// получает State объект, а возвращает извлеченные данные, например IP адрес.
func (s *API) ExtraPublicFunc(funcName string, f any, stateToExtras any) {
	fd := funcData{
		IsPublic: true,
	}
	funcT := s.getFuncT(&fd, funcName, f)
	// проверяю extra функцию
	s.checkExtraFunc(&fd, funcName, stateToExtras)

	paramsNumber := funcT.NumIn()
	switch paramsNumber {
	case 1:
		// extras
		if funcT.In(0) != fd.ExtraType {
			log.Fatalf("%s: 1st argument type must be %s\n",
				funcName, fd.ExtraType.String())
		}
	case 2:
		// request, extras
		if funcT.In(1) != fd.ExtraType {
			log.Fatalf("%s: 2nd argument type must be %s\n",
				funcName, fd.ExtraType.String())
		}
		fd.ArgumentType = funcT.In(0)
		fd.HasArgument = true // ?
	default:
		log.Fatalf("%s: has wrong arguments qty %d\n", funcName, paramsNumber)
	}
	// проверяю возвращаемые значения
	s.checkReturns(&fd, funcName, funcT)
	s.funcMap[funcName] = fd
}

func (s *API) callFunc(fd funcData, data []byte, args []reflect.Value, extraValue reflect.Value) (result any, err error) {
	if fd.HasArgument {
		instance := reflect.New(fd.ArgumentType)
		dst := instance.Interface()
		switch dst.(type) {
		case *[]byte:
			args = append(args, reflect.ValueOf(data))
		default:
			if len(data) > 0 {
				err = json.Unmarshal(data, dst)
				if err != nil {
					err = fix.Error(BadRequestBody)
					return
				}
			}
			args = append(args, instance.Elem())
		}
	}
	if fd.HasExtra {
		args = append(args, extraValue)
	}
	output := fd.Func.Call(args)
	if fd.HasReturnParam {
		result = output[0].Interface()
		err, _ = output[1].Interface().(error)
	} else {
		result = nil
		err, _ = output[0].Interface().(error)
	}
	return
}

func (s *API) Handle(state *web.State, reply *web.Reply) error {
	result, err := s.handle(state)
	if err != nil {
		s.logger.Printf("api call: %s\n", err)
	}
	s.writeResponse(reply, result, err)
	return nil
}

func (s *API) handle(state *web.State) (_ any, err error) {
	req, err := s.extractRequest(state)
	if err != nil {
		return
	}
	fd, ok := s.funcMap[req.FuncName()]
	if !ok {
		return nil, fixme.Error{
			Code:    UnknownFunc,
			Message: fmt.Sprintf("UnknownFunc '%s'", req.FuncName()),
		}
	}
	var (
		extraValue reflect.Value
		args       []reflect.Value
	)
	if fd.HasExtra {
		// вызываю метод
		extraOutput := fd.ExtraFunc.Call([]reflect.Value{
			reflect.ValueOf(state),
		})
		// Если 2 возвращаемих значения - проверка на ошибку
		if fd.ExtraFunc.Type().NumOut() == 2 {
			err, _ = extraOutput[1].Interface().(error)
			if err != nil {
				err = fmt.Errorf("call extraFunc: %s", err)
				return
			}
		}
		extraValue = extraOutput[0]
	}
	if !fd.IsPublic {
		var (
			authToken string
			userID    int64
			apps      = []int{
				s.app,
			}
		)
		authToken, err = req.AuthToken()
		if err != nil {
			return
		}
		userID, _, err = s.authenticate(authToken, apps)
		if err != nil {
			return
		}
		args = []reflect.Value{
			reflect.ValueOf(userID),
		}
	}
	return s.callFunc(fd, req.Data(), args, extraValue)
}

// type MethodType int

// const (
// 	SignInMethod  MethodType = 1
// 	PrivateMethod MethodType = 2
// 	PublicMethod  MethodType = 3
// )

// type MethodInfo struct {
// 	Name       string     `json:"name"`
// 	MethodType MethodType `json:"methodType"`
// 	Req        *TypeInfo  `json:"req"`
// 	Resp       *TypeInfo  `json:"resp"`
// }

// func (s *API) Spec() (list []MethodInfo, err error) {
// 	for funcName, fd := range s.funcMap {
// 		fmt.Printf("method: %s\n", funcName)
// 		if funcName == specFuncName {
// 			continue
// 		}
// 		info := MethodInfo{
// 			Name: funcName,
// 		}
// 		if fd.IsSignInFunc {
// 			info.MethodType = SignInMethod
// 		} else if fd.IsPublic {
// 			info.MethodType = PublicMethod
// 		} else {
// 			info.MethodType = PrivateMethod
// 		}
// 		if fd.HasArgument {
// 			info.Req, err = DescribeType(fd.ArgumentType)
// 			if err != nil {
// 				return nil, fmt.Errorf("DescribeType of argument %s: %s",
// 					funcName, err)
// 			}
// 		}
// 		if fd.HasReturnParam {
// 			info.Resp, err = DescribeType(fd.ReturnParamType)
// 			if err != nil {
// 				return nil, fmt.Errorf("DescribeType of return value  %s: %s",
// 					funcName, err)
// 			}
// 		}
// 		list = append(list, info)
// 	}
// 	fmt.Printf("methods: %d\n", len(list))
// 	return
// }
