// Copyright 2016 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package plugin

import (
	"context"
	"crypto/tls"
	"errors"
	"fmt"
	"net/url"
	"strings"
	"time"

	log "github.com/Sirupsen/logrus"

	"github.com/vmware/govmomi/object"
	"github.com/vmware/govmomi/vim25/types"
	"github.com/vmware/vic/pkg/trace"
	"github.com/vmware/vic/pkg/version"
	"github.com/vmware/vic/pkg/vsphere/session"
)

type Info struct {
	*ManagedEntityInfo

	Company               string
	Key                   string
	Name                  string
	ServerThumbprint      string
	ShowInSolutionManager bool
	Summary               string
	Type                  string
	URL                   string
	Version               string
}

type ManagedEntityInfo struct {
	Description  string
	IconURL      string
	SmallIconURL string
	EntityType   string
}

type Pluginator struct {
	Session          *session.Session
	ExtensionManager *object.ExtensionManager
	Context          context.Context

	info *Info

	tURL        *url.URL
	tThumbprint string
	connected   bool
}

func NewPluginator(ctx context.Context, target *url.URL, thumbprint string, i *Info) (*Pluginator, error) {
	defer trace.End(trace.Begin(""))

	p := &Pluginator{
		tURL:        target,
		tThumbprint: thumbprint,
		info:        i,
	}
	p.Context = ctx

	err := p.connect()
	if err != nil {
		return nil, err
	}

	return p, nil
}

func (p *Pluginator) disconnect() error {
	defer trace.End(trace.Begin(""))
	if p.Session != nil {
		err := p.Session.Client.Logout(p.Context)
		if err != nil {
			return fmt.Errorf("failed to disconnect: %s", err)
		}
	}
	p.connected = false
	return nil
}

func (p *Pluginator) connect() error {
	defer trace.End(trace.Begin(""))
	var err error

	if p.tURL.Scheme == "https" && p.tThumbprint == "" {
		var cert object.HostCertificateInfo
		if err = cert.FromURL(p.tURL, new(tls.Config)); err != nil {
			return err
		}

		if cert.Err != nil {
			log.Errorf("Failed to verify certificate for target=%s (thumbprint=%s)",
				p.tURL.Host, cert.ThumbprintSHA1)
			return cert.Err
		}

		p.tThumbprint = cert.ThumbprintSHA1
		log.Debugf("Accepting host %q thumbprint %s", p.tURL.Host, p.tThumbprint)
	}

	sessionconfig := &session.Config{
		Thumbprint: p.tThumbprint,
		UserAgent:  version.UserAgent("vic-ui-installer"),
	}
	sessionconfig.Service = p.tURL.String()

	p.Session = session.NewSession(sessionconfig)
	p.Session, err = p.Session.Connect(p.Context)
	if err != nil {
		return fmt.Errorf("failed to connect: %s", err)
	}

	// #nosec: Errors unhandled.
	p.Session.Populate(p.Context)

	em, err := object.GetExtensionManager(p.Session.Client.Client)
	if err != nil {
		return fmt.Errorf("failed to get extension manager: %s", err)
	}
	p.ExtensionManager = em

	p.connected = true
	return nil
}

// Register installs an extension to the target
func (p *Pluginator) Register() error {
	defer trace.End(trace.Begin(""))
	var err error
	if !p.connected {
		return errors.New("not connected")
	}

	desc := types.Description{
		Label:   p.info.Name,
		Summary: p.info.Summary,
	}

	e := types.Extension{
		Key:         p.info.Key,
		Version:     p.info.Version,
		Company:     p.info.Company,
		Description: &desc,
	}

	if p.info.ManagedEntityInfo != nil {
		e.Type = p.info.EntityType
	}

	eci := types.ExtensionClientInfo{
		Version:     p.info.Version,
		Company:     p.info.Company,
		Description: &desc,
		Type:        p.info.Type,
		Url:         p.info.URL,
	}
	e.Client = append(e.Client, eci)

	d := types.KeyValue{
		Key:   "name",
		Value: p.info.Name,
	}

	eri := types.ExtensionResourceInfo{
		Locale: "en_US",
		Module: "name",
	}

	if p.info.ManagedEntityInfo != nil {
		mei := types.ExtManagedEntityInfo{
			Description: p.info.ManagedEntityInfo.Description,
			Type:        p.info.ManagedEntityInfo.EntityType,
		}
		e.ManagedEntityInfo = append(e.ManagedEntityInfo, mei)
	}

	eri.Data = append(eri.Data, d)

	e.ResourceList = append(e.ResourceList, eri)

	// HTTPS requires extension server info
	if strings.HasPrefix(strings.ToLower(p.info.URL), "https://") {
		esi := types.ExtensionServerInfo{
			Url:              p.info.URL,
			Description:      &desc,
			Company:          p.info.Company,
			Type:             "HTTPS",
			AdminEmail:       []string{"noreply@vmware.com"},
			ServerThumbprint: p.info.ServerThumbprint,
		}
		e.Server = append(e.Server, esi)
	}

	e.ShownInSolutionManager = &p.info.ShowInSolutionManager

	e.LastHeartbeatTime = time.Now().UTC()

	err = p.ExtensionManager.Register(p.Context, e)
	if err != nil {
		return err
	}

	return nil
}

// Unregister removes an extension from the target
func (p *Pluginator) Unregister(key string) error {
	defer trace.End(trace.Begin(""))
	if !p.connected {
		return errors.New("not connected")
	}

	if err := p.ExtensionManager.Unregister(p.Context, key); err != nil {
		return err
	}
	return nil
}

// IsRegistered checks for presence of an extension on the target
func (p *Pluginator) IsRegistered(key string) (bool, error) {
	defer trace.End(trace.Begin(""))
	if !p.connected {
		return false, errors.New("not connected")
	}

	e, err := p.ExtensionManager.Find(p.Context, key)
	if err != nil {
		return false, err
	}
	if e != nil {
		log.Debugf("%q is registered", key)
		return true, nil
	}
	log.Debugf("%q is not registered", key)
	return false, nil
}

// IsRegistered checks for presence of an extension on the target
func (p *Pluginator) GetPlugin(key string) (*types.Extension, error) {
	defer trace.End(trace.Begin(""))
	if !p.connected {
		return nil, errors.New("not connected")
	}

	return p.ExtensionManager.Find(p.Context, key)
}
