// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Drone Non-Commercial License
// that can be found in the LICENSE file.

// +build !oss

package secret

import (
	"context"
	"time"

	"github.com/drone/drone-yaml/yaml"
	"github.com/drone/drone/core"
	"github.com/drone/drone/logger"

	"github.com/drone/drone-go/drone"
	"github.com/drone/drone-go/plugin/secret"
)

// External returns a new external Secret controller.
func External(endpoint, secret string, skipVerify bool) core.SecretService {
	return &externalController{
		endpoint:   endpoint,
		secret:     secret,
		skipVerify: skipVerify,
	}
}

type externalController struct {
	endpoint   string
	secret     string
	skipVerify bool
}

func (c *externalController) Find(ctx context.Context, in *core.SecretArgs) (*core.Secret, error) {
	if c.endpoint == "" {
		return nil, nil
	}

	logger := logger.FromContext(ctx).
		WithField("name", in.Name).
		WithField("kind", "secret")

	// lookup the named secret in the manifest. If the
	// secret does not exist, return a nil variable,
	// allowing the next secret controller in the chain
	// to be invoked.
	path, name, ok := getExternal(in.Conf, in.Name)
	if !ok {
		logger.Trace("secret: external: no matching secret")
		return nil, nil
	}

	// include a timeout to prevent an API call from
	// hanging the build process indefinitely. The
	// external service must return a request within
	// one minute.
	ctx, cancel := context.WithTimeout(ctx, time.Minute)
	defer cancel()

	req := &secret.Request{
		Name:  name,
		Path:  path,
		Repo:  toRepo(in.Repo),
		Build: toBuild(in.Build),
	}
	client := secret.Client(c.endpoint, c.secret, c.skipVerify)
	res, err := client.Find(ctx, req)
	if err != nil {
		logger.WithError(err).Trace("secret: external: cannot get secret")
		return nil, err
	}

	// if no error is returned and the secret is empty,
	// this indicates the client returned No Content,
	// and we should exit with no secret, but no error.
	if res.Data == "" {
		logger.Trace("secret: external: secret disabled for pull requests")
		return nil, nil
	}

	// the secret can be restricted to non-pull request
	// events. If the secret is restricted, return
	// empty results.
	if (res.Pull == false && res.PullRequest == false) &&
		in.Build.Event == core.EventPullRequest {
		logger.Trace("secret: external: restricted from forks")
		return nil, nil
	}

	logger.Trace("secret: external: found matching secret")

	return &core.Secret{
		Name:        in.Name,
		Data:        res.Data,
		PullRequest: res.Pull,
	}, nil
}

func getExternal(manifest *yaml.Manifest, match string) (path, name string, ok bool) {
	for _, resource := range manifest.Resources {
		secret, ok := resource.(*yaml.Secret)
		if !ok {
			continue
		}
		if secret.Name != match {
			continue
		}
		if secret.Get.Name == "" && secret.Get.Path == "" {
			continue
		}
		return secret.Get.Path, secret.Get.Name, true
	}
	return
}

func toRepo(from *core.Repository) drone.Repo {
	return drone.Repo{
		ID:         from.ID,
		UID:        from.UID,
		UserID:     from.UserID,
		Namespace:  from.Namespace,
		Name:       from.Name,
		Slug:       from.Slug,
		SCM:        from.SCM,
		HTTPURL:    from.HTTPURL,
		SSHURL:     from.SSHURL,
		Link:       from.Link,
		Branch:     from.Branch,
		Private:    from.Private,
		Visibility: from.Visibility,
		Active:     from.Active,
		Config:     from.Config,
		Trusted:    from.Trusted,
		Protected:  from.Protected,
		Timeout:    from.Timeout,
	}
}

func toBuild(from *core.Build) drone.Build {
	return drone.Build{
		ID:           from.ID,
		RepoID:       from.RepoID,
		Trigger:      from.Trigger,
		Number:       from.Number,
		Parent:       from.Parent,
		Status:       from.Status,
		Error:        from.Error,
		Event:        from.Event,
		Action:       from.Action,
		Link:         from.Link,
		Timestamp:    from.Timestamp,
		Title:        from.Title,
		Message:      from.Message,
		Before:       from.Before,
		After:        from.After,
		Ref:          from.Ref,
		Fork:         from.Fork,
		Source:       from.Source,
		Target:       from.Target,
		Author:       from.Author,
		AuthorName:   from.AuthorName,
		AuthorEmail:  from.AuthorEmail,
		AuthorAvatar: from.AuthorAvatar,
		Sender:       from.Sender,
		Params:       from.Params,
		Deploy:       from.Deploy,
		Started:      from.Started,
		Finished:     from.Finished,
		Created:      from.Created,
		Updated:      from.Updated,
		Version:      from.Version,
	}
}
