package plugin

import (
	"net/rpc"

	plugin "github.com/hashicorp/go-plugin"
	"github.com/hashicorp/terraform/terraform"
)

// ResourceProviderPlugin is the plugin.Plugin implementation.
type ResourceProviderPlugin struct {
	ResourceProvider func() terraform.ResourceProvider
}

func (p *ResourceProviderPlugin) Server(b *plugin.MuxBroker) (interface{}, error) {
	return &ResourceProviderServer{
		Broker:   b,
		Provider: p.ResourceProvider(),
	}, nil
}

func (p *ResourceProviderPlugin) Client(
	b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
	return &ResourceProvider{Broker: b, Client: c}, nil
}

// ResourceProvider is an implementation of terraform.ResourceProvider
// that communicates over RPC.
type ResourceProvider struct {
	Broker *plugin.MuxBroker
	Client *rpc.Client
}

func (p *ResourceProvider) Stop() error {
	var resp ResourceProviderStopResponse
	err := p.Client.Call("Plugin.Stop", new(interface{}), &resp)
	if err != nil {
		return err
	}
	if resp.Error != nil {
		err = resp.Error
	}

	return err
}

func (p *ResourceProvider) GetSchema(req *terraform.ProviderSchemaRequest) (*terraform.ProviderSchema, error) {
	var result ResourceProviderGetSchemaResponse
	args := &ResourceProviderGetSchemaArgs{
		Req: req,
	}

	err := p.Client.Call("Plugin.GetSchema", args, &result)
	if err != nil {
		return nil, err
	}

	if result.Error != nil {
		err = result.Error
	}

	return result.Schema, err
}

func (p *ResourceProvider) Input(
	input terraform.UIInput,
	c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) {
	id := p.Broker.NextId()
	go p.Broker.AcceptAndServe(id, &UIInputServer{
		UIInput: input,
	})

	var resp ResourceProviderInputResponse
	args := ResourceProviderInputArgs{
		InputId: id,
		Config:  c,
	}

	err := p.Client.Call("Plugin.Input", &args, &resp)
	if err != nil {
		return nil, err
	}
	if resp.Error != nil {
		err = resp.Error
		return nil, err
	}

	return resp.Config, nil
}

func (p *ResourceProvider) Validate(c *terraform.ResourceConfig) ([]string, []error) {
	var resp ResourceProviderValidateResponse
	args := ResourceProviderValidateArgs{
		Config: c,
	}

	err := p.Client.Call("Plugin.Validate", &args, &resp)
	if err != nil {
		return nil, []error{err}
	}

	var errs []error
	if len(resp.Errors) > 0 {
		errs = make([]error, len(resp.Errors))
		for i, err := range resp.Errors {
			errs[i] = err
		}
	}

	return resp.Warnings, errs
}

func (p *ResourceProvider) ValidateResource(
	t string, c *terraform.ResourceConfig) ([]string, []error) {
	var resp ResourceProviderValidateResourceResponse
	args := ResourceProviderValidateResourceArgs{
		Config: c,
		Type:   t,
	}

	err := p.Client.Call("Plugin.ValidateResource", &args, &resp)
	if err != nil {
		return nil, []error{err}
	}

	var errs []error
	if len(resp.Errors) > 0 {
		errs = make([]error, len(resp.Errors))
		for i, err := range resp.Errors {
			errs[i] = err
		}
	}

	return resp.Warnings, errs
}

func (p *ResourceProvider) Configure(c *terraform.ResourceConfig) error {
	var resp ResourceProviderConfigureResponse
	err := p.Client.Call("Plugin.Configure", c, &resp)
	if err != nil {
		return err
	}
	if resp.Error != nil {
		err = resp.Error
	}

	return err
}

func (p *ResourceProvider) Apply(
	info *terraform.InstanceInfo,
	s *terraform.InstanceState,
	d *terraform.InstanceDiff) (*terraform.InstanceState, error) {
	var resp ResourceProviderApplyResponse
	args := &ResourceProviderApplyArgs{
		Info:  info,
		State: s,
		Diff:  d,
	}

	err := p.Client.Call("Plugin.Apply", args, &resp)
	if err != nil {
		return nil, err
	}
	if resp.Error != nil {
		err = resp.Error
	}

	return resp.State, err
}

