package targets

import (
	"context"
	"net/url"
	"regexp"
	"strconv"
	"time"

	"github.com/aws/aws-sdk-go/aws/client"
	"github.com/pkg/errors"
	"github.com/twitchtv/twirp"
	"go.uber.org/zap"

	"code.justin.tv/eventbus/controlplane/internal/arn"
	"code.justin.tv/eventbus/controlplane/internal/auditlog"
	"code.justin.tv/eventbus/controlplane/internal/db"
	"code.justin.tv/eventbus/controlplane/internal/environment"
	"code.justin.tv/eventbus/controlplane/internal/ldap"
	"code.justin.tv/eventbus/controlplane/internal/logger"
	"code.justin.tv/eventbus/controlplane/internal/sqsutil"
	"code.justin.tv/eventbus/controlplane/internal/twirperr"
	"code.justin.tv/eventbus/controlplane/internal/validator"
	"code.justin.tv/eventbus/controlplane/rpc"
)

type SQSManager interface {
	GetQueueAttributes(ctx context.Context, url string) (map[string]string, error)
}

type TargetsService struct {
	DB             db.DB
	RootSession    client.ConfigProvider
	QueueValidator *validator.QueueValidator
	SQSManager     SQSManager
}

func (ts *TargetsService) GetTargetsForService(ctx context.Context, req *rpc.GetTargetsForServiceReq) (*rpc.GetTargetsForServiceResp, error) {
	serviceID, err := strconv.Atoi(req.ServiceId)
	if err != nil {
		return nil, twirp.NewError(twirp.InvalidArgument, "could not convert service id to int")
	}

	s, err := ts.DB.ServiceByID(ctx, serviceID)
	if err != nil {
		return nil, twirperr.Convert(err)
	} else if !ldap.BelongsToGroup(ctx, s.LDAPGroup) {
		return nil, twirp.NewError(twirp.PermissionDenied, "access denied")
	}

	targets, err := ts.DB.SubscriptionTargetsByServiceID(ctx, serviceID)
	if err != nil {
		return nil, err
	}

	outTargets := make([]*rpc.Target, len(targets))
	for i, target := range targets {
		t := targetToRPC(target)
		outTargets[i] = t
	}

	return &rpc.GetTargetsForServiceResp{
		ServiceId: req.ServiceId,
		Targets:   outTargets,
	}, nil
}

func (ts *TargetsService) Create(ctx context.Context, req *rpc.CreateTargetReq) (*rpc.CreateTargetResp, error) {
	serviceID, err := strconv.Atoi(req.ServiceId)
	if err != nil {
		return nil, errors.Wrap(err, "could not convert service id to int")
	}

	base := &auditlog.BaseLog{
		ResourceName: req.Name,
		ServiceID:    serviceID,
		DB:           ts.DB,
	}

	s, err := ts.DB.ServiceByID(ctx, serviceID)
	if err != nil {
		err = errors.Wrap(err, "could not validate service owning target to create")
		base.LogSubscriptionTargetCreate(ctx, err, nil)
		return nil, twirperr.Convert(err)
	} else if !ldap.BelongsToGroup(ctx, s.LDAPGroup) {
		err = twirp.NewError(twirp.PermissionDenied, "access denied")
		base.LogSubscriptionTargetCreate(ctx, err, nil)
		return nil, err
	}

	if req.Name == "" {
		err = errors.New("target name cannot be blank")
		base.LogSubscriptionTargetCreate(ctx, err, nil)
		return nil, err
	}

	err = validator.Validate(req.GetName(), isValidTargetName)
	if err != nil {
		err = twirp.NewError(twirp.InvalidArgument, "invalid subscription target name "+err.Error())
		base.LogSubscriptionTargetCreate(ctx, err, nil)
		return nil, err
	}

	target := &db.SubscriptionTarget{
		Name:      req.Name,
		ServiceID: serviceID,
	}

	if req.Type == rpc.TargetType_TARGET_TYPE_SQS {
		sqsDetails := req.GetSqs()
		if sqsDetails == nil {
			err = twirp.NewError(twirp.InvalidArgument, "missing sqs details")
			base.LogSubscriptionTargetCreate(ctx, err, nil)
			return nil, err
		}

		queueURL := sqsDetails.GetQueueUrl()
		if queueURL == "" {
			err = twirp.NewError(twirp.InvalidArgument, "must supply a queue URL")
			base.LogSubscriptionTargetCreate(ctx, err, nil)
			return nil, err
		}

		awsAccountID, err := sqsutil.AccountIDFromQueueURL(queueURL)
		if err != nil {
			err = twirp.NewError(twirp.InvalidArgument, "could not infer account ID from queue URL")
			base.LogSubscriptionTargetCreate(ctx, err, nil)
			return nil, err
		}
		assumeRoleARN := arn.AssumeRoleARN(awsAccountID, environment.Environment())
		target.AssumeRoleARN = assumeRoleARN

		if err := validator.Validate(sqsDetails.QueueUrl, isValidQueueURL); err != nil {
			err = twirp.NewError(twirp.InvalidArgument, "queue URL is not a valid URL")
			base.LogSubscriptionTargetCreate(ctx, err, nil)
			return nil, err
		}

		if err := ts.QueueValidator.ValidateQueue(ctx, sqsDetails.QueueUrl); err != nil {
			err = twirp.NewError(twirp.InvalidArgument, "queue failed validation: "+err.Error())
			base.LogSubscriptionTargetCreate(ctx, err, nil)
			return nil, err
		}

		attrs, err := ts.SQSManager.GetQueueAttributes(ctx, sqsDetails.QueueUrl)
		if err != nil {
			return nil, errors.Wrap(err, "unable to get queue attributes")
		}

		target.SQSDetails.SQSQueueURL = sqsDetails.QueueUrl
		target.SQSDetails.SQSQueueARN = attrs["QueueArn"]
		target.Status = Created.DBString()
	} else {
		err = errors.New("unsupported target type")
		base.LogSubscriptionTargetCreate(ctx, err, nil)
		return nil, err
	}

	id, err := ts.DB.SubscriptionTargetCreate(ctx, target)
	if err != nil {
		err = errors.Wrap(err, "error creating a target in postgres")
		base.LogSubscriptionTargetCreate(ctx, err, nil)
		return nil, twirperr.Convert(err)
	}

	target.ID = id
	t := targetToRPC(target)
	base.LogSubscriptionTargetCreate(ctx, nil, target)

	return &rpc.CreateTargetResp{
		Target: t,
	}, nil
}

