package core

import (
	"context"
	"errors"
	"time"
)

// Platform abstracts a messaging platform (Feishu, DingTalk, Slack, etc.).
type Platform interface {
	Name() string
	Start(handler MessageHandler) error
	Reply(ctx context.Context, replyCtx any, content string) error
	Send(ctx context.Context, replyCtx any, content string) error
	Stop() error
}

// ErrNotSupported indicates a platform doesn't support a particular operation.
var ErrNotSupported = errors.New("operation not supported by this platform")

// ReplyContextReconstructor is an optional interface for platforms that can
// recreate a reply context from a session key. This is needed for cron jobs
// to send messages to users without an incoming message.
type ReplyContextReconstructor interface {
	ReconstructReplyCtx(sessionKey string) (any, error)
}

// MessageRecallDetector is an optional interface for platforms that can check
// whether the message targeted by a reply context was recalled/deleted.
type MessageRecallDetector interface {
	IsMessageRecalled(ctx context.Context, replyCtx any) (bool, error)
}

// CronReplyTargetResolver is an optional interface for platforms that need to
// map a logical cron session key to the actual reply target used at execution
// time. This is useful for platforms where proactive replies may need to create
// or switch to a thread before the cron run starts.
//
// Implementations that do not need special handling should return
// ErrNotSupported so callers can fall back to ReconstructReplyCtx(sessionKey).
type CronReplyTargetResolver interface {
	ResolveCronReplyTarget(sessionKey string, title string) (resolvedSessionKey string, replyCtx any, err error)
}

// SessionEnvInjector is an optional interface for agents that accept
// per-session environment variables (e.g. CC_PROJECT, CC_SESSION_KEY).
type SessionEnvInjector interface {
	SetSessionEnv(env []string)
}

// FormattingInstructionProvider is an optional interface for platforms that
// provide platform-specific formatting instructions for the agent system prompt
// (e.g., Slack mrkdwn vs standard Markdown).
type FormattingInstructionProvider interface {
	FormattingInstructions() string
}

// PlatformPromptInjector is an optional interface for agents that can receive
// platform-specific prompt fragments (e.g., formatting instructions).
// The engine calls this before StartSession when the platform provides formatting.
type PlatformPromptInjector interface {
	SetPlatformPrompt(prompt string)
}

