// 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 registry

import (
	"context"
	"time"

	"github.com/drone/drone-go/plugin/secret"
	"github.com/drone/drone-yaml/yaml"
	"github.com/drone/drone/core"
	"github.com/drone/drone/logger"
	"github.com/drone/drone/plugin/registry/auths"

	droneapi "github.com/drone/drone-go/drone"
)

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

type externalController struct {
	endpoint   string
	secret     string
	skipVerify bool
}

func (c *externalController) List(ctx context.Context, in *core.RegistryArgs) ([]*core.Registry, error) {
	var results []*core.Registry

	for _, match := range in.Pipeline.PullSecrets {
		logger := logger.FromContext(ctx).
			WithField("name", match).
			WithField("kind", "secret").
			WithField("secret", c.endpoint)
		logger.Trace("image_pull_secrets: find 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, match)
		if !ok {
			logger.Trace("image_pull_secrets: no matching secret resource in yaml")
			return nil, nil
		}

		logger = logger.
			WithField("get.path", path).
			WithField("get.name", name)

		// 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("image_pull_secrets: 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 == "" {
			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.WithError(err).Trace("image_pull_secrets: pull_request access denied")
			return nil, nil
		}

		parsed, err := auths.ParseString(res.Data)
		if err != nil {
			return nil, err
		}

		logger.Trace("image_pull_secrets: found secret")
		results = append(results, parsed...)
	}

	return results, 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) droneapi.Repo {
	return droneapi.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) droneapi.Build {
	return droneapi.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,
	}
}
