package slackwebhook

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"sort"
	"strings"
	"time"

	"github.com/sipeed/picoclaw/pkg/bus"
	"github.com/sipeed/picoclaw/pkg/channels"
	"github.com/sipeed/picoclaw/pkg/config"
	"github.com/sipeed/picoclaw/pkg/logger"
)

const maxTextBlockLength = 3000

// SlackWebhookChannel is an output-only channel that sends messages
// to Slack via Incoming Webhooks using Block Kit formatting.
type SlackWebhookChannel struct {
	*channels.BaseChannel
	bc     *config.Channel
	config *config.SlackWebhookSettings
	client *http.Client
}

// NewSlackWebhookChannel creates a new Slack webhook channel.
func NewSlackWebhookChannel(
	bc *config.Channel,
	cfg *config.SlackWebhookSettings,
	bus *bus.MessageBus,
) (*SlackWebhookChannel, error) {
	if len(cfg.Webhooks) == 0 {
		return nil, fmt.Errorf("slack_webhook: at least one webhook target is required")
	}

	if _, hasDefault := cfg.Webhooks["default"]; !hasDefault {
		return nil, fmt.Errorf("slack_webhook: a 'default' webhook target is required")
	}

	for name, target := range cfg.Webhooks {
		webhookURL := target.WebhookURL.String()
		if webhookURL == "" {
			return nil, fmt.Errorf("slack_webhook: webhook %q has empty webhook_url", name)
		}
		parsed, err := url.Parse(webhookURL)
		if err != nil {
			return nil, fmt.Errorf("slack_webhook: webhook %q has invalid URL format: %w", name, err)
		}
		if !strings.EqualFold(parsed.Scheme, "https") {
			return nil, fmt.Errorf("slack_webhook: webhook %q must use HTTPS (got %q)", name, parsed.Scheme)
		}
	}

	base := channels.NewBaseChannel(
		"slack_webhook",
		cfg,
		bus,
		[]string{"*"},
		channels.WithMaxMessageLength(40000),
	)

	return &SlackWebhookChannel{
		BaseChannel: base,
		bc:          bc,
		config:      cfg,
		client: &http.Client{
			Timeout: 30 * time.Second,
		},
	}, nil
}

// Start initializes the channel. For output-only channels, this is a no-op.
func (c *SlackWebhookChannel) Start(ctx context.Context) error {
	targets := make([]string, 0, len(c.config.Webhooks))
	for name := range c.config.Webhooks {
		targets = append(targets, name)
	}
	sort.Strings(targets)
	logger.InfoCF("slack_webhook", "Starting Slack webhook channel (output-only)", map[string]any{
		"targets": targets,
	})
	c.SetRunning(true)
	return nil
}

// Stop shuts down the channel.
func (c *SlackWebhookChannel) Stop(ctx context.Context) error {
	logger.InfoC("slack_webhook", "Stopping Slack webhook channel")
	c.SetRunning(false)
	return nil
}

