// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

package wshrpc

import (
	"context"
	"fmt"
	"log"
	"reflect"
	"strings"
)

type WshRpcMethodDecl struct {
	Command                 string
	CommandType             string
	MethodName              string
	CommandDataType         reflect.Type
	DefaultResponseDataType reflect.Type
}

var contextRType = reflect.TypeOf((*context.Context)(nil)).Elem()
var wshRpcInterfaceRType = reflect.TypeOf((*WshRpcInterface)(nil)).Elem()

func getWshCommandType(method reflect.Method) string {
	if method.Type.NumOut() == 1 {
		outType := method.Type.Out(0)
		if outType.Kind() == reflect.Chan {
			return RpcType_ResponseStream
		}
	}
	return RpcType_Call
}

func getWshMethodResponseType(commandType string, method reflect.Method) reflect.Type {
	switch commandType {
	case RpcType_ResponseStream:
		if method.Type.NumOut() != 1 {
			panic(fmt.Sprintf("method %q has invalid number of return values for response stream", method.Name))
		}
		outType := method.Type.Out(0)
		if outType.Kind() != reflect.Chan {
			panic(fmt.Sprintf("method %q has invalid return type %s for response stream", method.Name, outType))
		}
		elemType := outType.Elem()
		if !strings.HasPrefix(elemType.Name(), "RespOrErrorUnion") {
			panic(fmt.Sprintf("method %q has invalid return element type %s for response stream (should be RespOrErrorUnion)", method.Name, elemType))
		}
		respField, found := elemType.FieldByName("Response")
		if !found {
			panic(fmt.Sprintf("method %q has invalid return element type %s for response stream (missing Response field)", method.Name, elemType))
		}
		return respField.Type
	case RpcType_Call:
		if method.Type.NumOut() > 1 {
			return method.Type.Out(0)
		}
		return nil
	default:
		panic(fmt.Sprintf("unsupported command type %q", commandType))
	}
}

func generateWshCommandDecl(method reflect.Method) *WshRpcMethodDecl {
	if method.Type.NumIn() == 0 || method.Type.In(0) != contextRType {
		panic(fmt.Sprintf("method %q does not have context as first argument", method.Name))
	}
	cmdStr := method.Name
	decl := &WshRpcMethodDecl{}
	// remove Command suffix
	if !strings.HasSuffix(cmdStr, "Command") {
		panic(fmt.Sprintf("method %q does not have Command suffix", cmdStr))
	}
	cmdStr = cmdStr[:len(cmdStr)-len("Command")]
	decl.Command = strings.ToLower(cmdStr)
	decl.CommandType = getWshCommandType(method)
	decl.MethodName = method.Name
	var cdataType reflect.Type
	if method.Type.NumIn() > 1 {
		cdataType = method.Type.In(1)
	}
	decl.CommandDataType = cdataType
	decl.DefaultResponseDataType = getWshMethodResponseType(decl.CommandType, method)
	return decl
}

func MakeMethodMapForImpl(impl any, declMap map[string]*WshRpcMethodDecl) map[string]reflect.Method {
	rtype := reflect.TypeOf(impl)
	rtnMap := make(map[string]reflect.Method)
	for midx := 0; midx < rtype.NumMethod(); midx++ {
		method := rtype.Method(midx)
		if !strings.HasSuffix(method.Name, "Command") {
			continue
		}
		commandName := strings.ToLower(method.Name[:len(method.Name)-len("Command")])
		decl := declMap[commandName]
		if decl == nil {
			log.Printf("WARNING: method %q does not match a command method", method.Name)
			continue
		}
		rtnMap[commandName] = method
	}
	return rtnMap

}

func GenerateWshCommandDeclMap() map[string]*WshRpcMethodDecl {
	rtype := wshRpcInterfaceRType
	rtnMap := make(map[string]*WshRpcMethodDecl)
	for midx := 0; midx < rtype.NumMethod(); midx++ {
		method := rtype.Method(midx)
		decl := generateWshCommandDecl(method)
		rtnMap[decl.Command] = decl
	}
	return rtnMap
}