package logging

import (
	"context"
	"fmt"
	"log/slog"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"sync"
	"time"

	"github.com/go-kratos/kratos/contrib/otel/v3/tracing"
	kratoslog "github.com/go-kratos/kratos/v3/log"
	"go.uber.org/zap"
	"go.uber.org/zap/exp/zapslog"
	"go.uber.org/zap/zapcore"
)

type Options struct {
	Level, Format, EncodeLevel, Prefix, StacktraceKey string
	LogInConsole, ShowLine                            bool
	RetentionDay                                      int
	FileOnlyModules                                   []string
}

// ErrorEntry is the storage-neutral representation of an Error-level log.
// Keeping it in pkg/logging lets the log core report failures without taking a
// dependency on the application service or persistence layers.
type ErrorEntry struct {
	Form, Info, Level, RequestID, TraceID string
}

// ErrorSink receives Error-level log entries. The sink must not log failures
// through the same logger, otherwise a storage failure could recurse forever.
type ErrorSink interface {
	RecordLogError(context.Context, ErrorEntry) error
}

type ErrorSinkFunc func(context.Context, ErrorEntry) error

func (f ErrorSinkFunc) RecordLogError(ctx context.Context, entry ErrorEntry) error {
	return f(ctx, entry)
}

type errorSinkState struct {
	mu   sync.RWMutex
	sink ErrorSink
}

func (s *errorSinkState) record(entry ErrorEntry) {
	if s == nil {
		return
	}
	s.mu.RLock()
	sink := s.sink
	s.mu.RUnlock()
	if sink != nil {
		_ = sink.RecordLogError(context.Background(), entry)
	}
}

type handlerOperation struct {
	attrs []slog.Attr
	group string
}

type reloadableHandlerState struct {
	mu      sync.RWMutex
	handler slog.Handler
	cleanup func()
}

type reloadableHandler struct {
	state *reloadableHandlerState
	ops   []handlerOperation
}

func (h *reloadableHandler) resolved() slog.Handler {
	current := h.state.handler
	for _, operation := range h.ops {
		if operation.group != "" {
			current = current.WithGroup(operation.group)
		} else {
			current = current.WithAttrs(operation.attrs)
		}
	}
	return current
}

func (h *reloadableHandler) Enabled(ctx context.Context, level slog.Level) bool {
	h.state.mu.RLock()
	defer h.state.mu.RUnlock()
	return h.resolved().Enabled(ctx, level)
}

func (h *reloadableHandler) Handle(ctx context.Context, record slog.Record) error {
	h.state.mu.RLock()
	defer h.state.mu.RUnlock()
	return h.resolved().Handle(ctx, record)
}

func (h *reloadableHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
	next := append([]handlerOperation(nil), h.ops...)
	next = append(next, handlerOperation{attrs: append([]slog.Attr(nil), attrs...)})
	return &reloadableHandler{state: h.state, ops: next}
}

func (h *reloadableHandler) WithGroup(name string) slog.Handler {
	next := append([]handlerOperation(nil), h.ops...)
	next = append(next, handlerOperation{group: name})
	return &reloadableHandler{state: h.state, ops: next}
}

type ReloadableLogger struct {
	state     *reloadableHandlerState
	filename  string
	errorSink *errorSinkState
}

func (l *ReloadableLogger) Reload(root string, options Options) {
	handler, cleanup := newZapHandler(root, l.filename, options, l.errorSink)
	l.state.mu.Lock()
	previous := l.state.cleanup
	l.state.handler = handler
	l.state.cleanup = cleanup
	l.state.mu.Unlock()
	if previous != nil {
		previous()
	}
}

// SetErrorSink changes the database/audit target while keeping the current
// logger and its hot-reload state intact.
func (l *ReloadableLogger) SetErrorSink(sink ErrorSink) {
	if l == nil || l.errorSink == nil {
		return
	}
	l.errorSink.mu.Lock()
	l.errorSink.sink = sink
	l.errorSink.mu.Unlock()
}

func (l *ReloadableLogger) Close() {
	l.state.mu.Lock()
	cleanup := l.state.cleanup
	l.state.cleanup = nil
	l.state.mu.Unlock()
	if cleanup != nil {
		cleanup()
	}
}

type consoleSummaryCore struct {
	level    zapcore.LevelEnabler
	output   zapcore.WriteSyncer
	prefix   string
	fileOnly map[string]struct{}
	fields   []zapcore.Field
}

func (c *consoleSummaryCore) Enabled(level zapcore.Level) bool { return c.level.Enabled(level) }

