// Package logging provides admin-server-local structured stdout logging.
// It intentionally lives under server/admin/internal/platform because admin is a separate Go module.
package logging

import (
	"context"
	"fmt"
	"io"
	"log"
	"log/slog"
	"net/http"
	"os"
	"strings"
	"sync"
	"time"

	"hyapp-admin-server/internal/appctx"

	"github.com/gin-gonic/gin"
)

const (
	defaultLevel           = "info"
	defaultFormat          = "json"
	defaultMaxPayloadBytes = 2048
	redactedValue          = "***REDACTED***"
)

type Config struct {
	Level               string `yaml:"level"`
	Format              string `yaml:"format"`
	IncludeRequestBody  bool   `yaml:"include_request_body"`
	IncludeResponseBody bool   `yaml:"include_response_body"`
	MaxPayloadBytes     int    `yaml:"max_payload_bytes"`
}

var (
	globalMu     sync.RWMutex
	globalLogger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo, ReplaceAttr: replaceAttr}))
)

func DefaultConfig() Config {
	return Config{Level: defaultLevel, Format: defaultFormat, MaxPayloadBytes: defaultMaxPayloadBytes}
}

func Init(service string, env string, nodeID string, cfg Config) error {
	level, err := parseLevel(cfg.Level)
	if err != nil {
		return err
	}
	format := strings.ToLower(strings.TrimSpace(cfg.Format))
	if format == "" {
		format = defaultFormat
	}

	var handler slog.Handler
	options := &slog.HandlerOptions{Level: level, ReplaceAttr: replaceAttr}
	switch format {
	case "json":
		handler = slog.NewJSONHandler(os.Stdout, options)
	case "text":
		handler = slog.NewTextHandler(os.Stdout, options)
	default:
		return fmt.Errorf("unsupported log format %q", cfg.Format)
	}
	logger := slog.New(handler).With(
		"env", nonEmpty(env, "local"),
		"service", nonEmpty(service, "admin-server"),
		"node_id", nonEmpty(nodeID, service),
	)

	globalMu.Lock()
	globalLogger = logger
	globalMu.Unlock()
	slog.SetDefault(logger)
	log.SetFlags(0)
	log.SetOutput(stdWriter{})
	return nil
}

func StdWriter() io.Writer {
	return stdWriter{}
}

func GinAccessLogger() gin.HandlerFunc {
	return func(c *gin.Context) {
		if c.Request.URL.Path == "/healthz" || c.Request.URL.Path == "/readyz" {
			c.Next()
			return
		}
		startedAt := time.Now()
		c.Next()

		attrs := []slog.Attr{
			slog.String("request_id", requestID(c)),
			slog.String("app_code", appctx.FromContext(c.Request.Context())),
			slog.String("method", c.Request.Method),
			slog.String("path", c.Request.URL.Path),
			slog.Int("status", c.Writer.Status()),
			slog.Int64("duration_ms", time.Since(startedAt).Milliseconds()),
			slog.String("client_ip", c.ClientIP()),
		}
		if len(c.Errors) > 0 {
			attrs = append(attrs, slog.String("error", c.Errors.String()))
		}
		switch {
		case c.Writer.Status() >= http.StatusInternalServerError:
			logAttrs(context.Background(), slog.LevelError, "http_access", attrs...)
		case c.Writer.Status() >= http.StatusBadRequest:
			logAttrs(context.Background(), slog.LevelWarn, "http_access", attrs...)
		default:
			logAttrs(context.Background(), slog.LevelInfo, "http_access", attrs...)
		}
	}
}

type stdWriter struct{}

func (stdWriter) Write(data []byte) (int, error) {
	line := strings.TrimSpace(string(data))
	if line != "" {
		logAttrs(context.Background(), slog.LevelInfo, "stdlog", slog.String("message", redactLine(line)))
	}
	return len(data), nil
}

func logAttrs(ctx context.Context, level slog.Level, msg string, attrs ...slog.Attr) {
	globalMu.RLock()
	logger := globalLogger
	globalMu.RUnlock()
	logger.LogAttrs(ctx, level, msg, attrs...)
}

func requestID(c *gin.Context) string {
	if value, ok := c.Get("requestID"); ok {
		if requestID, ok := value.(string); ok {
			return requestID
		}
	}
	return c.GetHeader("X-Request-ID")
}

func parseLevel(value string) (slog.Level, error) {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case "debug":
		return slog.LevelDebug, nil
	case "", "info":
		return slog.LevelInfo, nil
	case "warn", "warning":
		return slog.LevelWarn, nil
	case "error":
		return slog.LevelError, nil
	default:
		return slog.LevelInfo, fmt.Errorf("unsupported log level %q", value)
	}
}

func replaceAttr(_ []string, attr slog.Attr) slog.Attr {
	if isSensitiveKey(attr.Key) {
		return slog.String(attr.Key, redactedValue)
	}
	return attr
}

func isSensitiveKey(key string) bool {
	normalized := strings.ToLower(strings.ReplaceAll(key, "-", "_"))
	for _, part := range []string{"password", "credential", "secret", "token", "user_sig", "usersig", "authorization", "signature", "private_key", "receipt"} {
		if strings.Contains(normalized, part) {
			return true
		}
	}
	return false
}

func redactLine(line string) string {
	fields := strings.Fields(line)
	changed := false
	for index, field := range fields {
		separator := strings.IndexAny(field, "=:")
		if separator <= 0 {
			continue
		}
		if isSensitiveKey(strings.Trim(field[:separator], "\"'")) {
			fields[index] = field[:separator+1] + redactedValue
			changed = true
		}
	}
	if !changed {
		return line
	}
	return strings.Join(fields, " ")
}

func nonEmpty(value string, fallback string) string {
	value = strings.TrimSpace(value)
	if value == "" {
		return fallback
	}
	return value
}