// Send delivers a message to the specified Slack webhook target.
func (c *SlackWebhookChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
	if !c.IsRunning() {
		return nil, channels.ErrNotRunning
	}

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

	targetName := msg.ChatID
	if targetName == "" {
		targetName = "default"
	}

	target, ok := c.config.Webhooks[targetName]
	if !ok {
		logger.WarnCF("slack_webhook", "Unknown target, falling back to default", map[string]any{
			"requested": msg.ChatID,
			"using":     "default",
		})
		target = c.config.Webhooks["default"]
		targetName = "default"
	}

	payload := c.buildPayload(msg, target)

	jsonData, err := json.Marshal(payload)
	if err != nil {
		return nil, fmt.Errorf("slack_webhook: failed to marshal payload: %w", err)
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, target.WebhookURL.String(), bytes.NewReader(jsonData))
	if err != nil {
		return nil, fmt.Errorf("slack_webhook: failed to create request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")

	resp, err := c.client.Do(req)
	if err != nil {
		logger.ErrorCF("slack_webhook", "Failed to send message", map[string]any{
			"target": targetName,
		})
		// Don't expose raw error - it may contain webhook URL secrets
		return nil, fmt.Errorf("slack_webhook: network error: %w", channels.ErrTemporary)
	}
	defer resp.Body.Close()

	if resp.StatusCode >= 400 {
		respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
		respText := strings.TrimSpace(string(respBody))
		if respText == "" {
			respText = http.StatusText(resp.StatusCode)
			if respText == "" {
				respText = "unknown error"
			}
		}
		logger.ErrorCF("slack_webhook", "Slack API error", map[string]any{
			"target":   targetName,
			"status":   resp.StatusCode,
			"response": respText,
		})
		sendErr := fmt.Errorf("status %d: %s", resp.StatusCode, respText)
		return nil, fmt.Errorf("slack_webhook: %w", channels.ClassifySendError(resp.StatusCode, sendErr))
	}

	logger.DebugCF("slack_webhook", "Message sent successfully", map[string]any{
		"target": targetName,
	})

	return nil, nil
}

func (c *SlackWebhookChannel) buildPayload(msg bus.OutboundMessage, target config.SlackWebhookTarget) map[string]any {
	payload := make(map[string]any)

	if target.Username != "" {
		payload["username"] = target.Username
	}
	if target.IconEmoji != "" {
		payload["icon_emoji"] = target.IconEmoji
	}

	content := msg.Content
	if content == "" {
		content = "(empty message)"
	}

	blocks := c.buildBlocks(content)
	payload["blocks"] = blocks

	return payload
}

func (c *SlackWebhookChannel) buildBlocks(content string) []map[string]any {
	var blocks []map[string]any

	segments := splitContentWithTables(content)

	for _, seg := range segments {
		if seg.isTable {
			tableText := renderTable(seg.content)
			for _, chunk := range splitText(tableText, maxTextBlockLength) {
				blocks = append(blocks, c.textSection(chunk))
			}
		} else {
			text := strings.TrimSpace(seg.content)
			if text == "" {
				continue
			}
			converted := convertMarkdownToMrkdwn(text)
			for _, chunk := range splitText(converted, maxTextBlockLength) {
				blocks = append(blocks, c.textSection(chunk))
			}
		}
	}

	if len(blocks) == 0 {
		blocks = append(blocks, c.textSection("(empty message)"))
	}

	return blocks
}

func (c *SlackWebhookChannel) textSection(text string) map[string]any {
	return map[string]any{
		"type": "section",
		"text": map[string]any{
			"type": "mrkdwn",
			"text": text,
		},
	}
}

func splitText(text string, maxLen int) []string {
	runes := []rune(text)
	if len(runes) <= maxLen {
		return []string{text}
	}

	const fencePrefix = "```\n"
	const fenceSuffix = "\n```"
	fencePrefixLen := len([]rune(fencePrefix))
	fenceSuffixLen := len([]rune(fenceSuffix))

	var chunks []string
	inFence := false

	for len(runes) > 0 {
		// Calculate content budget reserving space for fence markers
		prefixLen := 0
		if inFence {
			prefixLen = fencePrefixLen
		}
		contentBudget := maxLen - prefixLen - fenceSuffixLen
		if contentBudget <= 0 {
			contentBudget = maxLen
		}

		splitAt := len(runes)
		if splitAt > contentBudget {
			splitAt = findSplitPoint(runes, contentBudget)
			if splitAt <= 0 || splitAt > contentBudget {
				splitAt = contentBudget
			}
		}

		chunkBody := string(runes[:splitAt])
		chunkEndsInFence := endsInsideFence(chunkBody, inFence)
		chunk := wrapFenceChunk(chunkBody, inFence, chunkEndsInFence)

		chunks = append(chunks, chunk)
		inFence = chunkEndsInFence
		runes = runes[splitAt:]
	}

	return chunks
}

func wrapFenceChunk(text string, wasInFence bool, endsInFence bool) string {
	if wasInFence && !strings.HasPrefix(strings.TrimSpace(text), "```") {
		text = "```\n" + text
	}
	if endsInFence {
		text = strings.TrimSuffix(text, "\n") + "\n```"
	}
	return text
}

func findSplitPoint(runes []rune, maxLen int) int {
	if len(runes) <= maxLen {
		return len(runes)
	}
	window := string(runes[:maxLen])

	// Try splitting on newline
	if idx := strings.LastIndex(window, "\n"); idx > 0 {
		return len([]rune(window[:idx])) + 1
	}

	// Try splitting on space
	if idx := strings.LastIndex(window, " "); idx > 0 {
		return len([]rune(window[:idx])) + 1
	}

	// Try to split before a fence marker
	if idx := strings.LastIndex(window, "```"); idx > 0 {
		return len([]rune(window[:idx]))
	}

	return maxLen
}

func endsInsideFence(text string, wasInFence bool) bool {
	return wasInFence != (strings.Count(text, "```")%2 == 1)
}
