// Package logger provides structured logging utilities.
package logger

import (
	"context"
	"fmt"
	"io"
	"log/slog"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"sync"

	"changkun.de/x/wallfacer/internal/constants"
	"changkun.de/x/wallfacer/internal/pkg/uuidutil"
)

// Package-level named loggers, populated by Init.
// Each component that needs structured logging imports this package and
// references the appropriate var directly (e.g. logger.Runner.Info(...)).
var (
	Main     *slog.Logger
	Runner   *slog.Logger
	Store    *slog.Logger
	Git      *slog.Logger
	Handler  *slog.Logger
	Recovery *slog.Logger
	Prompts  *slog.Logger
)

// init sets up default text-format loggers so that any package importing logger
// gets working loggers even if Init is never called explicitly.
func init() {
	Init("text")
}

// Init configures all named loggers.
// format is "text" (colored, human-friendly) or "json" (structured JSON).
func Init(format string) {
	opts := &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: true}
	var h slog.Handler
	if format == "json" {
		h = slog.NewJSONHandler(os.Stdout, opts)
	} else {
		h = newPrettyHandler(os.Stdout, opts)
	}
	base := slog.New(h)
	Main = base.With("component", "main")
	Runner = base.With("component", "runner")
	Store = base.With("component", "store")
	Git = base.With("component", "git")
	Handler = base.With("component", "handler")
	Recovery = base.With("component", "recovery")
	Prompts = base.With("component", "prompts")
}

// Fatal prints a user-friendly error to stderr and exits with code 1.
// It accepts slog-style key-value pairs; if an "error" key is present,
// its value is appended to the message for context.
func Fatal(msg string, args ...any) {
	// Walk the key-value pairs looking for a key named "error" to extract
	// a human-readable error detail, matching the slog convention.
	var errVal string
	for i := 0; i+1 < len(args); i += 2 {
		if k, ok := args[i].(string); ok && k == "error" {
			errVal = fmt.Sprint(args[i+1])
			break
		}
	}
	if errVal != "" {
		fmt.Fprintf(os.Stderr, "fatal: %s: %s\n", msg, errVal)
	} else {
		fmt.Fprintf(os.Stderr, "fatal: %s\n", msg)
	}
	os.Exit(1)
}

// ANSI escape codes.
const (
	ansiReset  = "\033[0m"
	ansiDim    = "\033[2m"
	ansiBold   = "\033[1m"
	ansiGray   = "\033[90m"
	ansiGreen  = "\033[32m"
	ansiYellow = "\033[33m"
	ansiRed    = "\033[31m"
	ansiCyan   = "\033[36m"
)

// prettyHandler formats log records for human consumption with alignment and color.
type prettyHandler struct {
	w        io.Writer
	opts     *slog.HandlerOptions
	mu       sync.Mutex
	preAttrs []slog.Attr
	color    bool
}

// newPrettyHandler creates a prettyHandler that writes colored, human-friendly
// log lines to w. Color is auto-detected based on terminal capabilities.
func newPrettyHandler(w io.Writer, opts *slog.HandlerOptions) *prettyHandler {
	return &prettyHandler{
		w:     w,
		opts:  opts,
		color: isColorEnabled(w),
	}
}

// isColorEnabled returns true when ANSI colors should be written to w.
// It respects NO_COLOR and TERM=dumb, and only enables colors on real terminals.
func isColorEnabled(w io.Writer) bool {
	if os.Getenv("NO_COLOR") != "" || os.Getenv("TERM") == "dumb" {
		return false
	}
	f, ok := w.(*os.File)
	if !ok {
		return false
	}
	fi, err := f.Stat()
	if err != nil {
		return false
	}
	// ModeCharDevice distinguishes real terminals (TTYs) from regular files
	// and pipes, so colors are only emitted to interactive sessions.
	return fi.Mode()&os.ModeCharDevice != 0
}

// clone returns a shallow copy with an independent preAttrs slice.
// The three-index slice expression caps capacity so that subsequent appends
// to the clone allocate a new backing array rather than mutating the original.
func (h *prettyHandler) clone() *prettyHandler {
	return &prettyHandler{
		w:        h.w,
		opts:     h.opts,
		preAttrs: h.preAttrs[:len(h.preAttrs):len(h.preAttrs)],
		color:    h.color,
	}
}

// Enabled reports whether the handler is configured to log at the given level.
func (h *prettyHandler) Enabled(_ context.Context, level slog.Level) bool {
	return level >= h.opts.Level.Level()
}

