// Command faketgmd is a fake Telegram Bot API server for the
// M3 smoke test. It mimics the parts of api.telegram.org
// that broad-announce uses:
//
//   POST /bot<token>/sendMessage   — accept the message, log it
//   POST /bot<token>/getUpdates    — return queued updates, or
//                                    block for `timeout` seconds
//
// Plus admin endpoints for the smoke test:
//
//   POST /admin/queue              — queue a fake incoming
//                                    update (user text) for a
//                                    given bot token. The
//                                    next getUpdates call
//                                    will return it.
//   GET  /admin/sent               — JSON list of every
//                                    sendMessage call received,
//                                    with timestamp. Used by
//                                    the smoke test to confirm
//                                    a delivery happened.
//   POST /admin/reset              — clear sent + queue.
//   GET  /health                   — liveness probe.
//
// M3 uses this purely for tests. The production switch is
// one env var (BA_TELEGRAM_BASE_URL on the deliverd /
// telegramd side); faketgmd is never deployed.
package main

import (
	"encoding/json"
	"flag"
	"io"
	"log/slog"
	"net/http"
	"os"
	"strconv"
	"strings"
	"sync"
	"time"
)

// sentMessage and queuedUpdate are the records faketgmd
// keeps in memory. M3 lives in dev; persistence is
// explicitly out of scope.
type sentMessage struct {
	BotToken string    `json:"bot_token"`
	ChatID   int64     `json:"chat_id"`
	Text     string    `json:"text"`
	SentAt   time.Time `json:"sent_at"`
}

type queuedUpdate struct {
	BotToken  string
	UpdateID  int64
	UserID    int64
	ChatID    int64
	Text      string
	FirstName string
}

type server struct {
	mu              sync.Mutex
	sent            []sentMessage
	queue           []queuedUpdate
	offsetByBot     map[string]int64
	defaultTimeout  int
	failRatePercent int
	logger          *slog.Logger
}

func main() {
	addr := flag.String("addr", ":8830", "listen address")
	timeout := flag.Int("timeout", 25, "default long-poll timeout in seconds")
	failRate := flag.Int("fail-rate", 0, "percent of sendMessage calls to fail (0-100)")
	flag.Parse()

	logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo}))

	s := &server{
		offsetByBot:     map[string]int64{},
		defaultTimeout:  *timeout,
		failRatePercent: *failRate,
		logger:          logger,
	}

	mux := http.NewServeMux()
	mux.HandleFunc("/health", s.handleHealth)
	mux.HandleFunc("/admin/queue", s.handleAdminQueue)
	mux.HandleFunc("/admin/sent", s.handleAdminSent)
	mux.HandleFunc("/admin/reset", s.handleAdminReset)
	// Telegram-shaped endpoints. ServeMux's `/bot` (no
	// trailing slash) is exact-match only. We register
	// `/` (root subtree) so we can read the path inside
	// the handler. /admin/* and /health are still matched
	// by the more specific patterns above because ServeMux
	// prefers the longest match.
	mux.HandleFunc("/", s.routeAny)

	logger.Info("faketgmd listening", "addr", *addr, "timeout", *timeout, "fail_rate", *failRate)
	if err := http.ListenAndServe(*addr, mux); err != nil {
		logger.Error("listen", "err", err)
		os.Exit(1)
	}
}

func (s *server) handleHealth(w http.ResponseWriter, r *http.Request) {
	s.mu.Lock()
	nSent := len(s.sent)
	s.mu.Unlock()
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(map[string]any{
		"status":   "ok",
		"service":  "faketgmd",
		"received": nSent,
		"failed":   0,
	})
}

// handleBot dispatches /bot<token>/<method> to the right
// handler. ServeMux only matches exact paths unless the
// registered pattern ends in "/", and Telegram's URL
// (`/bot<token>/<method>`) doesn't have a slash after
// "bot", so we register `/` and dispatch on the path
// inside routeAny.
func (s *server) routeAny(w http.ResponseWriter, r *http.Request) {
	path := r.URL.Path
	switch {
	case strings.HasPrefix(path, "/bot"):
		rest := strings.TrimPrefix(path, "/bot")
		if rest == "" {
			http.Error(w, "bad path; expected /bot<token>/<method>", http.StatusBadRequest)
			return
		}
		parts := strings.SplitN(rest, "/", 2)
		if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
			http.Error(w, "bad path; expected /bot<token>/<method>", http.StatusBadRequest)
			return
		}
		token, method := parts[0], parts[1]
		switch method {
		case "sendMessage":
			s.handleSendMessage(w, r, token)
		case "getUpdates":
			s.handleGetUpdates(w, r, token)
		default:
			http.Error(w, "unknown method "+method, http.StatusNotFound)
		}
	default:
		http.NotFound(w, r)
	}
}

// handleBot kept for backward-compat with earlier callers; routes
// through routeAny.
func (s *server) handleBot(w http.ResponseWriter, r *http.Request) {
	s.routeAny(w, r)
}