// AgentSystemPrompt returns the system prompt fragment that informs agents about
// cc-connect capabilities (cron scheduling, etc.).
// The prompt is designed to be appended to the agent's existing system prompt.
func AgentSystemPrompt() string {
	return `You are running inside cc-connect, a bridge that connects you to messaging platforms.
Your normal text responses are automatically delivered to the user — just reply normally, do NOT use cc-connect send for ordinary text replies.

## Available tools

### Send generated images, files, or voice messages back to the user
When you generate a local image or file that should be sent to the user, use:

  cc-connect send --image /absolute/path/to/image.png
  cc-connect send --file /absolute/path/to/report.pdf
  cc-connect send --file /absolute/path/to/report.pdf --image /absolute/path/to/chart.png

You may repeat --image / --file multiple times. Use this only for generated attachments that need to be delivered to the user.
If you include --message, do not repeat the exact same sentence again in your normal reply, because your normal reply is also delivered automatically.

When sending an audio (mp3/wav/m4a/ogg/opus) or video (mp4/mov/webm) clip that should render inline as a native voice bubble or video player — instead of as a generic file download — use the dedicated flags:

  cc-connect send --audio /absolute/path/to/clip.mp3
  cc-connect send --video /absolute/path/to/demo.mp4

These render as native media on platforms that support it (e.g. Feishu voice bubbles, Telegram voice messages). cc-connect transparently transcodes audio to the platform's preferred codec (e.g. opus for Feishu). On platforms without dedicated audio/video support cc-connect automatically falls back to the file-attachment path so delivery is preserved. Do NOT downgrade the user's request to --file when they explicitly asked for audio or video.

When the user explicitly asks you to synthesize speech from text, use:

  cc-connect send --tts "text to speak"

After cc-connect send --tts (or --audio) succeeds, reply only with NO_REPLY unless the user also asked for a visible text confirmation. This prevents sending an extra text message after the voice message.

### Scheduled tasks: when to use /cron vs /timer

cc-connect has TWO distinct scheduling commands. Picking the wrong one creates a confusing UX for the user.

  ┌──────────────────────────────┬─────────────────────────────┐
  │ Use cc-connect cron …        │ Use cc-connect timer …      │
  ├──────────────────────────────┼─────────────────────────────┤
  │ Recurring schedule           │ One-shot delay / one-time   │
  │ "每天/每周/每小时"            │ "X 分钟后/小时后/明天"        │
  │ "every day/week/Monday"      │ "in 30 min", "tomorrow 9am"  │
  │ "每天早上6点总结"             │ "3 分钟后检查负载"            │
  │ Lives forever until deleted  │ Auto-archives after firing  │
  │ Queried via /cron            │ Queried via /timer          │
  └──────────────────────────────┴─────────────────────────────┘

When telling the user the task is scheduled, tell them which command to use to view/manage it
(say "use /timer to view" for one-shot, "use /cron to view" for recurring).

### Scheduled tasks (cron) — RECURRING
When the user asks you to do something on a schedule (e.g. "每天早上6点帮我总结GitHub trending"), use the Bash tool to run:

  cc-connect cron add --cron "<min> <hour> <day> <month> <weekday>" --prompt "<task description>" --desc "<short label>"

Environment variables CC_PROJECT and CC_SESSION_KEY are already set, so you do NOT need to specify --project or --session-key.

Optional flags:
  --session-mode <mode>     reuse (default) or new-per-run (fresh session each trigger)
  --timeout-mins <n>        max wait per run in minutes (default 30, 0 = unlimited)
  --exec <command>          run a shell command directly instead of --prompt

Examples:
  cc-connect cron add --cron "0 6 * * *" --prompt "Collect GitHub trending repos and send a summary" --desc "Daily GitHub Trending"
  cc-connect cron add --cron "0 9 * * 1" --prompt "Generate a weekly project status report" --desc "Weekly Report"
  cc-connect cron add --cron "*/2 * * * *" --exec "ipconfig" --session-mode new-per-run --desc "Every 2 min ipconfig"

You can also list, inspect, run, edit, or delete cron jobs:
  cc-connect cron list
  cc-connect cron info <job-id> [field]
  cc-connect cron exec <job-id>
  cc-connect cron edit <job-id> <field> <value>
  cc-connect cron del <job-id>

When changing an existing job, first run ` + "`cc-connect cron info <job-id>`" + ` to inspect the current values, then use ` + "`cron edit`" + ` for only the field(s) the user asked to change.
Use ` + "`cron exec <job-id>`" + ` to run an existing scheduled task immediately; this is different from the ` + "`--exec <command>`" + ` flag used when creating a shell-command cron job.
Use ` + "`cron edit`" + ` instead of delete-and-recreate when only one field changes. Do not delete and recreate a job unless the user explicitly asks to replace it.
Common editable fields:
  cron_expr     new schedule, e.g. "0 9 * * *"
  prompt        new task prompt (or ` + "`exec`" + ` for shell command)
  description   short label
  enabled       true / false  (pause without deleting)
  mute          true / false  (silence all messages)
  timeout_mins  integer minutes (0 = unlimited)
Run ` + "`cc-connect cron edit --help`" + ` for the full field list.

Examples:
  cc-connect cron exec abc123
  cc-connect cron edit abc123 cron_expr "0 9 * * *"
  cc-connect cron edit abc123 enabled false
  cc-connect cron edit abc123 prompt "Updated daily summary task"

### One-shot timers (timer) — ONE-TIME DELAY
When the user asks you to do something AFTER A DELAY or AT A SPECIFIC FUTURE TIME
(e.g. "两小时后帮我检查PR", "3 分钟后看下系统负载", "明天早上 9 点提醒我"),
use the Bash tool to run:

  cc-connect timer add --delay <duration> --prompt "<task description>"

IMPORTANT: do NOT use cron for one-shot delays. A cron expression like "4 19 14 6 *"
means "every year on June 14 at 19:04", not "once on this date". Cron has no built-in
"fire once" mode — use timer for any one-time / delayed request.

Duration examples: 30m, 2h, 1h30m. Or use absolute time: --at "2026-05-16T09:00"
Absolute times without timezone (e.g. "2026-05-16T09:00") are interpreted as the
system's local timezone. When the user says "明天早上9点", use local time.
Environment variables CC_PROJECT and CC_SESSION_KEY are already set.

Optional flags:
  --exec <command>          run a shell command directly instead of --prompt
  --desc <text>             short description
  --session-mode <mode>     reuse (default) or new-per-run (fresh session each run)
  --timeout-mins <n>        max wait per run in minutes (default 30, 0 = unlimited)
  --mute                    suppress all messages (start notification + result)

Examples:
  cc-connect timer add --delay 2h --prompt "Check PR status" --desc "PR check"
  cc-connect timer add --delay 30m --exec "df -h" --desc "Disk check"
  cc-connect timer add --at "2026-05-16T09:00" --prompt "Morning standup reminder"

You can also list or cancel timers:
  cc-connect timer list
  cc-connect timer del <timer-id>

### Bot-to-bot relay
When you need to communicate with another bot (e.g. ask another AI agent a question), use:

  cc-connect relay send --to <target_project> "<message>"

IMPORTANT: <target_project> must be the EXACT project name from the /bind command output.
Do NOT guess or modify the name — use it exactly as shown (e.g. "gemini", not "gemini-bot").

This sends a message to the target bot and waits for its response (printed to stdout).
The conversation is visible in the group chat and each bot maintains its own relay session.

Environment variables CC_PROJECT and CC_SESSION_KEY are already set, so the relay knows which group chat to use.

### Silent reply (suppress delivery)
If the current turn warrants no user-visible response — e.g. a scheduled trigger
found nothing worth reporting, the incoming message was an acknowledgement that
needs no reaction, or it was clearly directed at another participant — end your
reply with the token ` + "`NO_REPLY`" + ` on its own line (case-insensitive). cc-connect strips
the trailing marker before delivery:
- If the whole reply is just ` + "`NO_REPLY`" + ` (or the text becomes empty after the
  marker is stripped), nothing is delivered — no preview, no done reaction, no
  TTS. Prefer this for group-chat gate decisions where silence is the whole point.
- If you wrote reasoning before the marker, the stripped reasoning is still
  delivered as a normal reply (the marker only suppresses itself, not the
  surrounding text).
Use this sparingly; when in doubt, send a brief reply instead.
`
}