func (p *ResourceProvider) Diff(
	info *terraform.InstanceInfo,
	s *terraform.InstanceState,
	c *terraform.ResourceConfig) (*terraform.InstanceDiff, error) {
	var resp ResourceProviderDiffResponse
	args := &ResourceProviderDiffArgs{
		Info:   info,
		State:  s,
		Config: c,
	}
	err := p.Client.Call("Plugin.Diff", args, &resp)
	if err != nil {
		return nil, err
	}
	if resp.Error != nil {
		err = resp.Error
	}

	return resp.Diff, err
}

func (p *ResourceProvider) ValidateDataSource(
	t string, c *terraform.ResourceConfig) ([]string, []error) {
	var resp ResourceProviderValidateResourceResponse
	args := ResourceProviderValidateResourceArgs{
		Config: c,
		Type:   t,
	}

	err := p.Client.Call("Plugin.ValidateDataSource", &args, &resp)
	if err != nil {
		return nil, []error{err}
	}

	var errs []error
	if len(resp.Errors) > 0 {
		errs = make([]error, len(resp.Errors))
		for i, err := range resp.Errors {
			errs[i] = err
		}
	}

	return resp.Warnings, errs
}

func (p *ResourceProvider) Refresh(
	info *terraform.InstanceInfo,
	s *terraform.InstanceState) (*terraform.InstanceState, error) {
	var resp ResourceProviderRefreshResponse
	args := &ResourceProviderRefreshArgs{
		Info:  info,
		State: s,
	}

	err := p.Client.Call("Plugin.Refresh", args, &resp)
	if err != nil {
		return nil, err
	}
	if resp.Error != nil {
		err = resp.Error
	}

	return resp.State, err
}

func (p *ResourceProvider) ImportState(
	info *terraform.InstanceInfo,
	id string) ([]*terraform.InstanceState, error) {
	var resp ResourceProviderImportStateResponse
	args := &ResourceProviderImportStateArgs{
		Info: info,
		Id:   id,
	}

	err := p.Client.Call("Plugin.ImportState", args, &resp)
	if err != nil {
		return nil, err
	}
	if resp.Error != nil {
		err = resp.Error
	}

	return resp.State, err
}

func (p *ResourceProvider) Resources() []terraform.ResourceType {
	var result []terraform.ResourceType

	err := p.Client.Call("Plugin.Resources", new(interface{}), &result)
	if err != nil {
		// TODO: panic, log, what?
		return nil
	}

	return result
}

func (p *ResourceProvider) ReadDataDiff(
	info *terraform.InstanceInfo,
	c *terraform.ResourceConfig) (*terraform.InstanceDiff, error) {
	var resp ResourceProviderReadDataDiffResponse
	args := &ResourceProviderReadDataDiffArgs{
		Info:   info,
		Config: c,
	}

	err := p.Client.Call("Plugin.ReadDataDiff", args, &resp)
	if err != nil {
		return nil, err
	}
	if resp.Error != nil {
		err = resp.Error
	}

	return resp.Diff, err
}

func (p *ResourceProvider) ReadDataApply(
	info *terraform.InstanceInfo,
	d *terraform.InstanceDiff) (*terraform.InstanceState, error) {
	var resp ResourceProviderReadDataApplyResponse
	args := &ResourceProviderReadDataApplyArgs{
		Info: info,
		Diff: d,
	}

	err := p.Client.Call("Plugin.ReadDataApply", args, &resp)
	if err != nil {
		return nil, err
	}
	if resp.Error != nil {
		err = resp.Error
	}

	return resp.State, err
}

func (p *ResourceProvider) DataSources() []terraform.DataSource {
	var result []terraform.DataSource

	err := p.Client.Call("Plugin.DataSources", new(interface{}), &result)
	if err != nil {
		// TODO: panic, log, what?
		return nil
	}

	return result
}

func (p *ResourceProvider) Close() error {
	return p.Client.Close()
}

// ResourceProviderServer is a net/rpc compatible structure for serving
// a ResourceProvider. This should not be used directly.
type ResourceProviderServer struct {
	Broker   *plugin.MuxBroker
	Provider terraform.ResourceProvider
}

