// Package moveowebhooks contains types for Moveo.AI webhook requests and
// responses.
//
// Drop this file into your project to type the bodies your webhook
// endpoint receives from Moveo and the responses you send back.
//
// Custom context variables: extend Context by embedding it in your own
// struct, or read them from the Extra map after unmarshaling.
package moveowebhooks

import "encoding/json"

// ---------------------------------------------------------------------------
// Context
// ---------------------------------------------------------------------------

// UserLocation describes the user's reported location. All fields are
// optional and depend on the channel.
type UserLocation struct {
	City      string  `json:"city,omitempty"`
	Country   string  `json:"country,omitempty"`
	Region    string  `json:"region,omitempty"`
	Latitude  float64 `json:"latitude,omitempty"`
	Longitude float64 `json:"longitude,omitempty"`
}

// ContextUser is the user object inside Context. Populated by Moveo from
// the channel the conversation is happening on and enriched with anything
// the agent collects during the conversation.
type ContextUser struct {
	UserID     string `json:"user_id"`
	ExternalID string `json:"external_id,omitempty"`

	DisplayName string `json:"display_name,omitempty"`
	Avatar      string `json:"avatar,omitempty"`
	Email       string `json:"email,omitempty"`
	Phone       string `json:"phone,omitempty"`
	Address     string `json:"address,omitempty"`

	Language string        `json:"language,omitempty"`
	Timezone string        `json:"timezone,omitempty"`
	Location *UserLocation `json:"location,omitempty"`

	Browser  string `json:"browser,omitempty"`
	Platform string `json:"platform,omitempty"`
	Device   string `json:"device,omitempty"`
	IP       string `json:"ip,omitempty"`

	Verified       bool   `json:"verified,omitempty"`
	VerifiedMethod string `json:"verified_method,omitempty"` // jwt | otp | email | qr
	VerifiedAt     string `json:"verified_at,omitempty"`

	PastVisits     int    `json:"past_visits,omitempty"`
	PastChats      int    `json:"past_chats,omitempty"`
	LastSeenAt     string `json:"last_seen_at,omitempty"`
	LastRepliedAt  string `json:"last_replied_at,omitempty"`
}

// CampaignContext is the campaign-scoped subset of Context.
type CampaignContext struct {
	CampaignID   string `json:"campaign_id,omitempty"`
	SubscriberID string `json:"subscriber_id,omitempty"`
	SenderID     string `json:"sender_id,omitempty"`
}

// Context is the full session context.
//
// Custom context variables collected during the conversation appear as
// extra JSON fields. Use Extra to access them, or embed Context in your
// own struct with the additional fields you expect.
//
// The Sys* fields are read only — Moveo rejects any attempt to overwrite
// them in the response.
type Context struct {
	User             *ContextUser               `json:"user,omitempty"`
	Tags             []string                   `json:"tags,omitempty"`
	Channels         map[string]json.RawMessage `json:"channels,omitempty"`
	Global           map[string]json.RawMessage `json:"global,omitempty"`
	Campaign         *CampaignContext           `json:"campaign,omitempty"`
	LiveInstructions json.RawMessage            `json:"live_instructions,omitempty"`

	SysChannel             string `json:"sys-channel,omitempty"`
	SysSession             string `json:"sys-session,omitempty"`
	SysBusiness            string `json:"sys-business,omitempty"` // open | closed
	SysUserMessageCounter  int    `json:"sys-user_message_counter,omitempty"`
	SysUnknownCounter      int    `json:"sys-unknown_counter,omitempty"`

	// Extra holds custom context variables not represented as struct fields.
	// Populate it by calling UnmarshalContext rather than the standard
	// json.Unmarshal — see the function below.
	Extra map[string]json.RawMessage `json:"-"`
}

// UnmarshalContext decodes a JSON context object into c, populating Extra
// with any fields not represented as struct fields.
func UnmarshalContext(data []byte, c *Context) error {
	type alias Context
	if err := json.Unmarshal(data, (*alias)(c)); err != nil {
		return err
	}

	var raw map[string]json.RawMessage
	if err := json.Unmarshal(data, &raw); err != nil {
		return err
	}

	known := map[string]struct{}{
		"user": {}, "tags": {}, "channels": {}, "global": {}, "campaign": {},
		"live_instructions": {}, "sys-channel": {}, "sys-session": {},
		"sys-business": {}, "sys-user_message_counter": {}, "sys-unknown_counter": {},
	}
	for key := range known {
		delete(raw, key)
	}
	c.Extra = raw
	return nil
}

