package observability

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"path/filepath"

	"github.com/prometheus/client_golang/prometheus/promhttp"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
)

// NewLogger creates a zap production logger at the given level that writes to stderr.
func NewLogger(level string, pretty bool) (*zap.Logger, error) {
	lvl, err := zapcore.ParseLevel(level)
	if err != nil {
		lvl = zapcore.InfoLevel
	}

	cfg := zap.NewProductionConfig()
	cfg.Level = zap.NewAtomicLevelAt(lvl)
	if !pretty {
		return cfg.Build()
	}

	encoder := zapcore.NewJSONEncoder(cfg.EncoderConfig)
	sink := &prettyJSONWriteSyncer{out: zapcore.Lock(zapcore.AddSync(os.Stderr))}
	core := zapcore.NewCore(encoder, sink, cfg.Level)
	return zap.New(core, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel)), nil
}

// NewLoggerWithFile creates a zap production logger at the given level. If path
// is empty it behaves exactly like NewLogger. When path is set, the log file's
// parent directory is created and log entries are written to that file
// instead of stderr.
func NewLoggerWithFile(level string, pretty bool, path string) (*zap.Logger, error) {
	if path == "" {
		return NewLogger(level, pretty)
	}

	lvl, err := zapcore.ParseLevel(level)
	if err != nil {
		lvl = zapcore.InfoLevel
	}

	if dir := filepath.Dir(path); dir != "" && dir != "." {
		if err := os.MkdirAll(dir, 0o755); err != nil {
			return nil, fmt.Errorf("mkdir log dir %s: %w", dir, err)
		}
	}
	file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600)
	if err != nil {
		return nil, fmt.Errorf("open log file %s: %w", path, err)
	}

	cfg := zap.NewProductionConfig()
	cfg.Level = zap.NewAtomicLevelAt(lvl)

	var sink zapcore.WriteSyncer = zapcore.Lock(zapcore.AddSync(file))
	if pretty {
		sink = &prettyJSONWriteSyncer{out: sink}
	}
	encoder := zapcore.NewJSONEncoder(cfg.EncoderConfig)
	core := zapcore.NewCore(encoder, sink, cfg.Level)
	return zap.New(core, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel)), nil
}

type prettyJSONWriteSyncer struct {
	out zapcore.WriteSyncer
}

func (w *prettyJSONWriteSyncer) Write(p []byte) (int, error) {
	trimmed := bytes.TrimSpace(p)
	if len(trimmed) == 0 {
		return len(p), nil
	}

	var pretty bytes.Buffer
	if err := json.Indent(&pretty, trimmed, "", "  "); err != nil {
		_, writeErr := w.out.Write(p)
		return len(p), writeErr
	}
	pretty.WriteByte('\n')

	_, err := w.out.Write(pretty.Bytes())
	return len(p), err
}

func (w *prettyJSONWriteSyncer) Sync() error {
	return w.out.Sync()
}

// ServeMetrics starts a Prometheus /metrics endpoint on the given port.
// It blocks until the server exits.
func ServeMetrics(port int) error {
	mux := http.NewServeMux()
	mux.Handle("/metrics", promhttp.Handler())
	mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte("ok"))
	})
	addr := fmt.Sprintf(":%d", port)
	return http.ListenAndServe(addr, mux)
}