type ResourceProviderStopResponse struct {
	Error *plugin.BasicError
}

type ResourceProviderGetSchemaArgs struct {
	Req *terraform.ProviderSchemaRequest
}

type ResourceProviderGetSchemaResponse struct {
	Schema *terraform.ProviderSchema
	Error  *plugin.BasicError
}

type ResourceProviderConfigureResponse struct {
	Error *plugin.BasicError
}

type ResourceProviderInputArgs struct {
	InputId uint32
	Config  *terraform.ResourceConfig
}

type ResourceProviderInputResponse struct {
	Config *terraform.ResourceConfig
	Error  *plugin.BasicError
}

type ResourceProviderApplyArgs struct {
	Info  *terraform.InstanceInfo
	State *terraform.InstanceState
	Diff  *terraform.InstanceDiff
}

type ResourceProviderApplyResponse struct {
	State *terraform.InstanceState
	Error *plugin.BasicError
}

type ResourceProviderDiffArgs struct {
	Info   *terraform.InstanceInfo
	State  *terraform.InstanceState
	Config *terraform.ResourceConfig
}

type ResourceProviderDiffResponse struct {
	Diff  *terraform.InstanceDiff
	Error *plugin.BasicError
}

type ResourceProviderRefreshArgs struct {
	Info  *terraform.InstanceInfo
	State *terraform.InstanceState
}

type ResourceProviderRefreshResponse struct {
	State *terraform.InstanceState
	Error *plugin.BasicError
}

type ResourceProviderImportStateArgs struct {
	Info *terraform.InstanceInfo
	Id   string
}

type ResourceProviderImportStateResponse struct {
	State []*terraform.InstanceState
	Error *plugin.BasicError
}

type ResourceProviderReadDataApplyArgs struct {
	Info *terraform.InstanceInfo
	Diff *terraform.InstanceDiff
}

type ResourceProviderReadDataApplyResponse struct {
	State *terraform.InstanceState
	Error *plugin.BasicError
}

type ResourceProviderReadDataDiffArgs struct {
	Info   *terraform.InstanceInfo
	Config *terraform.ResourceConfig
}

type ResourceProviderReadDataDiffResponse struct {
	Diff  *terraform.InstanceDiff
	Error *plugin.BasicError
}

type ResourceProviderValidateArgs struct {
	Config *terraform.ResourceConfig
}

type ResourceProviderValidateResponse struct {
	Warnings []string
	Errors   []*plugin.BasicError
}

type ResourceProviderValidateResourceArgs struct {
	Config *terraform.ResourceConfig
	Type   string
}

type ResourceProviderValidateResourceResponse struct {
	Warnings []string
	Errors   []*plugin.BasicError
}

func (s *ResourceProviderServer) Stop(
	_ interface{},
	reply *ResourceProviderStopResponse) error {
	err := s.Provider.Stop()
	*reply = ResourceProviderStopResponse{
		Error: plugin.NewBasicError(err),
	}

	return nil
}

func (s *ResourceProviderServer) GetSchema(
	args *ResourceProviderGetSchemaArgs,
	result *ResourceProviderGetSchemaResponse,
) error {
	schema, err := s.Provider.GetSchema(args.Req)
	result.Schema = schema
	if err != nil {
		result.Error = plugin.NewBasicError(err)
	}
	return nil
}

func (s *ResourceProviderServer) Input(
	args *ResourceProviderInputArgs,
	reply *ResourceProviderInputResponse) error {
	conn, err := s.Broker.Dial(args.InputId)
	if err != nil {
		*reply = ResourceProviderInputResponse{
			Error: plugin.NewBasicError(err),
		}
		return nil
	}
	client := rpc.NewClient(conn)
	defer client.Close()

	input := &UIInput{Client: client}

	config, err := s.Provider.Input(input, args.Config)
	*reply = ResourceProviderInputResponse{
		Config: config,
		Error:  plugin.NewBasicError(err),
	}

	return nil
}

func (s *ResourceProviderServer) Validate(
	args *ResourceProviderValidateArgs,
	reply *ResourceProviderValidateResponse) error {
	warns, errs := s.Provider.Validate(args.Config)
	berrs := make([]*plugin.BasicError, len(errs))
	for i, err := range errs {
		berrs[i] = plugin.NewBasicError(err)
	}
	*reply = ResourceProviderValidateResponse{
		Warnings: warns,
		Errors:   berrs,
	}
	return nil
}