// handleSendMessage accepts the JSON body, optionally
// fails, and logs to the sent[] ring.
func (s *server) handleSendMessage(w http.ResponseWriter, r *http.Request, token string) {
	body, _ := io.ReadAll(r.Body)
	defer r.Body.Close()

	var req struct {
		ChatID int64  `json:"chat_id"`
		Text   string `json:"text"`
	}
	if err := json.Unmarshal(body, &req); err != nil {
		http.Error(w, "bad json: "+err.Error(), http.StatusBadRequest)
		return
	}

	// Roll the failure dice.
	if s.failRatePercent > 0 {
		if n := time.Now().UnixNano() % 100; int(n) < s.failRatePercent {
			s.logger.Warn("faketgmd failing this sendMessage", "fail_rate", s.failRatePercent)
			http.Error(w, `{"ok":false,"description":"intentional fake fail"}`, http.StatusInternalServerError)
			return
		}
	}

	s.mu.Lock()
	s.sent = append(s.sent, sentMessage{
		BotToken: token,
		ChatID:   req.ChatID,
		Text:     req.Text,
		SentAt:   time.Now().UTC(),
	})
	s.mu.Unlock()

	resp := map[string]any{
		"ok": true,
		"result": map[string]any{
			"message_id": time.Now().UnixNano() % 1_000_000,
			"chat":       map[string]any{"id": req.ChatID, "type": "private"},
			"date":       time.Now().Unix(),
			"text":       req.Text,
		},
	}
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(resp)
}

// handleGetUpdates long-polls: waits up to defaultTimeout
// seconds for at least one queued update for this bot,
// returns everything queued, advances the offset.
func (s *server) handleGetUpdates(w http.ResponseWriter, r *http.Request, token string) {
	body, _ := io.ReadAll(r.Body)
	defer r.Body.Close()

	var req struct {
		Offset  int64 `json:"offset"`
		Timeout int   `json:"timeout"`
	}
	if len(body) > 0 {
		_ = json.Unmarshal(body, &req)
	}
	if req.Timeout == 0 {
		req.Timeout = s.defaultTimeout
	}

	deadline := time.Now().Add(time.Duration(req.Timeout) * time.Second)
	for time.Now().Before(deadline) {
		s.mu.Lock()
		var updates []map[string]any
		for _, u := range s.queue {
			if u.BotToken != token {
				continue
			}
			if u.UpdateID < req.Offset {
				continue
			}
			updates = append(updates, map[string]any{
				"update_id": u.UpdateID,
				"message": map[string]any{
					"message_id": u.UpdateID,
					"from": map[string]any{
						"id":         u.UserID,
						"is_bot":     false,
						"first_name": u.FirstName,
					},
					"chat": map[string]any{
						"id":   u.ChatID,
						"type": "private",
					},
					"text": u.Text,
					"date": time.Now().Unix(),
				},
			})
		}
		// Advance offset to last+1.
		var maxID int64
		for _, u := range s.queue {
			if u.BotToken == token && u.UpdateID > maxID {
				maxID = u.UpdateID
			}
		}
		if maxID >= req.Offset {
			s.offsetByBot[token] = maxID + 1
		}
		s.mu.Unlock()

		if len(updates) > 0 {
			w.Header().Set("Content-Type", "application/json")
			_ = json.NewEncoder(w).Encode(map[string]any{
				"ok":     true,
				"result": updates,
			})
			return
		}
		// Sleep a bit, then poll again. Long-poll in the
		// spec keeps the connection open; we simulate by
		// re-checking every 500ms.
		time.Sleep(500 * time.Millisecond)
	}

	// Timeout with no updates.
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(map[string]any{
		"ok":     true,
		"result": []map[string]any{},
	})
}

// handleAdminQueue accepts a JSON body to enqueue a fake
// incoming update for a bot. Used by the smoke test to
// simulate a user typing /start <code> or /subscribe ...
func (s *server) handleAdminQueue(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "POST only", http.StatusMethodNotAllowed)
		return
	}
	body, _ := io.ReadAll(r.Body)
	defer r.Body.Close()

	var req struct {
		BotToken  string `json:"bot_token"`
		UserID    int64  `json:"user_id"`
		ChatID    int64  `json:"chat_id"`
		Text      string `json:"text"`
		FirstName string `json:"first_name"`
	}
	if err := json.Unmarshal(body, &req); err != nil {
		http.Error(w, "bad json: "+err.Error(), http.StatusBadRequest)
		return
	}
	if req.BotToken == "" || req.UserID == 0 || req.ChatID == 0 {
		http.Error(w, "bot_token, user_id, chat_id required", http.StatusBadRequest)
		return
	}
	if req.FirstName == "" {
		req.FirstName = "FakeUser"
	}

	s.mu.Lock()
	defer s.mu.Unlock()
	s.queue = append(s.queue, queuedUpdate{
		BotToken:  req.BotToken,
		UpdateID:  int64(len(s.queue) + 1),
		UserID:    req.UserID,
		ChatID:    req.ChatID,
		Text:      req.Text,
		FirstName: req.FirstName,
	})
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(map[string]any{
		"queued":    true,
		"update_id": len(s.queue),
	})
}

func (s *server) handleAdminSent(w http.ResponseWriter, r *http.Request) {
	s.mu.Lock()
	defer s.mu.Unlock()
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(map[string]any{
		"count": len(s.sent),
		"items": s.sent,
	})
}

func (s *server) handleAdminReset(w http.ResponseWriter, r *http.Request) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.sent = nil
	s.queue = nil
	s.offsetByBot = map[string]int64{}
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(map[string]any{"ok": true})
}

var _ = strconv.Itoa // keep import