func (ts *TargetsService) Delete(ctx context.Context, req *rpc.DeleteTargetReq) (*rpc.DeleteTargetResp, error) {
	subscriptionTargetID, err := strconv.Atoi(req.GetSubscriptionTargetId())
	if err != nil {
		return nil, twirp.NewError(twirp.InvalidArgument, "could not convert subscription target id to int")
	}

	target, err := ts.DB.SubscriptionTargetByID(ctx, subscriptionTargetID)
	if err != nil {
		return nil, twirperr.Convert(errors.Wrap(err, "could not fetch subscription target"))
	}

	base := &auditlog.BaseLog{
		ResourceName: target.Name,
		ServiceID:    target.ServiceID,
		DB:           ts.DB,
	}

	subs, err := ts.DB.SubscriptionsBySubscriptionTargetID(ctx, subscriptionTargetID)
	if err != nil {
		err = errors.Wrapf(err, "could not get subscriptions for subscription target with id %d", subscriptionTargetID)
		base.LogSubscriptionTargetDelete(ctx, err)
		return nil, err
	}

	if hasActiveSubscription(subs) {
		err = twirp.NewError(twirp.FailedPrecondition, "cannot delete a subscription target with active subscriptions")
		base.LogSubscriptionTargetDelete(ctx, err)
		return nil, err
	}

	// Remove any hanging subscriptions
	for _, sub := range subs {
		lease, ctx, err := ts.DB.SubscriptionAcquireLease(ctx, sub.ID, 30*time.Second)
		if err != nil {
			return nil, errors.Wrap(err, "could not delete subscription for target")
		}

		err = ts.DB.SubscriptionDelete(ctx, lease, sub.ID)
		if err != nil {
			return nil, errors.Wrap(err, "could not delete subscription target")
		}

		err = ts.DB.SubscriptionReleaseLease(lease)
		if err != nil {
			logger.FromContext(ctx).Warn("could not release subscription lease", zap.Int("targetID", target.ID), zap.Int("subscriptionID", sub.ID))
		}

	}

	targetLease, ctx, err := ts.DB.SubscriptionTargetAcquireLease(ctx, subscriptionTargetID, 10*time.Second)
	if err != nil {
		err = errors.Wrapf(err, "could not acquire lease for deleting subscription target %d", subscriptionTargetID)
		base.LogSubscriptionTargetDelete(ctx, err)
		return nil, err
	}

	log := logger.FromContext(ctx)
	defer func() {
		if err := ts.DB.SubscriptionTargetReleaseLease(targetLease); err != nil {
			log.Error("could not release subscription target lease", zap.Error(err))
		}
	}()

	err = ts.DB.SubscriptionTargetDelete(ctx, targetLease, subscriptionTargetID)
	if err != nil {
		err = errors.Wrap(err, "could not delete subscription target")
		base.LogSubscriptionTargetDelete(ctx, err)
		return nil, err
	}

	base.LogSubscriptionTargetDelete(ctx, nil)
	return &rpc.DeleteTargetResp{
		SubscriptionTargetId: req.GetSubscriptionTargetId(),
	}, nil
}

func (ts *TargetsService) Validate(ctx context.Context, req *rpc.ValidateTargetReq) (*rpc.ValidateTargetResp, error) {
	target, err := ts.DB.SubscriptionTargetByQueueURL(ctx, req.SqsQueueUrl)
	if err != nil {
		return nil, twirperr.Convert(errors.Wrap(err, "unable to get subscription target"))
	}

	service, err := ts.DB.ServiceByID(ctx, target.ServiceID)
	if err != nil {
		return nil, twirperr.Convert(errors.Wrap(err, "unable to get service for subscription target"))
	}

	if !ldap.BelongsToGroup(ctx, service.LDAPGroup) {
		return nil, twirp.NewError(twirp.PermissionDenied, "access denied")
	}

	err = ts.QueueValidator.ValidateQueue(ctx, target.SQSDetails.SQSQueueURL)
	if err != nil {
		return &rpc.ValidateTargetResp{
			IsValid: false,
			Message: err.Error(),
		}, nil
	}

	return &rpc.ValidateTargetResp{
		IsValid: true,
	}, nil
}

func isValidTargetName(name string) bool {
	return len(name) > TargetNameMinLength &&
		len(name) < TargetNameMaxLength &&
		validator.IsValidVisibleASCII(name)
}

func isValidTargetRoleARN(arn string) (bool, error) {
	matched, err := regexp.MatchString(TargetIAMRoleARNRegex, arn)
	if err != nil {
		return false, err
	}

	return matched, nil
}

func isValidQueueURL(queueURL string) bool {
	_, err := url.Parse(queueURL)
	return (err == nil)
}

func hasActiveSubscription(subs []*db.Subscription) bool {
	for _, sub := range subs {
		if sub.SNSSubscriptionARN != "" {
			return true
		}
	}

	return false
}