// ---------------------------------------------------------------------------
// Common building blocks
// ---------------------------------------------------------------------------

type MessageAttachment struct {
	Type     string `json:"type"` // image | video | audio | file
	URL      string `json:"url,omitempty"`
	MimeType string `json:"mime_type,omitempty"`
	Filename string `json:"filename,omitempty"`
	Title    string `json:"title,omitempty"`
}

type MessageInput struct {
	Text          string              `json:"text,omitempty"`
	Attachments   []MessageAttachment `json:"attachments,omitempty"`
	TriggerNodeID string              `json:"trigger_node_id,omitempty"`
}

type Intent struct {
	Intent     string  `json:"intent"`
	Confidence float64 `json:"confidence"`
}

type Entity struct {
	Entity     string  `json:"entity"`
	Value      string  `json:"value"`
	Start      int     `json:"start"`
	End        int     `json:"end"`
	Confidence float64 `json:"confidence"`
	Type       string  `json:"type"`
}

type HistoryMessage struct {
	Role      string `json:"role"` // user | assistant
	Content   string `json:"content"`
	Timestamp int64  `json:"timestamp"`
}

type DialogStackEntry struct {
	NodeID string `json:"node_id"`
	Name   string `json:"name"`
}

type Debug struct {
	DialogStack []DialogStackEntry `json:"dialog_stack"`
}

// ---------------------------------------------------------------------------
// Request bodies
// ---------------------------------------------------------------------------

// RequestBase contains the fields common to every webhook request,
// regardless of type. History is only present when the webhook is
// configured with the X-Moveo-Include-History: true custom header.
type RequestBase struct {
	Channel       string           `json:"channel"`
	ChannelType   string           `json:"channel_type"`
	SessionID     string           `json:"session_id"`
	DeskID        string           `json:"desk_id"`
	IntegrationID string           `json:"integration_id"`
	BrainID       string           `json:"brain_id"`
	Lang          string           `json:"lang"`
	Context       Context          `json:"context"`
	Timestamp     int64            `json:"timestamp"`
	History       []HistoryMessage `json:"history,omitempty"`
}

// DialogWebhookRequest is the body of a dialog webhook call (a webhook
// action inside a workflow node).
type DialogWebhookRequest struct {
	RequestBase
	Input              MessageInput `json:"input"`
	Intents            []Intent     `json:"intents"`
	Entities           []Entity     `json:"entities"`
	UserMessageCounter int          `json:"user_message_counter"`
	Debug              Debug        `json:"debug"`
}

// FirstMessageWebhookRequest is the body of a first message webhook call.
// Fires once per session for a given AI Agent on the user's first
// inbound message.
type FirstMessageWebhookRequest struct {
	RequestBase
	Input          MessageInput `json:"input"`
	BusinessClosed bool         `json:"business_closed"`
}

// PreMessageWebhookRequest is the body of a pre message webhook call.
// Fires before every user message is processed by the agent.
type PreMessageWebhookRequest struct {
	RequestBase
	Input          MessageInput `json:"input"`
	BusinessClosed bool         `json:"business_closed"`
}

// PostMessageWebhookRequest is the body of a post message webhook call.
// Fires after the agent has generated its reply, before the reply is
// delivered to the user.
type PostMessageWebhookRequest struct {
	RequestBase
	Input              MessageInput  `json:"input"`
	Intents            []Intent      `json:"intents"`
	Entities           []Entity      `json:"entities"`
	UserMessageCounter int           `json:"user_message_counter"`
	Output             []AgentAction `json:"output"`
	Debug              Debug         `json:"debug"`
}

// ---------------------------------------------------------------------------
// Response actions
// ---------------------------------------------------------------------------

