package logging

import (
	"io"
	"log"
	"os"

	"github.com/UpmeshLTD/urus-aio/infrastructure/logging/cloudwatchwriter"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
	"github.com/aws/aws-sdk-go/service/sts"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
)

type zapLogger struct {
	zap *zap.SugaredLogger
}

// NewZapLogger returns a new instance of Logger that uses Zap & Stack Driver.
func NewZapLogger(cfg LoggerConfig) Logger {
	var cWriter io.Writer

	encoderConfig := zap.NewProductionEncoderConfig()
	encoderConfig.TimeKey = "time"
	encoderConfig.MessageKey = "message"
	encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
	encoderConfig.EncodeCaller = zapcore.FullCallerEncoder
	encoderConfig.EncodeTime = zapcore.EpochTimeEncoder

	level, err := zap.ParseAtomicLevel(cfg.Level)
	if err != nil {
		level = zap.NewAtomicLevelAt(zapcore.InfoLevel)
	}

	consoleCore := func() zapcore.Core {
		encoder := zapcore.NewJSONEncoder(encoderConfig)
		if cfg.Colorized {
			consoleEncoderConfig := encoderConfig
			consoleEncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
			consoleEncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
			encoder = zapcore.NewConsoleEncoder(consoleEncoderConfig)
		}
		return zapcore.NewCore(encoder, zapcore.Lock(os.Stdout), level)
	}()

	coreLoggers := []zapcore.Core{consoleCore}

	if cfg.CloudWatchLog != nil {
		func() {
			log.Println("initialize CloudWatchWriter")

			if cfg.CloudWatchLog.Region == "" || cfg.CloudWatchLog.GroupName == "" || cfg.CloudWatchLog.StreamName == "" {
				log.Println("failed to initialize CloudWatchWriter: missing configs Region, GroupName or StreamName")
				return
			}

			// init cloudwatch
			sess, err := session.NewSession(&aws.Config{
				Region: aws.String(cfg.CloudWatchLog.Region),
			})
			if err != nil {
				log.Println("failed to create a session:", err)
				return
			}

			// check credentials
			if _, err := sts.New(sess).GetCallerIdentity(&sts.GetCallerIdentityInput{}); err != nil {
				log.Println("failed to validate aws credentials:", err)
				return
			}

			cWriter, err = cloudwatchwriter.New(
				cloudwatchlogs.New(sess),
				cfg.CloudWatchLog.BatchFrequency,
				cfg.CloudWatchLog.GroupName,
				cfg.CloudWatchLog.StreamName,
			)
			if err != nil {
				log.Println("failed to initialize CloudWatchWriter:", err)
				return
			}

			log.Println("successfully initialize CloudWatchWriter")

			// Reference
			// https://github.com/vmlellis/zapcloudwatchcore
			// https://github.com/bahadirbb/zapcloudwatch
			cloudWatchCore := func() zapcore.Core {
				encoder := zapcore.NewJSONEncoder(encoderConfig)
				return zapcore.NewCore(encoder, zapcore.Lock(zapcore.AddSync(cWriter)), level)
			}()

			coreLoggers = append(coreLoggers, cloudWatchCore)
		}()
	}

	// init logger with options
	logger := zap.New(zapcore.NewTee(coreLoggers...), zap.AddCaller(), zap.AddCallerSkip(1))

	if cWriter != nil {
		// set std log output
		log.SetOutput(io.MultiWriter(os.Stdout, cWriter))

		// set cloudwatch to logging writer
		writer = &cWriter
	}

	log.Println("successfully initialize ZapLogger")

	return &zapLogger{
		zap: logger.Sugar(),
	}
}

func (l *zapLogger) Print(args ...interface{}) {
	l.Info(args...)
}

func (l *zapLogger) Println(args ...interface{}) {
	l.Info(args...)
}

func (l *zapLogger) Printf(format string, args ...interface{}) {
	l.Infof(format, args...)
}

func (l *zapLogger) Error(args ...interface{}) {
	l.zap.Error(args...)
}

func (l *zapLogger) Warn(args ...interface{}) {
	l.zap.Warn(args...)
}

func (l *zapLogger) Info(args ...interface{}) {
	l.zap.Info(args...)
}

func (l *zapLogger) Debug(args ...interface{}) {
	l.zap.Debug(args...)
}

func (l *zapLogger) Errorf(format string, args ...interface{}) {
	l.zap.Errorf(format, args...)
}

func (l *zapLogger) Warnf(format string, args ...interface{}) {
	l.zap.Warnf(format, args...)
}

func (l *zapLogger) Infof(format string, args ...interface{}) {
	l.zap.Infof(format, args...)
}

func (l *zapLogger) Debugf(format string, args ...interface{}) {
	l.zap.Debugf(format, args...)
}

func (l *zapLogger) Errorw(msg string, args ...interface{}) {
	l.zap.Errorw(msg, args...)
}

func (l *zapLogger) Warnw(msg string, args ...interface{}) {
	l.zap.Warnw(msg, args...)
}

func (l *zapLogger) Infow(msg string, args ...interface{}) {
	l.zap.Infow(msg, args...)
}

func (l *zapLogger) Debugw(msg string, args ...interface{}) {
	l.zap.Debugw(msg, args...)
}

// With returns a new logger with given arguments.
func (l *zapLogger) With(args ...interface{}) Logger {
	if len(args) == 1 {
		switch field := args[0].(type) {
		case map[string]interface{}:
			fields := make([]interface{}, 0)
			for k, v := range field {
				fields = append(fields, k, v)
			}
			return &zapLogger{
				zap: l.zap.With(fields...),
			}
		}
	}
	return &zapLogger{
		zap: l.zap.With(args...),
	}
}