// SystemPromptSupporter is an optional marker interface for agents that
// natively inject AgentSystemPrompt() (e.g., via --append-system-prompt).
// Agents that do NOT implement this need the instructions written to their
// memory/instruction file for relay and cron to work.
type SystemPromptSupporter interface {
	HasSystemPromptSupport() bool
}

// SessionIDValidator is an optional interface for agents that can validate
// whether a stored session ID actually belongs to the current project's
// session store. The engine uses this to prevent cross-project session
// context leakage (issue #599): a stale ID from another project's workspace
// would otherwise resume the wrong conversation history.
//
// Implementations should return false when:
//   - the session ID is empty
//   - the session file does not exist under the agent's per-project store
//   - the agent cannot determine the current project directory
//
// The engine treats a false return as "clear the stored ID and start fresh".
type SessionIDValidator interface {
	ValidateSessionID(ctx context.Context, sessionID string) bool
}

// TypingIndicator is an optional interface for platforms that can show a
// "processing" indicator (typing bubble, emoji reaction, etc.) while the
// agent is working. StartTyping is called when processing begins and returns
// a stop function that the caller must invoke when processing ends.
type TypingIndicator interface {
	StartTyping(ctx context.Context, replyCtx any) (stop func())
}

// TypingIndicatorDone is an optional interface for platforms that can show a
// "done" reaction after processing completes. The engine calls AddDoneReaction
// when the agent finishes a multi-round turn in quiet mode, so the user gets
// a push notification (e.g. Feishu card edits don't trigger pushes).
type TypingIndicatorDone interface {
	AddDoneReaction(replyCtx any)
}

// AtMentionSender is an optional interface for platforms that support @mention in
// reply messages (e.g. DingTalk). Platforms that implement this interface can
// include @user notifications when replying in group chats.
type AtMentionSender interface {
	ReplyWithAt(ctx context.Context, replyCtx any, content string, atUsers []string, atAll bool) error
}

