// Package ksbot provides a client for the КругСвоих Bot API.
//
// https://krugsvoih.ru/bots.html
//
// Quick start:
//
//	bot := ksbot.New("YOUR_TOKEN")
//
//	bot.OnCommand("start", func(msg *ksbot.Message) {
//	    bot.SendMessage(msg.ChatID, "Привет! 👋", nil)
//	})
//
//	log.Fatal(bot.RunPolling(context.Background(), 30))
package ksbot

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"mime"
	"mime/multipart"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"
)

const baseURL = "https://v.krugsvoih.ru/bot"

// ── Types ──────────────────────────────────────────────────────────────────────

// BotError is returned when the API responds with ok=false.
type BotError struct {
	Description string
}

func (e *BotError) Error() string { return "bot error: " + e.Description }

// Message represents an incoming or sent message.
type Message struct {
	ID              string    `json:"id"`
	ChatID          string    `json:"chatId"`
	SenderID        string    `json:"senderId"`
	SenderUsername  string    `json:"senderUsername"`
	Content         string    `json:"content"`
	SentAt          time.Time `json:"sentAt"`
	IsEdited        bool      `json:"isEdited"`
}

// CallbackQuery represents an inline keyboard button press.
type CallbackQuery struct {
	ID           string `json:"id"`
	FromUserID   string `json:"fromUserId"`
	FromUsername string `json:"fromUsername"`
	ChatID       string `json:"chatId"`
	MessageID    string `json:"messageId"`
	Data         string `json:"data"`
}

// Update represents an incoming update from getUpdates.
type Update struct {
	UpdateID      int64          `json:"updateId"`
	Type          string         `json:"type"`
	Message       *Message       `json:"message,omitempty"`
	CallbackQuery *CallbackQuery `json:"callbackQuery,omitempty"`
}

// BotInfo is returned by GetMe.
type BotInfo struct {
	ID          string `json:"id"`
	Username    string `json:"username"`
	DisplayName string `json:"displayName"`
	AvatarURL   string `json:"avatarUrl"`
	IsBot       bool   `json:"isBot"`
}

// ChatInfo is returned by GetChat.
type ChatInfo struct {
	ID   string `json:"id"`
	Name string `json:"name"`
	Type string `json:"type"`
}

// ChatMember is returned by GetChatMembers / GetChatMember.
type ChatMember struct {
	UserID      string `json:"userId"`
	Username    string `json:"username"`
	DisplayName string `json:"displayName"`
	AvatarURL   string `json:"avatarUrl"`
	Role        string `json:"role"`
	IsBot       bool   `json:"isBot"`
}

// WebhookInfo is returned by GetWebhookInfo.
type WebhookInfo struct {
	URL                string `json:"url"`
	PendingUpdateCount int    `json:"pendingUpdateCount"`
}

// BotCommand represents a bot command.
type BotCommand struct {
	Command     string `json:"command"`
	Description string `json:"description"`
}

// InlineKeyboardButton represents a single button.
type InlineKeyboardButton struct {
	Text         string `json:"text"`
	CallbackData string `json:"callbackData,omitempty"`
	URL          string `json:"url,omitempty"`
}

// InlineKeyboardMarkup represents an inline keyboard.
type InlineKeyboardMarkup struct {
	InlineKeyboard [][]InlineKeyboardButton `json:"inlineKeyboard"`
}

// SendMessageOptions holds optional parameters for SendMessage.
type SendMessageOptions struct {
	ReplyToMessageID string
	ReplyMarkup      *InlineKeyboardMarkup
}

// SendMediaOptions holds optional parameters for SendPhoto/SendDocument.
type SendMediaOptions struct {
	Caption     string
	ReplyMarkup *InlineKeyboardMarkup
}

// ── Client ─────────────────────────────────────────────────────────────────────

// Client is the Bot API client.
type Client struct {
	token   string
	base    string
	http    *http.Client
	offset  int64

	messageHandlers  []func(*Message)
	commandHandlers  map[string]func(*Message)
	callbackHandlers []func(*CallbackQuery)
}