// AgentAction is one entry in the post-message webhook's Output array.
// It covers every action type the agent can produce — including
// agent-internal types (handover, tag, reminder, internal webhook calls)
// that customers cannot return from their own endpoint.
//
// The Type field determines which other fields are populated.
//
// To return an action from your endpoint in DialogWebhookResponse.Responses
// or PostMessageWebhookResponse.Responses, use ResponseAction instead — it
// is the customer-returnable subset.
type AgentAction struct {
	Type     string         `json:"type"`     // text | image | video | audio | file | event | webview | carousel | reset | handover | url | tag | reminder | webhook
	ActionID string         `json:"action_id,omitempty"`
	Metadata map[string]any `json:"metadata,omitempty"`

	// text
	Texts   []string     `json:"texts,omitempty"`
	Options []QuickReply `json:"options,omitempty"`

	// image | video | audio | file | url
	URL  string `json:"url,omitempty"`
	Name string `json:"name,omitempty"`
	Size int    `json:"size,omitempty"`

	// event | reminder | webview
	TriggerNodeID string `json:"trigger_node_id,omitempty"`

	// webview
	Label  string `json:"label,omitempty"`
	Height string `json:"height,omitempty"` // tall | compact | full

	// carousel
	Cards []CarouselCard `json:"cards,omitempty"`

	// handover
	External bool `json:"external,omitempty"`

	// tag
	Tags []string `json:"tags,omitempty"`

	// reminder
	ReminderSeconds int `json:"reminder_seconds,omitempty"`

	// internal webhook call
	WebhookID string        `json:"webhook_id,omitempty"`
	Fallback  []AgentAction `json:"fallback,omitempty"`
}

// ResponseAction is one entry in a Responses array your endpoint sends
// back to Moveo. It is the customer-returnable subset of AgentAction,
// limited to the types the response validator accepts.
type ResponseAction struct {
	Type string `json:"type"` // text | image | video | audio | file | event | webview | carousel | reset

	// text
	Texts   []string     `json:"texts,omitempty"`
	Options []QuickReply `json:"options,omitempty"`

	// image | video | audio | file
	URL  string `json:"url,omitempty"`
	Name string `json:"name,omitempty"`
	Size int    `json:"size,omitempty"`

	// event
	TriggerNodeID string `json:"trigger_node_id,omitempty"`

	// webview
	Label  string `json:"label,omitempty"`
	Height string `json:"height,omitempty"` // tall | compact | full

	// carousel
	Cards    []CarouselCard `json:"cards,omitempty"`
	ActionID string         `json:"action_id,omitempty"`
}

type QuickReply struct {
	Label string `json:"label"`
	Text  string `json:"text"`
}

type CarouselMedia struct {
	Type string `json:"type"` // image | video
	URL  string `json:"url"`
}

// CarouselButton represents a button inside a carousel card. The Type
// field determines which other fields are populated:
//
//	postback: Label, Value
//	url:      Label, URL
//	webview:  Label, URL, Height (optional), TriggerNodeID (optional)
type CarouselButton struct {
	Type          string `json:"type"`
	Label         string `json:"label,omitempty"`
	Value         string `json:"value,omitempty"`
	URL           string `json:"url,omitempty"`
	Height        string `json:"height,omitempty"`
	TriggerNodeID string `json:"trigger_node_id,omitempty"`
}

type CarouselCard struct {
	Title         string           `json:"title"`
	Subtitle      string           `json:"subtitle,omitempty"`
	Media         CarouselMedia    `json:"media"`
	Buttons       []CarouselButton `json:"buttons,omitempty"`
	DefaultAction *CarouselButton  `json:"default_action,omitempty"`
}

// ---------------------------------------------------------------------------
// Response bodies
// ---------------------------------------------------------------------------

// DialogWebhookResponse is what your endpoint returns for a dialog
// webhook. Either Context, Responses, or both. Responses is validated
// against the action schema; invalid entries are skipped.
type DialogWebhookResponse struct {
	Context   map[string]any   `json:"context,omitempty"`
	Responses []ResponseAction `json:"responses,omitempty"`
}

// PreMessageWebhookResponse is what your endpoint returns for a pre
// message webhook. Returns context updates and may rewrite the user
// input before the agent sees it. Responses is not honored by this
// webhook type.
type PreMessageWebhookResponse struct {
	Context map[string]any `json:"context,omitempty"`
	Input   *MessageInput  `json:"input,omitempty"`
}

// FirstMessageWebhookResponse has the same shape as
// PreMessageWebhookResponse.
type FirstMessageWebhookResponse = PreMessageWebhookResponse

// PostMessageWebhookResponse is what your endpoint returns for a post
// message webhook. If Responses is present, it replaces the agent's
// planned reply for this turn.
type PostMessageWebhookResponse struct {
	Context   map[string]any   `json:"context,omitempty"`
	Responses []ResponseAction `json:"responses,omitempty"`
}
