package im

import (
	"context"
	"errors"
	"fmt"
	"strings"
)

// ErrUserNotFound means the directory has no user for the given identifier.
// Callers treat it as permanent: retrying the same lookup cannot fix it.
var ErrUserNotFound = errors.New("im user not found")

// ErrPermissionDenied means the app is missing an API scope. Only an operator
// granting it in the provider's console can change the answer, so callers stop
// rather than spend a retry budget re-asking a question already settled.
var ErrPermissionDenied = errors.New("im app lacks the required permission")

// MentionTag renders a provider user ID as an @-mention, falling back to a plain
// label when there is nothing safe to render. Provider IDs are opaque tokens,
// so one carrying markup is rejected rather than interpolated into a message.
func MentionTag(userID, fallback string) string {
	if userID != "" && !strings.ContainsAny(userID, "<>\"'& ") {
		return fmt.Sprintf("<at id=%s></at>", userID)
	}
	return fallback
}

// Channel describes an IM group that can receive subscription deliveries.
type Channel struct {
	ID             string
	Name           string
	BotIsInChannel bool
}

// Card represents a message card to send via IM. Title and Color render a
// header; leaving Title empty produces a headerless card, which is what a reply
// inside a topic wants — the topic already has a heading.
type Card struct {
	Title      string
	Color      string // template color: blue, red, yellow, green, etc.
	Body       string // markdown content
	Note       string // plain-text footer below the body and actions
	ActionRows [][]CardAction
}

// CardAction is a button on a card. Exactly one of Value or URL should be set:
// Value makes a callback button whose payload comes back to the handler; URL
// makes a link button that opens the address in a browser.
type CardAction struct {
	Label string
	Style string // "primary", "danger", or "" for default
	Value map[string]string
	URL   string
}

// Keys and values carried by a card button. They are a contract between
// whoever builds a card and whoever handles the click — two peers that must
// not depend on each other, so the vocabulary lives here with the rest of the
// IM types.
const (
	ActionKey     = "action"
	ActionAlertID = "alert_id"
	ActionAck     = "ack"
)

// CardActionResult answers a click. Toast is the transient confirmation shown
// to whoever clicked; Card, when set, replaces the card in place for everyone.
type CardActionResult struct {
	Toast string
	// ToastError renders the toast as a failure rather than a confirmation.
	ToastError bool
	Card       *Card
}

// IncomingEvent is a normalized inbound IM event.
// Exactly one of the fields below is set.
type IncomingEvent struct {
	EventID    string // provider event ID, for redelivery dedup
	Message    *IncomingMessage
	Recall     *RecallEvent
	CardAction *IncomingCardAction
}

// IncomingCardAction is someone pressing a button on a card. Unlike the other
// events it is answered synchronously: the provider holds the click open until
// the handler returns something to show the person who made it.
type IncomingCardAction struct {
	MessageID string // the card that was clicked
	ChannelID string
	UserID    string            // provider user ID of whoever clicked
	Value     map[string]string // whatever CardAction.Value carried
}

// IncomingMessage is a message sent by a human user in a group the bot is in.
// Provider implementations must filter out bot/app senders (including the
// bot's own replies) before returning a message.
type IncomingMessage struct {
	MessageID string
	ChannelID string
	ThreadID  string // empty if the message is not in a topic/thread
	SenderID  string // provider user ID (e.g. Lark open_id)
	MsgType   string // "text", or provider type for non-text ("image", "post", ...)
	Text      string // plain text with mention placeholders stripped; empty for non-text
	Mentions  []Mention
}

// Mention is a user @-mentioned in a message.
type Mention struct {
	UserID string
	Name   string
}

// RecallEvent signals a previously received message was recalled.
type RecallEvent struct {
	MessageID string
}

// Provider abstracts IM operations. Core code never references Lark/Slack directly.
// Phase 1 implementation: pkg/im/lark/
type Provider interface {
	// InspectChannel verifies that a channel exists and reports whether the
	// current bot is a member. It must not send a message to the channel.
	InspectChannel(ctx context.Context, channelID string) (*Channel, error)

	// ListChannels returns every group the bot is currently a member of, so
	// callers can offer a picker instead of asking for a raw channel ID.
	// Every returned channel has BotIsInChannel set.
	ListChannels(ctx context.Context) ([]Channel, error)

	// SendCardToChannel posts a card to a thread-mode group, where every
	// top-level message becomes a topic. Returns the messageID and the threadID
	// of the created topic.
	SendCardToChannel(ctx context.Context, channelID string, card *Card) (messageID string, threadID string, err error)

	// ReplyToThread replies within a topic/thread.
	ReplyToThread(ctx context.Context, messageID, content string) error

	// ForwardThread forwards a topic to another group as a pointer.
	// The target group can see all subsequent replies in the original topic.
	ForwardThread(ctx context.Context, threadID, targetChannelID string) error

	// ForwardThreadToUser forwards a topic into the bot's direct chat with one
	// user, as the same pointer ForwardThread produces, and returns the message
	// it created so the caller can act on it. Acting on the alert itself happens
	// in the original topic, so the user must be able to open it.
	ForwardThreadToUser(ctx context.Context, threadID, userID string) (messageID string, err error)

	// SendCard sends a standalone card message to a group (no thread association).
	SendCard(ctx context.Context, channelID string, card *Card) (messageID string, err error)

	// UpdateCard replaces the content of an interactive card this app sent.
	// Editing is silent — nobody is re-notified — so it keeps a posted card
	// accurate for whoever reads it later; it never substitutes for a reply.
	UpdateCard(ctx context.Context, messageID string, card *Card) error

	// MakeUrgent sends an in-app buzz notification for a message to specified users.
	MakeUrgent(ctx context.Context, messageID string, userIDs []string) error

	// GetReadUsers returns the list of user IDs who have read a message.
	GetReadUsers(ctx context.Context, messageID string) ([]string, error)

	// GetUserPhone returns the phone number for a user.
	GetUserPhone(ctx context.Context, userID string) (string, error)

	// GetUserEmail returns the email for a user, for audit actor identity.
	GetUserEmail(ctx context.Context, userID string) (string, error)

	// GetUserName returns the display name for a user.
	GetUserName(ctx context.Context, userID string) (string, error)

	// GetUserIDByEmail resolves an email to a provider user ID. Alert owners
	// come from the on-call schedule as emails, while delivery APIs address
	// people by provider ID. Returns ErrUserNotFound when there is no match.
	GetUserIDByEmail(ctx context.Context, email string) (string, error)
}
