// Package pulseradar provides a Go SDK for sending logs and exceptions to PulseRadar.
package pulseradar

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"runtime"
	"sync"
	"time"
)

const (
	defaultEndpoint = "https://ingest.pulseradar.cloud/v1/logs"
	flushSize       = 50
	flushInterval   = 5 * time.Second
)

// Field is a key-value pair attached to a log entry.
type Field struct {
	Key   string
	Value string
}

// F creates a Field.
func F(key, value string) Field { return Field{Key: key, Value: value} }

// Option configures a Client.
type Option func(*Client)

func WithSource(s string)   Option { return func(c *Client) { c.source = s } }
func WithServerID(s string) Option { return func(c *Client) { c.serverID = s } }
func WithEndpoint(e string) Option { return func(c *Client) { c.endpoint = e } }

type logEntry struct {
	Level     string            `json:"level"`
	Message   string            `json:"message"`
	Source    string            `json:"source"`
	Timestamp string            `json:"timestamp"`
	Fields    map[string]string `json:"fields,omitempty"`
}

// Client sends logs to PulseRadar.
type Client struct {
	apiKey   string
	source   string
	serverID string
	endpoint string

	mu    sync.Mutex
	queue []logEntry
	stop  chan struct{}
	done  chan struct{}
}

// New creates a PulseRadar client. apiKey is required.
func New(apiKey string, opts ...Option) *Client {
	c := &Client{
		apiKey:   apiKey,
		source:   "go",
		endpoint: defaultEndpoint,
		stop:     make(chan struct{}),
		done:     make(chan struct{}),
	}
	for _, o := range opts {
		o(c)
	}
	go c.flusher()
	return c
}

func (c *Client) Debug(msg string, fields ...Field)    { c.log("debug", msg, fields) }
func (c *Client) Info(msg string, fields ...Field)     { c.log("info", msg, fields) }
func (c *Client) Warning(msg string, fields ...Field)  { c.log("warning", msg, fields) }
func (c *Client) Error(msg string, fields ...Field)    { c.log("error", msg, fields) }
func (c *Client) Critical(msg string, fields ...Field) { c.log("critical", msg, fields) }

// CaptureException sends an error log with stack trace.
func (c *Client) CaptureException(err error, fields ...Field) {
	if err == nil {
		return
	}
	// Capture stack
	pcs := make([]uintptr, 6)
	n := runtime.Callers(2, pcs)
	frames := runtime.CallersFrames(pcs[:n])
	var stack string
	count := 0
	for {
		frame, more := frames.Next()
		if count > 0 {
			stack += " | "
		}
		stack += fmt.Sprintf("%s:%d %s", frame.File, frame.Line, frame.Function)
		count++
		if !more || count >= 5 {
			break
		}
	}

	f := append(fields,
		F("exception_type", fmt.Sprintf("%T", err)),
		F("stack", stack),
	)
	c.log("error", err.Error(), f)
}

// Flush sends all buffered entries immediately.
func (c *Client) Flush() {
	c.mu.Lock()
	batch := c.queue
	c.queue = nil
	c.mu.Unlock()
	if len(batch) > 0 {
		c.send(batch)
	}
}

// Close flushes remaining entries and stops the background goroutine.
func (c *Client) Close() {
	close(c.stop)
	<-c.done
	c.Flush()
}

func (c *Client) log(level, msg string, fields []Field) {
	fm := make(map[string]string, len(fields))
	for _, f := range fields {
		fm[f.Key] = f.Value
	}
	entry := logEntry{
		Level:     level,
		Message:   msg,
		Source:    c.source,
		Timestamp: time.Now().UTC().Format(time.RFC3339Nano),
		Fields:    fm,
	}
	c.mu.Lock()
	c.queue = append(c.queue, entry)
	shouldFlush := len(c.queue) >= flushSize
	c.mu.Unlock()
	if shouldFlush {
		go c.Flush()
	}
}

func (c *Client) flusher() {
	defer close(c.done)
	ticker := time.NewTicker(flushInterval)
	defer ticker.Stop()
	for {
		select {
		case <-c.stop:
			return
		case <-ticker.C:
			c.Flush()
		}
	}
}

func (c *Client) send(entries []logEntry) {
	b, err := json.Marshal(entries)
	if err != nil {
		return
	}
	req, err := http.NewRequest(http.MethodPost, c.endpoint, bytes.NewReader(b))
	if err != nil {
		return
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+c.apiKey)
	req.Header.Set("User-Agent", "pulseradar-go/1.0")
	if c.serverID != "" {
		req.Header.Set("X-Server-Id", c.serverID)
	}
	client := &http.Client{Timeout: 10 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return
	}
	resp.Body.Close()
}
