package logger

import (
	"os"
	"strings"
	"time"

	"github.com/everything-on-mysql/cassandra-on-mysql/internal/config"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
)

// Logger wraps zap.Logger with optional query redaction.
//
// The embedded *zap.Logger is the "base" logger: its caller annotation points at
// whoever calls it directly, so it is correct for code that uses GetZapLogger()
// and logs on the raw zap.Logger. This type's own wrapper methods (Debug/Info/
// ...) add one stack frame, so they log through `wrapped`, which is the base
// logger plus AddCallerSkip(1). Keeping the skip out of the base and only on
// `wrapped` makes file:line accurate for both usage patterns and in both
// development and production modes.
type Logger struct {
	*zap.Logger
	wrapped  *zap.Logger
	redactor *Redactor
}

// newLogger wires a base zap.Logger together with the skip-adjusted variant used
// by Logger's wrapper methods.
func newLogger(base *zap.Logger, redactor *Redactor) *Logger {
	return &Logger{
		Logger:   base,
		wrapped:  base.WithOptions(zap.AddCallerSkip(1)),
		redactor: redactor,
	}
}

// NewLogger creates a new logger based on configuration
func NewLogger(cfg *config.LoggingConfig) (*Logger, error) {
	var zapLogger *zap.Logger
	var err error

	if cfg.Development {
		zapLogger, err = newDevelopmentLogger(cfg)
	} else {
		zapLogger, err = newProductionLogger(cfg)
	}

	if err != nil {
		return nil, err
	}

	redactor := NewRedactor(cfg.RedactQueriesEnabled(), cfg.RedactPatterns)

	return newLogger(zapLogger, redactor), nil
}