// New creates a new Bot API client with the given token.
func New(token string) *Client {
	return &Client{
		token:           token,
		base:            fmt.Sprintf("%s/%s", baseURL, token),
		http:            &http.Client{Timeout: 40 * time.Second},
		commandHandlers: make(map[string]func(*Message)),
	}
}

// ── Low-level request ──────────────────────────────────────────────────────────

func (c *Client) request(ctx context.Context, method string, payload interface{}) (json.RawMessage, error) {
	url := fmt.Sprintf("%s/%s", c.base, method)

	var body io.Reader
	httpMethod := http.MethodGet
	if payload != nil {
		data, err := json.Marshal(payload)
		if err != nil {
			return nil, err
		}
		body = bytes.NewReader(data)
		httpMethod = http.MethodPost
	}

	req, err := http.NewRequestWithContext(ctx, httpMethod, url, body)
	if err != nil {
		return nil, err
	}
	if payload != nil {
		req.Header.Set("Content-Type", "application/json")
	}

	resp, err := c.http.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	raw, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	var envelope struct {
		OK          bool            `json:"ok"`
		Result      json.RawMessage `json:"result"`
		Description string          `json:"description"`
	}
	if err := json.Unmarshal(raw, &envelope); err != nil {
		return nil, fmt.Errorf("unexpected response: %s", raw)
	}
	if !envelope.OK {
		return nil, &BotError{Description: envelope.Description}
	}
	return envelope.Result, nil
}

func decode[T any](raw json.RawMessage) (T, error) {
	var v T
	return v, json.Unmarshal(raw, &v)
}

// ── Bot info ───────────────────────────────────────────────────────────────────

// GetMe returns basic information about the bot.
func (c *Client) GetMe(ctx context.Context) (*BotInfo, error) {
	raw, err := c.request(ctx, "getMe", nil)
	if err != nil {
		return nil, err
	}
	return decode[*BotInfo](raw)
}

// ── Sending messages ───────────────────────────────────────────────────────────

// SendMessage sends a text message to a chat.
func (c *Client) SendMessage(ctx context.Context, chatID, text string, opts *SendMessageOptions) (*Message, error) {
	payload := map[string]interface{}{"chatId": chatID, "text": text}
	if opts != nil {
		if opts.ReplyToMessageID != "" {
			payload["replyToMessageId"] = opts.ReplyToMessageID
		}
		if opts.ReplyMarkup != nil {
			payload["replyMarkup"] = opts.ReplyMarkup
		}
	}
	raw, err := c.request(ctx, "sendMessage", payload)
	if err != nil {
		return nil, err
	}
	return decode[*Message](raw)
}

func (c *Client) uploadFile(ctx context.Context, method, fileField, filePath string, extra map[string]string) (*Message, error) {
	f, err := os.Open(filePath)
	if err != nil {
		return nil, err
	}
	defer f.Close()

	var buf bytes.Buffer
	w := multipart.NewWriter(&buf)

	for k, v := range extra {
		if err := w.WriteField(k, v); err != nil {
			return nil, err
		}
	}

	mimeType := mime.TypeByExtension(filepath.Ext(filePath))
	if mimeType == "" {
		mimeType = "application/octet-stream"
	}
	part, err := w.CreateFormFile(fileField, filepath.Base(filePath))
	if err != nil {
		return nil, err
	}
	if _, err = io.Copy(part, f); err != nil {
		return nil, err
	}
	w.Close()

	url := fmt.Sprintf("%s/%s", c.base, method)
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, &buf)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", w.FormDataContentType())

	resp, err := c.http.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	raw, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	var envelope struct {
		OK          bool            `json:"ok"`
		Result      json.RawMessage `json:"result"`
		Description string          `json:"description"`
	}
	if err := json.Unmarshal(raw, &envelope); err != nil {
		return nil, fmt.Errorf("unexpected response: %s", raw)
	}
	if !envelope.OK {
		return nil, &BotError{Description: envelope.Description}
	}
	return decode[*Message](envelope.Result)
}

