// Package telescope یک بازرسِ سبکِ درخواست‌ها است (شبیهِ Laravel Telescope).
// آخرین N درخواست را در یک رینگ‌بافرِ حافظه نگه می‌دارد: متد، مسیر، وضعیت،
// مدت‌زمان، IP، هدرها، payloadِ درخواست، پاسخ و شماره‌ی موبایلِ کاربر. بدونِ
// وابستگیِ خارجی و با سرباری ناچیز؛ برای پایشِ سریع روی سرورِ کم‌منابع مناسب است.
package telescope

import (
	"bufio"
	"bytes"
	"encoding/json"
	"io"
	"net"
	"net/http"
	"regexp"
	"sort"
	"strings"
	"sync"
	"time"
)

const (
	maxReqBody  = 8 << 10  // ۸ کیلوبایت از payloadِ درخواست
	maxRespBody = 16 << 10 // ۱۶ کیلوبایت از پاسخ
)

// Entry یک درخواستِ ثبت‌شده را نگه می‌دارد.
type Entry struct {
	ID       int64     `json:"id"`
	Time     time.Time `json:"time"`
	Method   string    `json:"method"`
	Path     string    `json:"path"`
	Status   int       `json:"status"`
	Duration int64     `json:"duration_ms"`
	IP       string    `json:"ip"`
	Bytes    int       `json:"bytes"`
	UserID   int64     `json:"user_id,omitempty"`
	Mobile   string    `json:"mobile,omitempty"` // در زمانِ نمایش resolve می‌شود
	Headers  string    `json:"headers,omitempty"`
	ReqBody  string    `json:"req_body,omitempty"`  // payloadِ درخواست
	RespBody string    `json:"resp_body,omitempty"` // بدنه‌ی پاسخ
}

// Telescope رینگ‌بافرِ هم‌زمان‌امنِ درخواست‌ها.
type Telescope struct {
	mu   sync.Mutex
	buf  []Entry
	size int
	next int64 // شناسه‌ی افزایشی
	full bool
	head int // اندیسِ نوشتنِ بعدی

	// decodeUID شناسه‌ی کاربر را از توکنِ درخواست (بدونِ DB، فقط HMAC) درمی‌آورد.
	decodeUID func(*http.Request) int64
	// resolveMobile شماره‌ی موبایل را از شناسه (با DB) می‌گیرد؛ فقط در زمانِ نمایش.
	resolveMobile func(int64) string
	mobileCache   map[int64]string
}

// New یک بازرس با ظرفیتِ size می‌سازد (حداقل ۱). decodeUID و resolveMobile
// می‌توانند nil باشند (مثلاً در تست‌ها).
func New(size int, decodeUID func(*http.Request) int64, resolveMobile func(int64) string) *Telescope {
	if size < 1 {
		size = 200
	}
	return &Telescope{
		buf:           make([]Entry, size),
		size:          size,
		decodeUID:     decodeUID,
		resolveMobile: resolveMobile,
		mobileCache:   map[int64]string{},
	}
}

func (t *Telescope) add(e Entry) {
	t.mu.Lock()
	t.next++
	e.ID = t.next
	t.buf[t.head] = e
	t.head = (t.head + 1) % t.size
	if t.head == 0 {
		t.full = true
	}
	t.mu.Unlock()
}

// Entries آخرین درخواست‌ها را به‌ترتیبِ جدید-به-قدیم برمی‌گرداند و در همین‌جا
// شماره‌ی موبایلِ هر کاربر را (با کش) resolve می‌کند.
func (t *Telescope) Entries() []Entry {
	t.mu.Lock()
	n := t.head
	if t.full {
		n = t.size
	}
	out := make([]Entry, 0, n)
	for i := 0; i < n; i++ {
		idx := (t.head - 1 - i + t.size*2) % t.size
		out = append(out, t.buf[idx])
	}
	t.mu.Unlock()

	// resolveِ موبایل بیرون از قفلِ اصلی (ممکن است به DB بزند).
	for i := range out {
		out[i].Mobile = t.mobileFor(out[i].UserID)
		// درخواست‌های ورود (بدونِ توکن) شماره را در payload دارند.
		if out[i].Mobile == "" {
			out[i].Mobile = mobileFromBody(out[i].ReqBody)
		}
	}
	return out
}

func (t *Telescope) mobileFor(id int64) string {
	if id == 0 || t.resolveMobile == nil {
		return ""
	}
	t.mu.Lock()
	if m, ok := t.mobileCache[id]; ok {
		t.mu.Unlock()
		return m
	}
	t.mu.Unlock()
	m := t.resolveMobile(id)
	t.mu.Lock()
	t.mobileCache[id] = m
	t.mu.Unlock()
	return m
}

// statusRecorder کدِ وضعیت، حجم و بخشی از بدنه‌ی پاسخ را می‌گیرد.
type statusRecorder struct {
	http.ResponseWriter
	status int
	bytes  int
	buf    *bytes.Buffer
}