func (c *consoleSummaryCore) With(fields []zapcore.Field) zapcore.Core {
	inherited := append([]zapcore.Field(nil), c.fields...)
	inherited = append(inherited, fields...)
	return &consoleSummaryCore{level: c.level, output: c.output, prefix: c.prefix, fileOnly: c.fileOnly, fields: inherited}
}

func (c *consoleSummaryCore) Check(entry zapcore.Entry, checked *zapcore.CheckedEntry) *zapcore.CheckedEntry {
	if c.Enabled(entry.Level) {
		return checked.AddCore(entry, c)
	}
	return checked
}

func (c *consoleSummaryCore) Write(entry zapcore.Entry, fields []zapcore.Field) error {
	allFields := append([]zapcore.Field(nil), c.fields...)
	allFields = append(allFields, fields...)
	module := moduleField(allFields)
	if _, excluded := c.fileOnly[module]; excluded {
		return nil
	}
	_, err := c.output.Write([]byte(formatConsoleEntry(entry, module, allFields, c.prefix)))
	return err
}

func (c *consoleSummaryCore) Sync() error { return c.output.Sync() }

var hiddenConsoleFields = map[string]struct{}{
	"mod": {}, "service.id": {}, "service.name": {}, "service.version": {},
	"node": {}, "app_id": {}, "env": {}, "trace_id": {}, "span_id": {},
}

func formatConsoleEntry(entry zapcore.Entry, module string, fields []zapcore.Field, prefix string) string {
	values := zapcore.NewMapObjectEncoder()
	for _, field := range fields {
		field.AddTo(values)
	}
	keys := make([]string, 0, len(values.Fields))
	for key, value := range values.Fields {
		if _, hidden := hiddenConsoleFields[key]; hidden || consoleValueEmpty(value) {
			continue
		}
		keys = append(keys, key)
	}
	sort.Strings(keys)

	caller := ""
	if entry.Caller.Defined {
		caller = filepath.Base(entry.Caller.File) + fmt.Sprintf(":%d", entry.Caller.Line)
	}
	if module == "" {
		module = "app"
	}
	parts := []string{
		prefix + entry.Time.Format("15:04:05.000"),
		consoleLevel(entry.Level),
		fmt.Sprintf("%-10s", module),
	}
	if caller != "" {
		parts = append(parts, fmt.Sprintf("%-24s", caller))
	}
	parts = append(parts, entry.Message)
	for _, key := range keys {
		parts = append(parts, key+"="+formatConsoleValue(values.Fields[key]))
	}
	return strings.Join(parts, "  ") + "\n"
}

func consoleLevel(level zapcore.Level) string {
	label := strings.ToUpper(level.String())
	color := "\x1b[36m"
	switch {
	case level >= zapcore.ErrorLevel:
		color = "\x1b[31m"
	case level == zapcore.WarnLevel:
		color = "\x1b[33m"
	case level == zapcore.InfoLevel:
		color = "\x1b[32m"
	}
	return color + fmt.Sprintf("%-5s", label) + "\x1b[0m"
}

func consoleValueEmpty(value any) bool {
	return value == nil || value == ""
}

func formatConsoleValue(value any) string {
	text := fmt.Sprint(value)
	text = strings.Join(strings.Fields(text), " ")
	if len(text) > 180 {
		text = text[:177] + "..."
	}
	if strings.ContainsAny(text, " \t\r\n") {
		return fmt.Sprintf("%q", text)
	}
	return text
}

func moduleField(fields []zapcore.Field) string {
	for _, field := range fields {
		if field.Key == "mod" {
			if field.String != "" {
				return field.String
			}
			if field.Interface != nil {
				return fmt.Sprint(field.Interface)
			}
		}
	}
	return ""
}

type routedFileState struct {
	mu      sync.Mutex
	writers map[string]*DailyWriter
}

type routedFileCore struct {
	base         zapcore.Core
	encoder      zapcore.Encoder
	level        zapcore.LevelEnabler
	root         string
	retentionDay int
	state        *routedFileState
	fields       []zapcore.Field
	errorSink    *errorSinkState
}

func (c *routedFileCore) Enabled(level zapcore.Level) bool { return c.level.Enabled(level) }

func (c *routedFileCore) With(fields []zapcore.Field) zapcore.Core {
	inherited := append([]zapcore.Field(nil), c.fields...)
	inherited = append(inherited, fields...)
	return &routedFileCore{base: c.base.With(fields), encoder: c.encoder, level: c.level, root: c.root, retentionDay: c.retentionDay, state: c.state, fields: inherited, errorSink: c.errorSink}
}

func (c *routedFileCore) Check(entry zapcore.Entry, checked *zapcore.CheckedEntry) *zapcore.CheckedEntry {
	if c.Enabled(entry.Level) {
		return checked.AddCore(entry, c)
	}
	return checked
}