// SendPhotoFile uploads and sends a photo from a local file path.
func (c *Client) SendPhotoFile(ctx context.Context, chatID, filePath string, opts *SendMediaOptions) (*Message, error) {
	extra := map[string]string{"chatId": chatID}
	if opts != nil && opts.Caption != "" {
		extra["caption"] = opts.Caption
	}
	return c.uploadFile(ctx, "sendPhoto/upload", "photo", filePath, extra)
}

// SendDocumentFile uploads and sends a document from a local file path.
func (c *Client) SendDocumentFile(ctx context.Context, chatID, filePath string, opts *SendMediaOptions) (*Message, error) {
	extra := map[string]string{"chatId": chatID}
	if opts != nil && opts.Caption != "" {
		extra["caption"] = opts.Caption
	}
	return c.uploadFile(ctx, "sendDocument/upload", "document", filePath, extra)
}

// SendPhoto sends a photo by URL to a chat.
func (c *Client) SendPhoto(ctx context.Context, chatID, photoURL string, opts *SendMediaOptions) (*Message, error) {
	payload := map[string]interface{}{"chatId": chatID, "photoUrl": photoURL}
	if opts != nil {
		if opts.Caption != "" {
			payload["caption"] = opts.Caption
		}
		if opts.ReplyMarkup != nil {
			payload["replyMarkup"] = opts.ReplyMarkup
		}
	}
	raw, err := c.request(ctx, "sendPhoto", payload)
	if err != nil {
		return nil, err
	}
	return decode[*Message](raw)
}

// SendDocument sends a document by URL to a chat.
func (c *Client) SendDocument(ctx context.Context, chatID, docURL, fileName string, opts *SendMediaOptions) (*Message, error) {
	payload := map[string]interface{}{"chatId": chatID, "documentUrl": docURL, "fileName": fileName}
	if opts != nil {
		if opts.Caption != "" {
			payload["caption"] = opts.Caption
		}
		if opts.ReplyMarkup != nil {
			payload["replyMarkup"] = opts.ReplyMarkup
		}
	}
	raw, err := c.request(ctx, "sendDocument", payload)
	if err != nil {
		return nil, err
	}
	return decode[*Message](raw)
}

// ForwardMessage forwards a message to another chat.
func (c *Client) ForwardMessage(ctx context.Context, fromChatID, messageID, toChatID string) (*Message, error) {
	raw, err := c.request(ctx, "forwardMessage", map[string]string{
		"fromChatId": fromChatID,
		"messageId":  messageID,
		"toChatId":   toChatID,
	})
	if err != nil {
		return nil, err
	}
	return decode[*Message](raw)
}

// ── Editing and deleting ───────────────────────────────────────────────────────

// EditMessageText edits the text of a previously sent message.
func (c *Client) EditMessageText(ctx context.Context, chatID, messageID, sentAt, text string) (*Message, error) {
	raw, err := c.request(ctx, "editMessageText", map[string]string{
		"chatId": chatID, "messageId": messageID, "sentAt": sentAt, "text": text,
	})
	if err != nil {
		return nil, err
	}
	return decode[*Message](raw)
}

// DeleteMessage deletes a message.
func (c *Client) DeleteMessage(ctx context.Context, chatID, messageID, sentAt string) error {
	_, err := c.request(ctx, "deleteMessage", map[string]string{
		"chatId": chatID, "messageId": messageID, "sentAt": sentAt,
	})
	return err
}

// ── Chat management ────────────────────────────────────────────────────────────

// GetChat returns information about a chat.
func (c *Client) GetChat(ctx context.Context, chatID string) (*ChatInfo, error) {
	raw, err := c.request(ctx, "getChat", map[string]string{"chatId": chatID})
	if err != nil {
		return nil, err
	}
	return decode[*ChatInfo](raw)
}

