// Package srvdesc contains data structures that represent an SCPD at a higher level than XML.
package srvdesc

import (
	"errors"
	"fmt"
	"sort"

	"github.com/huin/goupnp/v2alpha/description/xmlsrvdesc"
)

var (
	ErrBadDescription         = errors.New("bad XML description")
	ErrMissingDefinition      = errors.New("missing definition")
	ErrUnsupportedDescription = errors.New("unsupported XML description")
)

// SCPD is the top level service description.
type SCPD struct {
	ActionByName   map[string]*Action
	VariableByName map[string]*StateVariable
}

// FromXML creates an SCPD from XML data.
//
// It assumes that xmlDesc.Clean() has been called.
func FromXML(xmlDesc *xmlsrvdesc.SCPD) (*SCPD, error) {
	scpd := &SCPD{
		ActionByName:   make(map[string]*Action, len(xmlDesc.Actions)),
		VariableByName: make(map[string]*StateVariable, len(xmlDesc.StateVariables)),
	}
	stateVariables := scpd.VariableByName
	for _, xmlSV := range xmlDesc.StateVariables {
		sv, err := stateVariableFromXML(xmlSV)
		if err != nil {
			return nil, fmt.Errorf("processing state variable %q: %w", xmlSV.Name, err)
		}
		if _, exists := stateVariables[sv.Name]; exists {
			return nil, fmt.Errorf("%w: multiple state variables with name %q",
				ErrBadDescription, sv.Name)
		}
		stateVariables[sv.Name] = sv
	}
	actions := scpd.ActionByName
	for _, xmlAction := range xmlDesc.Actions {
		action, err := actionFromXML(xmlAction, scpd)
		if err != nil {
			return nil, fmt.Errorf("processing action %q: %w", xmlAction.Name, err)
		}
		if _, exists := actions[action.Name]; exists {
			return nil, fmt.Errorf("%w: multiple actions with name %q",
				ErrBadDescription, action.Name)
		}
		actions[action.Name] = action
	}
	return scpd, nil
}

// SortedActions returns the actions, in order of name.
func (scpd *SCPD) SortedActions() []*Action {
	actions := make([]*Action, 0, len(scpd.ActionByName))
	for _, a := range scpd.ActionByName {
		actions = append(actions, a)
	}
	sort.Slice(actions, func(i, j int) bool {
		return actions[i].Name < actions[j].Name
	})
	return actions
}

// Action describes a single UPnP SOAP action.
type Action struct {
	SCPD    *SCPD
	Name    string
	InArgs  []*Argument
	OutArgs []*Argument
}

// actionFromXML creates an Action from the given XML description.
func actionFromXML(xmlAction *xmlsrvdesc.Action, scpd *SCPD) (*Action, error) {
	if xmlAction.Name == "" {
		return nil, fmt.Errorf("%w: empty action name", ErrBadDescription)
	}
	action := &Action{
		SCPD: scpd,
		Name: xmlAction.Name,
	}
	var inArgs []*Argument
	var outArgs []*Argument
	for _, xmlArg := range xmlAction.Arguments {
		arg, err := argumentFromXML(xmlArg, action)
		if err != nil {
			return nil, fmt.Errorf("processing argument %q: %w", xmlArg.Name, err)
		}
		switch xmlArg.Direction {
		case "in":
			inArgs = append(inArgs, arg)
		case "out":
			outArgs = append(outArgs, arg)
		default:
			return nil, fmt.Errorf("%w: argument %q has invalid direction %q",
				ErrBadDescription, xmlArg.Name, xmlArg.Direction)
		}
	}
	action.InArgs = inArgs
	action.OutArgs = outArgs
	return action, nil
}

// Argument description data.
type Argument struct {
	Action                   *Action
	Name                     string
	RelatedStateVariableName string
}

// argumentFromXML creates an Argument from the XML description.
func argumentFromXML(xmlArg *xmlsrvdesc.Argument, action *Action) (*Argument, error) {
	if xmlArg.Name == "" {
		return nil, fmt.Errorf("%w: empty argument name", ErrBadDescription)
	}
	if xmlArg.RelatedStateVariable == "" {
		return nil, fmt.Errorf("%w: empty related state variable", ErrBadDescription)
	}
	return &Argument{
		Action:                   action,
		Name:                     xmlArg.Name,
		RelatedStateVariableName: xmlArg.RelatedStateVariable,
	}, nil
}

func (arg *Argument) RelatedStateVariable() (*StateVariable, error) {
	if v, ok := arg.Action.SCPD.VariableByName[arg.RelatedStateVariableName]; ok {
		return v, nil
	}
	return nil, fmt.Errorf("%w: state variable %q", ErrMissingDefinition, arg.RelatedStateVariableName)
}

// StateVariable description data.
type StateVariable struct {
	Name     string
	DataType string

	AllowedValues []string
}

func stateVariableFromXML(xmlSV *xmlsrvdesc.StateVariable) (*StateVariable, error) {
	if xmlSV.Name == "" {
		return nil, fmt.Errorf("%w: empty state variable name", ErrBadDescription)
	}
	if xmlSV.DataType.Type != "" {
		return nil, fmt.Errorf("%w: unsupported data type %q",
			ErrUnsupportedDescription, xmlSV.DataType.Type)
	}
	if xmlSV.DataType.Name != "string" && len(xmlSV.AllowedValues) > 0 {
		return nil, fmt.Errorf("%w: allowedValueList is currently unsupported for type %q",
			ErrUnsupportedDescription, xmlSV.DataType.Name)
	}
	return &StateVariable{
		Name:          xmlSV.Name,
		DataType:      xmlSV.DataType.Name,
		AllowedValues: xmlSV.AllowedValues,
	}, nil
}