// ImageSender is an optional interface for platforms that support sending images.
type ImageSender interface {
	SendImage(ctx context.Context, replyCtx any, img ImageAttachment) error
}

// FileSender is an optional interface for platforms that support sending files.
type FileSender interface {
	SendFile(ctx context.Context, replyCtx any, file FileAttachment) error
}

// MessageUpdater is an optional interface for platforms that support updating messages.
type MessageUpdater interface {
	UpdateMessage(ctx context.Context, replyCtx any, content string) error
}

// StatusFooterSender is an optional Platform extension for sending a reply
// with a structured per-turn status footer rendered using platform-specific
// dim/small styling (e.g. Lark `text_size: "notation"`). Platforms that do
// not implement it fall back to receiving the footer appended inline to the
// content via Send/SendWithButtons/...
type StatusFooterSender interface {
	SendWithStatusFooter(ctx context.Context, replyCtx any, content, footer string) error
}

// StatusFooterUpdater is the streaming-preview counterpart of
// StatusFooterSender: it patches an existing preview message with a final
// content + structured status footer block.
type StatusFooterUpdater interface {
	UpdateMessageWithStatusFooter(ctx context.Context, replyCtx any, content, footer string) error
}

// ProgressStyleProvider is an optional interface for platforms that expose
// a preferred style for intermediate progress rendering.
// Typical values: "legacy", "compact", "card".
type ProgressStyleProvider interface {
	ProgressStyle() string
}

// ProgressCardPayloadSupport is an optional interface for platforms that can
// parse and render structured progress-card payloads.
type ProgressCardPayloadSupport interface {
	SupportsProgressCardPayload() bool
}

// ProgressUpdateThrottler is an optional interface for platforms that need
// rate-limited progress edits (e.g. Discord's ~5 edits / 5s per channel).
type ProgressUpdateThrottler interface {
	ProgressUpdateInterval() time.Duration
}

// ButtonOption represents a clickable inline button.
type ButtonOption struct {
	Text string // display text on the button
	Data string // callback data returned when clicked (≤64 bytes for Telegram)
}

// InlineButtonSender is an optional interface for platforms that support
// sending messages with clickable inline buttons (e.g. Telegram Inline Keyboard).
// Buttons is a 2D slice: each inner slice is one row of buttons.
type InlineButtonSender interface {
	SendWithButtons(ctx context.Context, replyCtx any, content string, buttons [][]ButtonOption) error
}

// CardSender is an optional interface for platforms that support sending
// structured rich cards (e.g. Feishu Interactive Card). Platforms that do not
// implement this interface will receive a plain-text fallback via Card.RenderText().
type CardSender interface {
	SendCard(ctx context.Context, replyCtx any, card *Card) error
	ReplyCard(ctx context.Context, replyCtx any, card *Card) error
}

// CardNavigationHandler is called by platforms to render a card for in-place
// card updates (e.g. Feishu card.action.trigger callback). The action string
// uses prefixes like "nav:/model" or "act:/model 3".
type CardNavigationHandler func(action string, sessionKey string) *Card

// CardNavigable is an optional interface for platforms that support in-place
// card navigation (updating the existing card instead of sending a new message).
type CardNavigable interface {
	SetCardNavigationHandler(h CardNavigationHandler)
}

// CardRefresher is an optional interface for platforms that can update a
// previously rendered card in-place after the original callback has returned.
// This is used when async operations (e.g. delete-mode deletion) need to
// refresh a "loading" card with the final result. Platforms that implement
// this interface should track the message ID from card action callbacks and
// use it to patch the card content.
type CardRefresher interface {
	RefreshCard(ctx context.Context, sessionKey string, card *Card) error
}

// PlatformLifecycleHandler receives readiness state transitions from async
// recoverable platforms.
type PlatformLifecycleHandler interface {
	OnPlatformReady(p Platform)
	OnPlatformUnavailable(p Platform, err error)
}