// WithAttrs returns a new handler with the given attributes pre-set on every record.
func (h *prettyHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
	cp := h.clone()
	cp.preAttrs = append(cp.preAttrs, attrs...)
	return cp
}

// WithGroup is a no-op; groups are not used in this application.
func (h *prettyHandler) WithGroup(_ string) slog.Handler {
	return h // groups are not used in this application
}

// Handle formats and writes a single log record.
// Layout: timestamp  level  component  source  message  | key=value ...
func (h *prettyHandler) Handle(_ context.Context, r slog.Record) error {
	// Collect all attributes: pre-set (from With) then record attrs.
	all := make([]slog.Attr, 0, len(h.preAttrs)+r.NumAttrs())
	all = append(all, h.preAttrs...)
	r.Attrs(func(a slog.Attr) bool {
		all = append(all, a)
		return true
	})

	// Separate the "component" attribute from the rest.
	component := ""
	extra := make([]slog.Attr, 0, len(all))
	for _, a := range all {
		if a.Key == "component" {
			component = a.Value.String()
		} else {
			extra = append(extra, a)
		}
	}

	// col wraps s in an ANSI escape sequence when colors are enabled.
	col := func(code, s string) string {
		if h.color {
			return code + s + ansiReset
		}
		return s
	}

	var b strings.Builder

	// Timestamp: HH:MM:SS.mmm — dim, de-emphasized.
	b.WriteString(col(ansiDim, r.Time.Format("15:04:05.000")))
	b.WriteString("  ")

	// Level: 3-char badge with color.
	switch r.Level {
	case slog.LevelDebug:
		b.WriteString(col(ansiGray, "DBG"))
	case slog.LevelInfo:
		b.WriteString(col(ansiGreen+ansiBold, "INF"))
	case slog.LevelWarn:
		b.WriteString(col(ansiYellow+ansiBold, "WRN"))
	default: // Error and above
		b.WriteString(col(ansiRed+ansiBold, "ERR"))
	}
	b.WriteString("  ")

	// Component: fixed 8-char column, dim cyan (de-emphasised label).
	b.WriteString(col(ansiCyan+ansiDim, fmt.Sprintf("%-8s", component)))
	b.WriteString("  ")

	// Source: file:line, dim so it does not dominate the message.
	if r.PC != 0 {
		frame := slogSource(r.PC)
		b.WriteString(col(ansiDim, fmt.Sprintf("%s:%d", filepath.Base(frame.File), frame.Line)))
		b.WriteString("  ")
	}

	// Message: bold so it stands out from surrounding metadata.
	b.WriteString(col(ansiBold, r.Message))

	// Key=value pairs, separated from the message by a dim pipe.
	if len(extra) > 0 {
		b.WriteString(col(ansiDim, "  │"))
		for _, a := range extra {
			b.WriteString("  ")
			b.WriteString(col(ansiDim, a.Key+"="))
			v := prettyValue(a.Value.Resolve())
			if a.Key == "error" {
				b.WriteString(col(ansiRed+ansiBold, v))
			} else {
				b.WriteString(v)
			}
		}
	}

	b.WriteByte('\n')

	h.mu.Lock()
	defer h.mu.Unlock()
	_, err := fmt.Fprint(h.w, b.String())
	return err
}

// slogSource extracts the source frame from a program counter.
func slogSource(pc uintptr) runtime.Frame {
	frames := runtime.CallersFrames([]uintptr{pc})
	f, _ := frames.Next()
	return f
}

// prettyValue formats a slog.Value for display.
// UUID-formatted strings are shortened to their first 8 characters.
// Very long strings are truncated at 200 characters to keep lines readable.
func prettyValue(v slog.Value) string {
	var s string
	if v.Kind() == slog.KindString {
		s = v.String()
	} else {
		s = fmt.Sprintf("%v", v.Any())
	}
	if uuidutil.IsValid(s) {
		return s[:8]
	}
	if len(s) > constants.LogValueMaxLen {
		s = s[:constants.LogValueMaxLen] + "…"
	}
	if needsQuoting(s) {
		return fmt.Sprintf("%q", s)
	}
	return s
}

// needsQuoting reports whether s must be wrapped in quotes for visual clarity.
func needsQuoting(s string) bool {
	if s == "" {
		return true
	}
	for _, r := range s {
		if r == ' ' || r == '\t' || r == '\n' || r == '\r' || r == '"' || r == '=' {
			return true
		}
	}
	return false
}