func (r *statusRecorder) WriteHeader(code int) {
	r.status = code
	r.ResponseWriter.WriteHeader(code)
}

func (r *statusRecorder) Write(b []byte) (int, error) {
	if r.status == 0 {
		r.status = http.StatusOK
	}
	if r.buf != nil && r.buf.Len() < maxRespBody {
		remain := maxRespBody - r.buf.Len()
		if remain >= len(b) {
			r.buf.Write(b)
		} else {
			r.buf.Write(b[:remain])
		}
	}
	n, err := r.ResponseWriter.Write(b)
	r.bytes += n
	return n, err
}

// Hijack برای WebSocket لازم است تا میدل‌ور مانعِ ارتقاء نشود.
func (r *statusRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) {
	if h, ok := r.ResponseWriter.(http.Hijacker); ok {
		return h.Hijack()
	}
	return nil, nil, http.ErrNotSupported
}

// Middleware هر درخواستِ /api و /admin را ثبت می‌کند (به‌جز خودِ صفحه‌ی تلسکوپ،
// WebSocket و فایل‌های استاتیک).
func (t *Telescope) Middleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		p := req.URL.Path
		if p == "/ws" || strings.HasPrefix(p, "/admin/telescope") ||
			strings.HasPrefix(p, "/cards/") || strings.HasPrefix(p, "/carpets/") {
			next.ServeHTTP(w, req)
			return
		}

		var reqBody string
		if req.Body != nil && req.Method != http.MethodGet {
			b, _ := io.ReadAll(io.LimitReader(req.Body, maxReqBody))
			req.Body = io.NopCloser(bytes.NewReader(b))
			reqBody = maskSecrets(string(b))
		}

		var uid int64
		if t.decodeUID != nil {
			uid = t.decodeUID(req) // فقط HMAC، بدون DB
		}

		rec := &statusRecorder{ResponseWriter: w, buf: &bytes.Buffer{}}
		start := time.Now()
		next.ServeHTTP(rec, req)
		dur := time.Since(start)

		t.add(Entry{
			Time:     start,
			Method:   req.Method,
			Path:     p,
			Status:   rec.status,
			Duration: dur.Milliseconds(),
			IP:       req.RemoteAddr,
			Bytes:    rec.bytes,
			UserID:   uid,
			Headers:  formatHeaders(req.Header),
			ReqBody:  reqBody,
			RespBody: maskSecrets(rec.buf.String()),
		})
	})
}

// formatHeaders هدرهای درخواست را (با پنهان‌کردنِ مقادیرِ حساس) به رشته تبدیل می‌کند.
func formatHeaders(h http.Header) string {
	keys := make([]string, 0, len(h))
	for k := range h {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	var sb strings.Builder
	for _, k := range keys {
		val := strings.Join(h[k], ", ")
		switch strings.ToLower(k) {
		case "authorization", "cookie", "x-access-token":
			val = "***"
		}
		sb.WriteString(k)
		sb.WriteString(": ")
		sb.WriteString(val)
		sb.WriteString("\n")
	}
	return sb.String()
}

var mobileRe = regexp.MustCompile(`"mobile"\s*:\s*"([^"]+)"`)

// mobileFromBody شماره‌ی موبایل را از payloadِ درخواست (مثلِ ورود) درمی‌آورد.
func mobileFromBody(body string) string {
	if m := mobileRe.FindStringSubmatch(body); len(m) == 2 {
		return m[1]
	}
	return ""
}

// maskSecrets مقادیرِ حساس (توکن، کد، پسورد، امضا) را در JSON پنهان می‌کند.
func maskSecrets(s string) string {
	for _, k := range []string{"password", "token", "code", "otp", "signature", "purchase_data"} {
		s = maskField(s, k)
	}
	return s
}

func maskField(s, key string) string {
	needle := `"` + key + `"`
	i := strings.Index(s, needle)
	if i < 0 {
		return s
	}
	j := strings.Index(s[i:], ":")
	if j < 0 {
		return s
	}
	rest := s[i+j+1:]
	rest = strings.TrimLeft(rest, " ")
	if strings.HasPrefix(rest, `"`) {
		end := strings.Index(rest[1:], `"`)
		if end >= 0 {
			return s[:i+j+1] + `"***"` + rest[end+2:]
		}
	}
	return s
}

// Data آخرین درخواست‌ها را به‌صورتِ JSON برمی‌گرداند.
func (t *Telescope) Data(w http.ResponseWriter, _ *http.Request) {
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	_ = json.NewEncoder(w).Encode(t.Entries())
}

// Page یک صفحه‌ی HTML سبک برای مرورِ درخواست‌ها می‌دهد.
func (t *Telescope) Page(w http.ResponseWriter, _ *http.Request) {
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	_, _ = io.WriteString(w, pageHTML)
}