// AsyncRecoverablePlatform is an optional interface for platforms that start
// a background recovery loop and later report readiness or unavailability.
//
// Platforms implementing this interface may return from Start() before they are
// actually ready to receive traffic. Callers must treat OnPlatformReady as the
// signal that deferred platform capabilities may be initialized and the
// platform is usable. A nil Start() return therefore means the recovery loop
// was launched successfully, not necessarily that an initial connection was
// established.
type AsyncRecoverablePlatform interface {
	Platform
	SetLifecycleHandler(h PlatformLifecycleHandler)
}

// MessageHandler is called by platforms when a new message arrives.
type MessageHandler func(p Platform, msg *Message)

// Agent abstracts an AI coding assistant (Claude Code, Cursor, Gemini CLI, etc.).
// All agents must support persistent bidirectional sessions via StartSession.
type Agent interface {
	Name() string
	// StartSession creates or resumes an interactive session with a persistent process.
	StartSession(ctx context.Context, sessionID string) (AgentSession, error)
	// ListSessions returns sessions known to the agent backend.
	ListSessions(ctx context.Context) ([]AgentSessionInfo, error)
	Stop() error
}

// AgentSession represents a running interactive agent session with a persistent process.
type AgentSession interface {
	// Send sends a user message (with optional images and files) to the running agent process.
	Send(prompt string, images []ImageAttachment, files []FileAttachment) error
	// RespondPermission sends a permission decision back to the agent process.
	RespondPermission(requestID string, result PermissionResult) error
	// Events returns the channel that emits agent events (kept open across turns).
	Events() <-chan Event
	// CurrentSessionID returns the current agent-side session ID.
	CurrentSessionID() string
	// Alive returns true if the underlying process is still running.
	Alive() bool
	// Close terminates the session and its underlying process.
	Close() error
}

// PermissionResult represents the user's decision on a permission request.
type PermissionResult struct {
	Behavior     string         `json:"behavior"`               // "allow" or "deny"
	UpdatedInput map[string]any `json:"updatedInput,omitempty"` // echoed back for allow
	Message      string         `json:"message,omitempty"`      // reason for deny
}

// ToolAuthorizer is an optional interface for agents that support dynamic tool authorization.
type ToolAuthorizer interface {
	AddAllowedTools(tools ...string) error
	GetAllowedTools() []string
}

// HistoryProvider is an optional interface for agents that can retrieve
// conversation history from their backend session files.
type HistoryProvider interface {
	GetSessionHistory(ctx context.Context, sessionID string, limit int) ([]HistoryEntry, error)
}

// ProviderConfig holds API provider settings for an agent.
type ProviderConfig struct {
	Name     string
	APIKey   string
	BaseURL  string
	Model    string
	Models   []ModelOption     // pre-configured list of available models for this provider
	Thinking string            // override thinking type sent to this provider ("disabled", "enabled", or "" for no rewrite)
	Env      map[string]string // arbitrary extra env vars (e.g. CLAUDE_CODE_USE_BEDROCK=1)
	// Codex-specific provider config (maps to Codex model_providers.<name>)
	CodexWireAPI     string            // wire API format (e.g. "responses")
	CodexHTTPHeaders map[string]string // custom HTTP headers
}

// ProviderSwitcher is an optional interface for agents that support multiple API providers.
type ProviderSwitcher interface {
	SetProviders(providers []ProviderConfig)
	SetActiveProvider(name string) bool
	GetActiveProvider() *ProviderConfig
	ListProviders() []ProviderConfig
}

// MemoryFileProvider is an optional interface for agents that support
// persistent instruction files (CLAUDE.md, AGENTS.md, GEMINI.md, etc.).
// The engine uses these paths for the /memory command.
type MemoryFileProvider interface {
	ProjectMemoryFile() string // project-level instruction file (e.g., <work_dir>/CLAUDE.md)
	GlobalMemoryFile() string  // user-level instruction file (e.g., ~/.claude/CLAUDE.md)
}

// ModelSwitcher is an optional interface for agents that support runtime model switching.
// Model changes take effect on the next session (existing sessions keep their model).
type ModelSwitcher interface {
	SetModel(model string)
	GetModel() string
	// AvailableModels tries to fetch models from the provider API.
	// Falls back to a built-in list on failure.
	AvailableModels(ctx context.Context) []ModelOption
}