func (c *routedFileCore) Write(entry zapcore.Entry, fields []zapcore.Field) error {
	baseErr := c.base.Write(entry, fields)
	allFields := append([]zapcore.Field(nil), c.fields...)
	allFields = append(allFields, fields...)
	if entry.Level >= zapcore.ErrorLevel && !isGORMLoggerEntry(entry.Caller.File, allFields) {
		c.errorSink.record(errorEntryFromZap(entry, allFields))
	}
	paths := routedLogPaths(moduleField(allFields), entry.Level)
	if len(paths) == 0 {
		return baseErr
	}
	buffer, err := c.encoder.Clone().EncodeEntry(entry, allFields)
	if err != nil {
		if baseErr != nil {
			return baseErr
		}
		return err
	}
	defer buffer.Free()
	for _, name := range paths {
		writer := c.writer(name)
		if _, writeErr := writer.Write(buffer.Bytes()); writeErr != nil && baseErr == nil {
			baseErr = writeErr
		}
	}
	return baseErr
}

func isGORMLoggerEntry(filename string, fields []zapcore.Field) bool {
	for _, field := range fields {
		if field.Key == "gorm_logger" {
			return true
		}
	}
	normalized := strings.ReplaceAll(filename, "\\", "/")
	return strings.HasSuffix(normalized, "/gorm_logger_writer.go") ||
		strings.HasSuffix(normalized, "/pkg/database/gormkit/logger.go")
}

func errorEntryFromZap(entry zapcore.Entry, fields []zapcore.Field) ErrorEntry {
	requestID, traceID, errorText := "", "", ""
	for _, field := range fields {
		switch field.Key {
		case "request_id":
			if requestID == "" {
				requestID = zapFieldString(field)
			}
		case "trace_id":
			if traceID == "" {
				traceID = zapFieldString(field)
			}
		case "error", "err":
			if errorText == "" {
				errorText = zapFieldString(field)
			}
		}
	}
	info := entry.Message
	if errorText != "" {
		info += " | 错误: " + errorText
	}
	if entry.Caller.File != "" {
		info += fmt.Sprintf(" \n 源文件:%s:%d", entry.Caller.File, entry.Caller.Line)
	}
	if entry.Stack != "" {
		info += " \n 调用栈：" + entry.Stack
		if frame, ok := finalApplicationCaller(entry.Stack); ok {
			functionName, source, startLine, endLine, err := functionSourceAt(frame.File, frame.Line)
			if err == nil {
				info += fmt.Sprintf(" \n 最终调用方法:%s:%d (%s lines %d-%d)\n----- 产生日志的方法代码如下 -----\n%s", frame.File, frame.Line, functionName, startLine, endLine, source)
			} else {
				info += fmt.Sprintf(" \n 最终调用方法:%s:%d (%s) | extract_err=%v", frame.File, frame.Line, functionName, err)
			}
		}
	}
	return ErrorEntry{Form: "后端", Info: info, Level: entry.Level.String(), RequestID: requestID, TraceID: traceID}
}

func zapFieldString(field zapcore.Field) string {
	if field.String != "" {
		return field.String
	}
	if field.Interface != nil {
		return fmt.Sprint(field.Interface)
	}
	return ""
}

func (c *routedFileCore) Sync() error {
	result := c.base.Sync()
	c.state.mu.Lock()
	defer c.state.mu.Unlock()
	for _, writer := range c.state.writers {
		if err := writer.Sync(); err != nil && result == nil {
			result = err
		}
	}
	return result
}

func (c *routedFileCore) writer(name string) *DailyWriter {
	c.state.mu.Lock()
	defer c.state.mu.Unlock()
	writer := c.state.writers[name]
	if writer == nil {
		writer = NewDailyWriter(c.root, name, c.retentionDay)
		c.state.writers[name] = writer
	}
	return writer
}

func (c *routedFileCore) Close() {
	c.state.mu.Lock()
	defer c.state.mu.Unlock()
	for name, writer := range c.state.writers {
		_ = writer.Close()
		delete(c.state.writers, name)
	}
}

func routedLogPaths(module string, level zapcore.Level) []string {
	paths := make([]string, 0, 2)
	if module = safeModuleName(module); module != "" {
		switch module {
		case "http":
			paths = append(paths, filepath.Join("http", "access.log"))
		case "timedTask":
			paths = append(paths, filepath.Join("timedTask", "task.log"))
		case "error":
			paths = append(paths, filepath.Join("error", "error.log"))
		default:
			paths = append(paths, filepath.Join(module, "application.log"))
		}
	}
	if level >= zapcore.ErrorLevel {
		errorPath := filepath.Join("error", "error.log")
		if len(paths) == 0 || paths[len(paths)-1] != errorPath {
			paths = append(paths, errorPath)
		}
	}
	return paths
}