func (s *ResourceProviderServer) ValidateResource(
	args *ResourceProviderValidateResourceArgs,
	reply *ResourceProviderValidateResourceResponse) error {
	warns, errs := s.Provider.ValidateResource(args.Type, args.Config)
	berrs := make([]*plugin.BasicError, len(errs))
	for i, err := range errs {
		berrs[i] = plugin.NewBasicError(err)
	}
	*reply = ResourceProviderValidateResourceResponse{
		Warnings: warns,
		Errors:   berrs,
	}
	return nil
}

func (s *ResourceProviderServer) Configure(
	config *terraform.ResourceConfig,
	reply *ResourceProviderConfigureResponse) error {
	err := s.Provider.Configure(config)
	*reply = ResourceProviderConfigureResponse{
		Error: plugin.NewBasicError(err),
	}
	return nil
}

func (s *ResourceProviderServer) Apply(
	args *ResourceProviderApplyArgs,
	result *ResourceProviderApplyResponse) error {
	state, err := s.Provider.Apply(args.Info, args.State, args.Diff)
	*result = ResourceProviderApplyResponse{
		State: state,
		Error: plugin.NewBasicError(err),
	}
	return nil
}

func (s *ResourceProviderServer) Diff(
	args *ResourceProviderDiffArgs,
	result *ResourceProviderDiffResponse) error {
	diff, err := s.Provider.Diff(args.Info, args.State, args.Config)
	*result = ResourceProviderDiffResponse{
		Diff:  diff,
		Error: plugin.NewBasicError(err),
	}
	return nil
}

func (s *ResourceProviderServer) Refresh(
	args *ResourceProviderRefreshArgs,
	result *ResourceProviderRefreshResponse) error {
	newState, err := s.Provider.Refresh(args.Info, args.State)
	*result = ResourceProviderRefreshResponse{
		State: newState,
		Error: plugin.NewBasicError(err),
	}
	return nil
}

func (s *ResourceProviderServer) ImportState(
	args *ResourceProviderImportStateArgs,
	result *ResourceProviderImportStateResponse) error {
	states, err := s.Provider.ImportState(args.Info, args.Id)
	*result = ResourceProviderImportStateResponse{
		State: states,
		Error: plugin.NewBasicError(err),
	}
	return nil
}

func (s *ResourceProviderServer) Resources(
	nothing interface{},
	result *[]terraform.ResourceType) error {
	*result = s.Provider.Resources()
	return nil
}

func (s *ResourceProviderServer) ValidateDataSource(
	args *ResourceProviderValidateResourceArgs,
	reply *ResourceProviderValidateResourceResponse) error {
	warns, errs := s.Provider.ValidateDataSource(args.Type, args.Config)
	berrs := make([]*plugin.BasicError, len(errs))
	for i, err := range errs {
		berrs[i] = plugin.NewBasicError(err)
	}
	*reply = ResourceProviderValidateResourceResponse{
		Warnings: warns,
		Errors:   berrs,
	}
	return nil
}

func (s *ResourceProviderServer) ReadDataDiff(
	args *ResourceProviderReadDataDiffArgs,
	result *ResourceProviderReadDataDiffResponse) error {
	diff, err := s.Provider.ReadDataDiff(args.Info, args.Config)
	*result = ResourceProviderReadDataDiffResponse{
		Diff:  diff,
		Error: plugin.NewBasicError(err),
	}
	return nil
}

func (s *ResourceProviderServer) ReadDataApply(
	args *ResourceProviderReadDataApplyArgs,
	result *ResourceProviderReadDataApplyResponse) error {
	newState, err := s.Provider.ReadDataApply(args.Info, args.Diff)
	*result = ResourceProviderReadDataApplyResponse{
		State: newState,
		Error: plugin.NewBasicError(err),
	}
	return nil
}

func (s *ResourceProviderServer) DataSources(
	nothing interface{},
	result *[]terraform.DataSource) error {
	*result = s.Provider.DataSources()
	return nil
}