// ReasoningEffortSwitcher is an optional interface for agents that support
// runtime switching of reasoning effort.
type ReasoningEffortSwitcher interface {
	SetReasoningEffort(effort string)
	GetReasoningEffort() string
	AvailableReasoningEfforts() []string
}

// ModelOption describes a selectable model.
type ModelOption struct {
	Name  string // model identifier passed to CLI
	Desc  string // short description (display_name or empty)
	Alias string // optional short alias for the /model command (e.g. "codex" for "gpt-5.3-codex")
}

// UsageReporter is an optional interface for agents that can report account or
// model quota usage from their backing provider.
type UsageReporter interface {
	GetUsage(ctx context.Context) (*UsageReport, error)
}

// UsageReport is a provider-neutral quota snapshot returned by UsageReporter.
type UsageReport struct {
	Provider  string
	AccountID string
	UserID    string
	Email     string
	Plan      string
	Buckets   []UsageBucket
	Credits   *UsageCredits
}

// UsageBucket groups one logical quota, such as standard requests or code review.
type UsageBucket struct {
	Name         string
	Allowed      bool
	LimitReached bool
	Windows      []UsageWindow
}

// UsageWindow describes a single quota window.
type UsageWindow struct {
	Name              string
	UsedPercent       int
	WindowSeconds     int
	ResetAfterSeconds int
	ResetAtUnix       int64
}

// UsageCredits contains optional credit/balance metadata.
type UsageCredits struct {
	HasCredits bool
	Unlimited  bool
	Balance    string
}

// ContextUsageReporter is an optional interface for running agent sessions that
// can report real runtime context usage for the active conversation.
type ContextUsageReporter interface {
	GetContextUsage() *ContextUsage
}

// ContextUsage describes runtime context consumption for the active session.
type ContextUsage struct {
	// UsedTokens is the current token load to compare against ContextWindow when
	// computing remaining context capacity for the next turn.
	UsedTokens int
	// BaselineTokens is the portion of the context window always occupied by
	// fixed runtime/system instructions and therefore excluded from user-visible
	// "left" calculations when the agent provides it.
	BaselineTokens           int
	TotalTokens              int
	InputTokens              int
	CachedInputTokens        int // cache-read tokens (prior context retrieved from cache)
	CacheCreationInputTokens int // cache-write tokens (new content written to cache)
	OutputTokens             int
	ReasoningOutputTokens    int
	ContextWindow            int
}

// ContextCompressor is an optional interface for agents that support
// compressing/compacting the conversation context within a running session.
// CompressCommand returns the native slash command (e.g. "/compact", "/compress")
// that will be forwarded to the agent process. Return "" if not supported.
type ContextCompressor interface {
	CompressCommand() string
}

// AgentSessionCanceller is an optional interface for agent sessions that support
// cancelling the current turn without terminating the session or its underlying
// process. When implemented, the engine calls CancelTurn instead of Close() for
// /stop, allowing the session to remain alive for the next user message.
type AgentSessionCanceller interface {
	CancelTurn() error
}

// CommandProvider is an optional interface for agents that expose custom slash
// commands via local files (e.g. .claude/commands/*.md). The engine scans the
// returned directories for *.md files and registers them as slash commands.
type CommandProvider interface {
	CommandDirs() []string
}

// SkillProvider is an optional interface for agents that expose skills via
// local directories (e.g. .claude/skills/<name>/SKILL.md). Only the depth-1
// layout is recognised: each immediate subdirectory of the returned dirs
// that contains a SKILL.md is registered as a skill. Nested SKILL.md files
// (e.g. inside `<name>/references/...`) are treated as skill assets and
// ignored — they match the Claude Code CLI convention (issue #1304) and
// prevent phantom slash commands from leaking into platform command menus.
// Skills are project-level and agent-specific — they are NOT shared across
// different agent types.
type SkillProvider interface {
	SkillDirs() []string
}

// SessionDeleter is an optional interface for agents that support deleting sessions.
type SessionDeleter interface {
	DeleteSession(ctx context.Context, sessionID string) error
}

type SessionTitleProvider interface {
	GetSessionTitle(sessionID string) string
}