// newDevelopmentLogger creates a pretty console logger for development
func newDevelopmentLogger(cfg *config.LoggingConfig) (*zap.Logger, error) {
	level := parseLogLevel(cfg.Level)

	encoderConfig := zap.NewDevelopmentEncoderConfig()
	encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
	encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
	encoderConfig.StacktraceKey = ""

	var writeSyncer zapcore.WriteSyncer
	if cfg.Output == "file" && cfg.FilePath != "" {
		file, err := os.OpenFile(cfg.FilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
		if err != nil {
			return nil, err
		}
		writeSyncer = zapcore.AddSync(file)
	} else {
		writeSyncer = zapcore.AddSync(os.Stdout)
	}

	core := zapcore.NewCore(
		zapcore.NewConsoleEncoder(encoderConfig),
		writeSyncer,
		level,
	)
	if cfg.Sampling.Enabled {
		initial, thereafter := samplingValues(cfg.Sampling)
		core = zapcore.NewSampler(core, time.Second, initial, thereafter)
	}

	// No AddCallerSkip here: the base logger must report its direct callers
	// accurately. The wrapper-method skip is applied in newLogger via `wrapped`.
	return zap.New(core, zap.AddCaller()), nil
}

// newProductionLogger creates a JSON logger for production
func newProductionLogger(cfg *config.LoggingConfig) (*zap.Logger, error) {
	level := parseLogLevel(cfg.Level)

	config := zap.NewProductionConfig()
	config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
	config.EncoderConfig.StacktraceKey = ""
	config.Level = zap.NewAtomicLevelAt(level)
	if cfg.Sampling.Enabled {
		initial, thereafter := samplingValues(cfg.Sampling)
		config.Sampling = &zap.SamplingConfig{
			Initial:    initial,
			Thereafter: thereafter,
		}
	}

	if cfg.Output == "file" && cfg.FilePath != "" {
		config.OutputPaths = []string{cfg.FilePath}
	} else {
		config.OutputPaths = []string{"stdout"}
	}

	return config.Build()
}

// parseLogLevel converts string level to zapcore.Level
func parseLogLevel(level string) zapcore.Level {
	switch strings.ToLower(level) {
	case "debug":
		return zapcore.DebugLevel
	case "info":
		return zapcore.InfoLevel
	case "warn", "warning":
		return zapcore.WarnLevel
	case "error":
		return zapcore.ErrorLevel
	case "fatal":
		return zapcore.FatalLevel
	default:
		return zapcore.InfoLevel
	}
}

func samplingValues(cfg config.LoggingSamplingConfig) (int, int) {
	initial := cfg.Initial
	thereafter := cfg.Thereafter
	if initial <= 0 {
		initial = 100
	}
	if thereafter <= 0 {
		thereafter = 100
	}
	return initial, thereafter
}

// Debug logs a debug message with optional query redaction
func (l *Logger) Debug(msg string, fields ...zap.Field) {
	l.wrapped.Debug(l.redactMessage(msg, fields), fields...)
}

// Info logs an info message with optional query redaction
func (l *Logger) Info(msg string, fields ...zap.Field) {
	l.wrapped.Info(l.redactMessage(msg, fields), fields...)
}

// Warn logs a warn message with optional query redaction
func (l *Logger) Warn(msg string, fields ...zap.Field) {
	l.wrapped.Warn(l.redactMessage(msg, fields), fields...)
}

// Error logs an error message with optional query redaction
func (l *Logger) Error(msg string, fields ...zap.Field) {
	l.wrapped.Error(l.redactMessage(msg, fields), fields...)
}

// Fatal logs a fatal message with optional query redaction
func (l *Logger) Fatal(msg string, fields ...zap.Field) {
	l.wrapped.Fatal(l.redactMessage(msg, fields), fields...)
}

// redactMessage redacts sensitive data in message and field values
func (l *Logger) redactMessage(msg string, fields []zap.Field) string {
	// Redact the message itself
	redactedMsg := l.redactor.RedactQuery(msg)

	// Redact specific field values that might contain queries
	for i, field := range fields {
		if field.Key == "query" || field.Key == "cql" || field.Key == "sql" {
			// For string-type fields, the value is in field.String
			if field.Type == zapcore.StringType {
				fields[i] = zap.String(field.Key, l.redactor.RedactQuery(field.String))
			}
		}
	}

	return redactedMsg
}

// Sugar returns a sugared logger with redaction support. RedactedSugaredLogger's
// methods add one frame, so it uses the skip-adjusted logger.
func (l *Logger) Sugar() *RedactedSugaredLogger {
	return &RedactedSugaredLogger{
		sugar:    l.wrapped.Sugar(),
		redactor: l.redactor,
	}
}

// With creates a child logger with context
func (l *Logger) With(fields ...zap.Field) *Logger {
	return newLogger(l.Logger.With(fields...), l.redactor)
}

// Named creates a child logger with a name
func (l *Logger) Named(name string) *Logger {
	return newLogger(l.Logger.Named(name), l.redactor)
}

// GetZapLogger returns the underlying zap.Logger for compatibility
func (l *Logger) GetZapLogger() *zap.Logger {
	return l.Logger
}

// RedactedSugaredLogger wraps zap.SugaredLogger with query redaction
type RedactedSugaredLogger struct {
	sugar    *zap.SugaredLogger
	redactor *Redactor
}

// Debug logs a debug message
func (l *RedactedSugaredLogger) Debug(args ...interface{}) {
	l.sugar.Debug(l.redactArgs(args)...)
}

// Info logs an info message
func (l *RedactedSugaredLogger) Info(args ...interface{}) {
	l.sugar.Info(l.redactArgs(args)...)
}

// Warn logs a warn message
func (l *RedactedSugaredLogger) Warn(args ...interface{}) {
	l.sugar.Warn(l.redactArgs(args)...)
}

// Error logs an error message
func (l *RedactedSugaredLogger) Error(args ...interface{}) {
	l.sugar.Error(l.redactArgs(args)...)
}

// Fatal logs a fatal message
func (l *RedactedSugaredLogger) Fatal(args ...interface{}) {
	l.sugar.Fatal(l.redactArgs(args)...)
}

// Debugf logs a formatted debug message
func (l *RedactedSugaredLogger) Debugf(template string, args ...interface{}) {
	l.sugar.Debugf(template, l.redactArgs(args)...)
}

// Infof logs a formatted info message
func (l *RedactedSugaredLogger) Infof(template string, args ...interface{}) {
	l.sugar.Infof(template, l.redactArgs(args)...)
}

// Warnf logs a formatted warn message
func (l *RedactedSugaredLogger) Warnf(template string, args ...interface{}) {
	l.sugar.Warnf(template, l.redactArgs(args)...)
}

// Errorf logs a formatted error message
func (l *RedactedSugaredLogger) Errorf(template string, args ...interface{}) {
	l.sugar.Errorf(template, l.redactArgs(args)...)
}

// Fatalf logs a formatted fatal message
func (l *RedactedSugaredLogger) Fatalf(template string, args ...interface{}) {
	l.sugar.Fatalf(template, l.redactArgs(args)...)
}

// With adds context
func (l *RedactedSugaredLogger) With(args ...interface{}) *RedactedSugaredLogger {
	return &RedactedSugaredLogger{
		sugar:    l.sugar.With(args...),
		redactor: l.redactor,
	}
}

// redactArgs redacts sensitive data in arguments
func (l *RedactedSugaredLogger) redactArgs(args []interface{}) []interface{} {
	if !l.redactor.enabled {
		return args
	}

	redacted := make([]interface{}, len(args))
	for i, arg := range args {
		if str, ok := arg.(string); ok {
			redacted[i] = l.redactor.RedactQuery(str)
		} else {
			redacted[i] = arg
		}
	}
	return redacted
}
