package logging

import (
	"io"
	"log"
	"os"
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
	"github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface"

	"github.com/segmentio/ksuid"
)

// NewCloudWatchLogsProvider creates a io.Writer that writes
// to a specifc log group.
//
// Each time NewCloudWatchLogsProvider is used, a new log stream is created
// inside the log group. The log stream will have a unique, random identifer
//
//	sess := session.Must(aws.NewConfig())
//	svc := cloudwatchlogs.New(sess)
//
//	provider, err := NewCloudWatchLogsProvider(svc, "pineapple-pizza")
//	if err != nil {
//		panic(err)
//	}
//
//	// set log output to the provider, all log messages will then be
//	// pushed through the Write func and sent to CloudWatch Logs
//	log.SetOutput(provider)
//	log.Printf("Eric loves pineapple pizza!")
func NewCloudWatchLogsProvider(client cloudwatchlogsiface.CloudWatchLogsAPI, logGroupName string) (io.Writer, error) {
	logger := New("internal: ")

	// If we're running in SAM CLI, we can return the stdout
	if len(os.Getenv("AWS_SAM_LOCAL")) > 0 && len(os.Getenv("AWS_FORCE_INTEGRATIONS")) == 0 {
		return stdErr, nil
	}

	ok, err := CloudWatchLogGroupExists(client, logGroupName)
	if err != nil {
		return nil, err
	}

	if !ok {
		logger.Printf("Need to create loggroup: %v", logGroupName)
		if err := CreateNewCloudWatchLogGroup(client, logGroupName); err != nil {
			return nil, err
		}
	}

	logStreamName := ksuid.New()
	// need to create logstream
	if err := CreateNewLogStream(client, logGroupName, logStreamName.String()); err != nil {
		return nil, err
	}

	provider := &cloudWatchLogsProvider{
		client: client,

		logGroupName:  logGroupName,
		logStreamName: logStreamName.String(),

		logger: logger,
	}

	if _, err := provider.Write([]byte("Initialization of log stream")); err != nil {
		return nil, err
	}

	return provider, nil
}

type cloudWatchLogsProvider struct {
	client cloudwatchlogsiface.CloudWatchLogsAPI

	logGroupName  string
	logStreamName string

	sequence string

	logger *log.Logger
}

func (p *cloudWatchLogsProvider) Write(b []byte) (int, error) {
	p.logger.Printf("Need to write: %v", string(b))

	input := &cloudwatchlogs.PutLogEventsInput{
		LogGroupName:  aws.String(p.logGroupName),
		LogStreamName: aws.String(p.logStreamName),

		LogEvents: []*cloudwatchlogs.InputLogEvent{
			{
				Message:   aws.String(string(b)),
				Timestamp: aws.Int64(time.Now().UnixNano() / int64(time.Millisecond)),
			},
		},
	}

	if len(p.sequence) != 0 {
		input.SetSequenceToken(p.sequence)
	}

	resp, err := p.client.PutLogEvents(input)

	if err != nil {
		return 0, err
	}

	p.sequence = *resp.NextSequenceToken

	return len(b), nil
}

// CloudWatchLogGroupExists checks if a log group exists
//
// Using the client provided, it will check the CloudWatch Logs
// service to verify the log group
//
//	sess := session.Must(aws.NewConfig())
//	svc := cloudwatchlogs.New(sess)
//
//	// checks if the pineapple-pizza log group exists
//	ok, err := LogGroupExists(svc, "pineapple-pizza")
//	if err != nil {
//		panic(err)
//	}
//	if ok {
//		// do something
//	}
func CloudWatchLogGroupExists(client cloudwatchlogsiface.CloudWatchLogsAPI, logGroupName string) (bool, error) {
	resp, err := client.DescribeLogGroups(&cloudwatchlogs.DescribeLogGroupsInput{
		Limit:              aws.Int64(1),
		LogGroupNamePrefix: aws.String(logGroupName),
	})

	if err != nil {
		return false, err
	}

	if len(resp.LogGroups) == 0 || *resp.LogGroups[0].LogGroupName != logGroupName {
		return false, nil
	}

	return true, nil
}

// CreateNewCloudWatchLogGroup creates a log group in CloudWatch Logs.
//
// Using a passed in client to create the call to the service, it
// will create a log group of the specified name
//
//	sess := session.Must(aws.NewConfig())
//	svc := cloudwatchlogs.New(sess)
//
//	if err := CreateNewCloudWatchLogGroup(svc, "pineapple-pizza"); err != nil {
//		panic("Unable to create log group", err)
//	}
func CreateNewCloudWatchLogGroup(client cloudwatchlogsiface.CloudWatchLogsAPI, logGroupName string) error {
	if _, err := client.CreateLogGroup(&cloudwatchlogs.CreateLogGroupInput{
		LogGroupName: aws.String(logGroupName),
	}); err != nil {
		return err
	}

	return nil
}

// CreateNewLogStream creates a log stream inside of a LogGroup
func CreateNewLogStream(client cloudwatchlogsiface.CloudWatchLogsAPI, logGroupName string, logStreamName string) error {
	_, err := client.CreateLogStream(&cloudwatchlogs.CreateLogStreamInput{
		LogGroupName:  aws.String(logGroupName),
		LogStreamName: aws.String(logStreamName),
	})

	return err
}