// WorkDirSwitcher is an optional interface for agents that support runtime
// work directory switching. The change takes effect on the next session start;
// the current running session is terminated automatically by the engine.
type WorkDirSwitcher interface {
	SetWorkDir(dir string)
	GetWorkDir() string
}

// AgentOptsProvider is an optional interface for agents that need to carry
// their full configuration options when the engine clones a per-workspace
// agent instance in multi-workspace mode. The engine merges the returned map
// into the workspace opts before calling the agent factory, giving workspace
// agents access to agent-specific options (e.g. "session" for the tmux agent)
// that are not covered by the standard GetModel / GetMode accessors.
// work_dir is always overridden by the engine and must not be returned here.
type AgentOptsProvider interface {
	BaseOpts() map[string]any
}

// ModeSwitcher is an optional interface for agents that support runtime permission mode switching.
type ModeSwitcher interface {
	SetMode(mode string)
	GetMode() string
	PermissionModes() []PermissionModeInfo
}

// WorkspaceAgentOptionSnapshotter is an optional interface for agents that can
// export reusable constructor options needed to recreate an equivalent agent in
// a different workspace. Snapshot values should omit work_dir; the caller is
// responsible for setting the target workspace explicitly. Provider wiring and
// run_as propagation may still be handled separately by the engine.
type WorkspaceAgentOptionSnapshotter interface {
	WorkspaceAgentOptions() map[string]any
}

// LiveModeSwitcher is an optional interface for running agent sessions that can
// apply a mode change immediately without restarting the process.
type LiveModeSwitcher interface {
	SetLiveMode(mode string) bool
}

// StartupWarner is an optional interface for agent sessions that need to surface
// a one-time warning to the IM user at session start (e.g. when a requested
// permission mode was silently downgraded due to OS constraints). The engine
// sends the returned message to the IM platform immediately after starting the
// session. Returns empty string when no warning is needed.
type StartupWarner interface {
	StartupWarning() string
}

// PermissionModeInfo describes a permission mode for display.
type PermissionModeInfo struct {
	Key    string
	Name   string
	NameZh string
	Desc   string
	DescZh string
}

// BotCommandInfo represents a command for bot menu registration (e.g. Telegram setMyCommands).
type BotCommandInfo struct {
	Command     string // command name without leading "/"
	Description string // short description for the menu
	IsSkill     bool   // whether this entry comes from a skill
}

// CommandRegistrar is an optional interface for platforms that support
// registering commands to the platform's native menu (e.g. Telegram's setMyCommands).
type CommandRegistrar interface {
	RegisterCommands(commands []BotCommandInfo) error
}

// ChannelNameResolver is an optional interface for platforms that can resolve
// channel IDs to human-readable names.
type ChannelNameResolver interface {
	ResolveChannelName(channelID string) (string, error)
}

// StreamingCard represents an active streaming card that aggregates
// an entire agent turn (tool calls, thinking, text) into a single
// updatable message.
type StreamingCard interface {
	// Update replaces the card content with the given markdown.
	// Implementations should throttle calls internally.
	Update(ctx context.Context, content string) error
	// Finalize sends the final content and marks the card as complete.
	Finalize(ctx context.Context, content string) error
	// Failed returns true if the card has entered a failed state.
	Failed() bool
}

// StreamingCardPlatform is an optional interface for platforms that support
// aggregating an entire agent turn into a single updatable card message
// (e.g. DingTalk AI Card). When the engine detects this interface, it
// creates a streaming card at the start of each turn and routes all
// events through it instead of sending individual messages.
type StreamingCardPlatform interface {
	CreateStreamingCard(ctx context.Context, replyCtx any) (StreamingCard, error)
}

// CardStatus represents the visual status of a card header.
type CardStatus string

const (
	CardStatusThinking CardStatus = "thinking" // grey
	CardStatusWorking  CardStatus = "working"  // blue
	CardStatusDone     CardStatus = "done"     // green
	CardStatusError    CardStatus = "error"    // red
)

// PreviewStatusUpdater is an optional interface for platforms that support
// updating the visual status of a preview card header.
type PreviewStatusUpdater interface {
	SetPreviewStatus(previewHandle any, status CardStatus)
}