// GetChatMembers returns all members of a chat.
func (c *Client) GetChatMembers(ctx context.Context, chatID string) ([]*ChatMember, error) {
	raw, err := c.request(ctx, "getChatMembers", map[string]string{"chatId": chatID})
	if err != nil {
		return nil, err
	}
	return decode[[]*ChatMember](raw)
}

// GetChatMember returns information about a specific chat member.
func (c *Client) GetChatMember(ctx context.Context, chatID, userID string) (*ChatMember, error) {
	raw, err := c.request(ctx, "getChatMember", map[string]string{"chatId": chatID, "userId": userID})
	if err != nil {
		return nil, err
	}
	return decode[*ChatMember](raw)
}

// BanChatMember bans a user from a chat.
func (c *Client) BanChatMember(ctx context.Context, chatID, userID string) error {
	_, err := c.request(ctx, "banChatMember", map[string]string{"chatId": chatID, "userId": userID})
	return err
}

// UnbanChatMember unbans a previously banned user.
func (c *Client) UnbanChatMember(ctx context.Context, chatID, userID string) error {
	_, err := c.request(ctx, "unbanChatMember", map[string]string{"chatId": chatID, "userId": userID})
	return err
}

// LeaveChat makes the bot leave a chat.
func (c *Client) LeaveChat(ctx context.Context, chatID string) error {
	_, err := c.request(ctx, "leaveChat", map[string]string{"chatId": chatID})
	return err
}

// ── Pinning messages ───────────────────────────────────────────────────────────

// PinMessage pins a message in a chat.
func (c *Client) PinMessage(ctx context.Context, chatID, messageID, sentAt string) error {
	_, err := c.request(ctx, "pinChatMessage", map[string]string{
		"chatId": chatID, "messageId": messageID, "sentAt": sentAt,
	})
	return err
}

// UnpinMessage unpins the current pinned message.
func (c *Client) UnpinMessage(ctx context.Context, chatID string) error {
	_, err := c.request(ctx, "unpinChatMessage", map[string]string{"chatId": chatID})
	return err
}

// ── Updates ────────────────────────────────────────────────────────────────────

// GetUpdates returns incoming updates using long polling.
// Set timeout > 0 for long polling (recommended: 20-30 seconds).
func (c *Client) GetUpdates(ctx context.Context, offset int64, limit, timeout int) ([]*Update, error) {
	raw, err := c.request(ctx, "getUpdates", map[string]interface{}{
		"offset": offset, "limit": limit, "timeout": timeout,
	})
	if err != nil {
		return nil, err
	}
	return decode[[]*Update](raw)
}

// ── Webhooks ───────────────────────────────────────────────────────────────────

// SetWebhook sets a URL to receive updates via webhook.
func (c *Client) SetWebhook(ctx context.Context, url string) error {
	_, err := c.request(ctx, "setWebhook", map[string]string{"url": url})
	return err
}

// DeleteWebhook removes the webhook.
func (c *Client) DeleteWebhook(ctx context.Context) error {
	_, err := c.request(ctx, "deleteWebhook", nil)
	return err
}

// GetWebhookInfo returns current webhook status.
func (c *Client) GetWebhookInfo(ctx context.Context) (*WebhookInfo, error) {
	raw, err := c.request(ctx, "getWebhookInfo", nil)
	if err != nil {
		return nil, err
	}
	return decode[*WebhookInfo](raw)
}

// ── Commands ───────────────────────────────────────────────────────────────────

// SetMyCommands sets the list of bot commands.
func (c *Client) SetMyCommands(ctx context.Context, commands []BotCommand) error {
	_, err := c.request(ctx, "setMyCommands", map[string]interface{}{"commands": commands})
	return err
}

