package validation

import (
	"context"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"strconv"

	"code.justin.tv/eventbus/controlplane/infrastructure/routing"
	"code.justin.tv/eventbus/controlplane/internal/environment"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/sns"

	"github.com/aws/aws-sdk-go/aws/awserr"
	"github.com/aws/aws-sdk-go/aws/client"
	"github.com/aws/aws-sdk-go/service/sns/snsiface"
	"github.com/pkg/errors"

	"code.justin.tv/eventbus/controlplane/internal/db"
)

const ErrSNSTopicNotFound = "sns topic not found"
const ErrS3RouteNotFound = "s3 route not found"
const ErrS3RouteJSONMalformed = "s3 route json malformed"
const ErrS3RouteARNIncorrect = "s3 route arn incorrect"

type EventStream struct {
	*db.EventStream
	snsClient snsiface.SNSAPI
}

func (e *EventStream) ID() string {
	return itemID(e)
}

func (e *EventStream) Type() string {
	return "EventStream"
}

func (e *EventStream) Attributes() []*ItemAttribute {
	return []*ItemAttribute{
		{
			Key:   "EventType",
			Value: e.EventType.Name,
		},
		{
			Key:   "Environment",
			Value: e.Environment,
		},
		{
			Key:   "ARN",
			Value: e.SNSDetails.SNSTopicARN,
		},
	}
}

func (e *EventStream) Validate(ctx context.Context) (*Report, error) {
	// decide on a severity based on the affected infrastructure
	reportWithSeverity := func(item Item, msg string) (*Report, error) {
		if e.EventStream.Environment == environment.Production {
			return Error(item, msg), nil
		}
		return Warn(item, msg), nil
	}

	_, err := e.snsClient.GetTopicAttributesWithContext(ctx, &sns.GetTopicAttributesInput{
		TopicArn: aws.String(e.EventStream.SNSDetails.SNSTopicARN),
	})
	if err != nil {
		if aerr, ok := err.(awserr.Error); ok {
			if aerr.Code() == sns.ErrCodeNotFoundException {
				return reportWithSeverity(e, ErrSNSTopicNotFound)
			}
			return nil, errors.Wrap(err, "could not get topic attributes")
		}
	}

	urlBase := routing.S3BucketName(environment.Environment())
	if urlBase == "" {
		return nil, errors.New("could not fetch s3 route for event stream: environment unknown")
	}

	urlPath := routing.ConfigPath(e.EventStream.EventType.Name, e.EventStream.Environment)
	urlFull := fmt.Sprintf("https://s3-us-west-2.amazonaws.com/%s%s", urlBase, urlPath)
	resp, err := http.Get(urlFull)
	if err != nil {
		return nil, errors.Wrap(err, "could not make s3 route request")
	}

	// NOTE: annoyingly, when an S3 allows public HTTP(S) reads on a bucket, and a request is made against
	// the bucket for an object that does not exist, a 403 is returned instead of a 404. Therefore, this
	// conditional checks for either.
	if resp.StatusCode == 404 || resp.StatusCode == 403 {
		return reportWithSeverity(e, ErrS3RouteNotFound)
	} else if resp.StatusCode != 200 {
		return nil, errors.New("unexpected status code from s3 route fetch: " + strconv.Itoa(resp.StatusCode))
	}

	b, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, errors.Wrap(err, "could not read s3 route response")
	}

	var config routing.Config
	err = json.Unmarshal(b, &config)
	if err != nil {
		return reportWithSeverity(e, ErrS3RouteJSONMalformed)
	} else if config.Arn != e.EventStream.SNSDetails.SNSTopicARN {
		return reportWithSeverity(e, ErrS3RouteARNIncorrect)
	}

	return Ok(e), nil
}

func EventStreams(ctx context.Context, sess client.ConfigProvider, db db.DB) ([]Item, error) {
	eventStreams, err := db.EventStreams(ctx)
	if err != nil {
		return nil, errors.Wrap(err, "could not get event streams")
	}

	snsClient := sns.New(sess)
	items := make([]Item, 0)
	for _, eventStream := range eventStreams {
		items = append(items, &EventStream{
			EventStream: eventStream,
			snsClient:   snsClient,
		})
	}

	return items, nil
}