func safeModuleName(value string) string {
	value = strings.TrimSpace(value)
	if value == "" {
		return ""
	}
	return strings.Map(func(r rune) rune {
		if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '-' || r == '_' {
			return r
		}
		return -1
	}, value)
}

// NewZapLogger adapts a Zap core to the slog logger used by Kratos v3.
// The file layout remains compatible with the administration log viewer.
func newZapHandler(root, filename string, options Options, errorSink *errorSinkState) (slog.Handler, func()) {
	file := NewDailyWriter(root, filename, options.RetentionDay)
	encoder := zap.NewProductionEncoderConfig()
	encoder.EncodeTime = zapcore.RFC3339NanoTimeEncoder
	if options.StacktraceKey != "" {
		encoder.StacktraceKey = options.StacktraceKey
	}
	switch options.EncodeLevel {
	case "CapitalLevelEncoder":
		encoder.EncodeLevel = zapcore.CapitalLevelEncoder
	case "CapitalColorLevelEncoder":
		encoder.EncodeLevel = zapcore.CapitalColorLevelEncoder
	case "LowercaseColorLevelEncoder":
		encoder.EncodeLevel = zapcore.LowercaseColorLevelEncoder
	default:
		encoder.EncodeLevel = zapcore.LowercaseLevelEncoder
	}
	var outputEncoder zapcore.Encoder = zapcore.NewJSONEncoder(encoder)
	if options.Format != "" && options.Format != "json" {
		if options.Prefix != "" {
			encoder.EncodeTime = func(value time.Time, output zapcore.PrimitiveArrayEncoder) {
				output.AppendString(options.Prefix + value.Format("2006-01-02 15:04:05.000"))
			}
		}
		outputEncoder = zapcore.NewConsoleEncoder(encoder)
	}
	level := zap.DebugLevel
	// zapcore.Level.Set reports an error but does not make the intended
	// configuration handling obvious here. Parse the configured value directly
	// so debug/warn/error are actually applied after startup and hot reload.
	if value := strings.TrimSpace(strings.ToLower(options.Level)); value != "" {
		if err := level.UnmarshalText([]byte(value)); err != nil {
			level = zap.DebugLevel
		}
	}
	levelEnabler := zap.NewAtomicLevelAt(level)
	fileCore := zapcore.NewCore(
		outputEncoder.Clone(),
		zapcore.AddSync(file),
		levelEnabler,
	)
	core := zapcore.Core(fileCore)
	if options.LogInConsole {
		fileOnly := make(map[string]struct{}, len(options.FileOnlyModules))
		for _, module := range options.FileOnlyModules {
			fileOnly[module] = struct{}{}
		}
		consoleCore := &consoleSummaryCore{level: levelEnabler, output: zapcore.AddSync(os.Stdout), prefix: options.Prefix, fileOnly: fileOnly}
		core = zapcore.NewTee(fileCore, consoleCore)
	}
	routed := &routedFileCore{base: core, encoder: outputEncoder.Clone(), level: levelEnabler, root: root, retentionDay: options.RetentionDay, state: &routedFileState{writers: map[string]*DailyWriter{}}, errorSink: errorSink}
	zapLogger := zap.New(routed)
	handlerOptions := []zapslog.HandlerOption{zapslog.AddStacktraceAt(slog.LevelError)}
	if options.ShowLine {
		handlerOptions = append(handlerOptions, zapslog.WithCaller(true))
	}
	handler := zapslog.NewHandler(zapLogger.Core(), handlerOptions...)
	cleanup := func() {
		_ = zapLogger.Sync()
		routed.Close()
		_ = file.Close()
	}
	return handler, cleanup
}

// NewReloadableZapLogger keeps the slog/Kratos adapter stable while replacing
// the underlying Zap core when the runtime configuration changes.
func NewReloadableZapLogger(root, filename string, options Options, attrs ...any) (*slog.Logger, *ReloadableLogger) {
	errorSink := &errorSinkState{}
	baseHandler, cleanup := newZapHandler(root, filename, options, errorSink)
	state := &reloadableHandlerState{handler: baseHandler, cleanup: cleanup}
	control := &ReloadableLogger{state: state, filename: filename, errorSink: errorSink}
	handler := &contextHandler{handler: &reloadableHandler{state: state}}
	logger := kratoslog.NewLogger(handler, kratoslog.WithExtractor(tracing.TraceAttrs)).With(attrs...)
	return logger, control
}

func NewZapLogger(root, filename string, options Options, attrs ...any) (*slog.Logger, func()) {
	logger, control := NewReloadableZapLogger(root, filename, options, attrs...)
	return logger, control.Close
}