// GetMyCommands returns the current list of bot commands.
func (c *Client) GetMyCommands(ctx context.Context) ([]BotCommand, error) {
	raw, err := c.request(ctx, "getMyCommands", nil)
	if err != nil {
		return nil, err
	}
	return decode[[]BotCommand](raw)
}

// ── Callback queries ───────────────────────────────────────────────────────────

// AnswerCallbackQuery answers an inline keyboard callback query.
func (c *Client) AnswerCallbackQuery(ctx context.Context, callbackQueryID, text string, showAlert bool) error {
	payload := map[string]interface{}{
		"callbackQueryId": callbackQueryID,
		"showAlert":       showAlert,
	}
	if text != "" {
		payload["text"] = text
	}
	_, err := c.request(ctx, "answerCallbackQuery", payload)
	return err
}

// ── Handler registration ───────────────────────────────────────────────────────

// OnMessage registers a handler for all non-command text messages.
func (c *Client) OnMessage(fn func(*Message)) *Client {
	c.messageHandlers = append(c.messageHandlers, fn)
	return c
}

// OnCommand registers a handler for a specific bot command (without the /).
func (c *Client) OnCommand(command string, fn func(*Message)) *Client {
	c.commandHandlers[strings.TrimPrefix(command, "/")] = fn
	return c
}

// OnCallback registers a handler for inline keyboard callback queries.
func (c *Client) OnCallback(fn func(*CallbackQuery)) *Client {
	c.callbackHandlers = append(c.callbackHandlers, fn)
	return c
}

// ── Dispatch ───────────────────────────────────────────────────────────────────

func (c *Client) dispatch(update *Update) {
	if update.Message != nil {
		msg := update.Message
		text := msg.Content
		if strings.HasPrefix(text, "/") {
			parts := strings.Fields(text)
			cmd := strings.ToLower(strings.TrimPrefix(strings.Split(parts[0], "@")[0], "/"))
			if handler, ok := c.commandHandlers[cmd]; ok {
				handler(msg)
				return
			}
		}
		for _, h := range c.messageHandlers {
			h(msg)
		}
	}
	if update.CallbackQuery != nil {
		for _, h := range c.callbackHandlers {
			h(update.CallbackQuery)
		}
	}
}

// ── Polling loop ───────────────────────────────────────────────────────────────

// RunPolling starts the long-polling loop. Blocks until context is cancelled.
// pollTimeout is the server-side wait time in seconds (use 20-30 for long polling).
func (c *Client) RunPolling(ctx context.Context, pollTimeout int) error {
	me, err := c.GetMe(ctx)
	if err != nil {
		return fmt.Errorf("getMe failed: %w", err)
	}
	log.Printf("Bot @%s started.", me.Username)

	for {
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
		}

		updates, err := c.GetUpdates(ctx, c.offset, 100, pollTimeout)
		if err != nil {
			if ctx.Err() != nil {
				return ctx.Err()
			}
			log.Printf("[error] %v — retrying in 5s", err)
			select {
			case <-time.After(5 * time.Second):
			case <-ctx.Done():
				return ctx.Err()
			}
			continue
		}

		for _, u := range updates {
			func() {
				defer func() {
					if r := recover(); r != nil {
						log.Printf("[handler panic] %v", r)
					}
				}()
				c.dispatch(u)
			}()
			c.offset = u.UpdateID + 1
		}
	}
}

// ── Keyboard helpers ───────────────────────────────────────────────────────────

// NewInlineKeyboard builds an InlineKeyboardMarkup from rows of buttons.
func NewInlineKeyboard(rows ...[]InlineKeyboardButton) InlineKeyboardMarkup {
	return InlineKeyboardMarkup{InlineKeyboard: rows}
}

// CallbackButton creates a button that triggers a callback query.
func CallbackButton(text, data string) InlineKeyboardButton {
	return InlineKeyboardButton{Text: text, CallbackData: data}
}

// URLButton creates a button that opens a URL.
func URLButton(text, url string) InlineKeyboardButton {
	return InlineKeyboardButton{Text: text, URL: url}
}
