package models

import (
	"database/sql"
	"encoding/json"
	"strings"
	"time"
)

// ── Base ────────────────────────────────────

type BaseModel struct {
	ID        string    `json:"id" db:"id"`
	CreatedAt time.Time `json:"created_at" db:"created_at"`
	UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}

// ── Scope Constants ─────────────────────────

const (
	ScopeGlobal   = "global"
	ScopeTeam     = "team"
	ScopePersonal = "personal"
)

// ── Visibility Constants ────────────────────

const (
	VisibilityVisible = "visible"
	VisibilityHidden  = "hidden"
)

// ── Role Constants ──────────────────────────

const (
	UserRoleUser  = "user"
	UserRoleAdmin = "admin"

	TeamRoleAdmin  = "admin"
	TeamRoleMember = "member"
)

// ── Grant Type Constants ────────────────────

const (
	GrantTypeTool          = "tool"
	GrantTypeKnowledgeBase = "knowledge_base"
	GrantTypeAPIEndpoint   = "api_endpoint"
)

// ── Channel Type Constants ──────────────────

const (
	ChannelTypeDirect  = "direct"
	ChannelTypeGroup   = "group"
	ChannelTypeChannel = "channel"
)

// ── Message Role Constants ──────────────────

const (
	MessageRoleUser      = "user"
	MessageRoleAssistant = "assistant"
	MessageRoleSystem    = "system"
	MessageRoleTool      = "tool"
)

// =========================================
// USERS
// =========================================

type User struct {
	BaseModel
	Username     string     `json:"username" db:"username"`
	Email        string     `json:"email" db:"email"`
	PasswordHash string     `json:"-" db:"password_hash"`
	DisplayName  string     `json:"display_name,omitempty" db:"display_name"`
	AvatarURL    string     `json:"avatar_url,omitempty" db:"avatar_url"`
	Role         string     `json:"role" db:"role"`
	IsActive     bool       `json:"is_active" db:"is_active"`
	Settings     JSONMap    `json:"settings,omitempty" db:"settings"`
	LastLoginAt  *time.Time `json:"last_login_at,omitempty" db:"last_login_at"`
	AuthSource   string     `json:"auth_source" db:"auth_source"`
	ExternalID   *string    `json:"external_id,omitempty" db:"external_id"`
	Handle       string     `json:"handle" db:"handle"`
}

// =========================================
// TEAMS
// =========================================

type Team struct {
	BaseModel
	Name        string  `json:"name" db:"name"`
	Description string  `json:"description,omitempty" db:"description"`
	CreatedBy   string  `json:"created_by" db:"created_by"`
	IsActive    bool    `json:"is_active" db:"is_active"`
	Settings    JSONMap `json:"settings,omitempty" db:"settings"`
	MemberCount int     `json:"member_count,omitempty"` // computed
	MyRole      string  `json:"my_role,omitempty"`      // computed, user context
}

type TeamMember struct {
	ID       string `json:"id" db:"id"`
	TeamID   string `json:"team_id" db:"team_id"`
	UserID   string `json:"user_id" db:"user_id"`
	Role     string `json:"role" db:"role"`
	JoinedAt string `json:"joined_at" db:"joined_at"`
	// Joined fields from users table
	Email       string `json:"email,omitempty"`
	DisplayName string `json:"display_name,omitempty"`
	Username    string `json:"username,omitempty"`
	UserRole    string `json:"user_role,omitempty"`
}

// =========================================
// PROVIDER CONFIGS (replaces APIConfig)
// =========================================

type ProviderConfig struct {
	BaseModel
	Scope        string  `json:"scope" db:"scope"`
	OwnerID      *string `json:"owner_id,omitempty" db:"owner_id"`
	Name         string  `json:"name" db:"name"`
	Provider     string  `json:"provider" db:"provider"`
	Endpoint     string  `json:"endpoint" db:"endpoint"`
	APIKeyEnc    []byte  `json:"-" db:"api_key_enc"`
	KeyNonce     []byte  `json:"-" db:"key_nonce"`
	KeyScope     string  `json:"-" db:"key_scope"`
	ModelDefault string  `json:"model_default,omitempty" db:"model_default"`
	Config       JSONMap `json:"config,omitempty" db:"config"`
	Headers      JSONMap `json:"headers,omitempty" db:"headers"`
	Settings     JSONMap `json:"settings,omitempty" db:"settings"`
	IsActive     bool    `json:"is_active" db:"is_active"`
	IsPrivate    bool    `json:"is_private" db:"is_private"`
	ProxyMode    string  `json:"proxy_mode" db:"proxy_mode"`
	ProxyURL     *string `json:"proxy_url,omitempty" db:"proxy_url"`
}

// HasKey returns true if an encrypted API key is stored.
func (p *ProviderConfig) HasKey() bool {
	return len(p.APIKeyEnc) > 0
}

type ProviderConfigPatch struct {
	Name         *string `json:"name,omitempty"`
	Endpoint     *string `json:"endpoint,omitempty"`
	APIKeyEnc    []byte  `json:"-"`
	KeyNonce     []byte  `json:"-"`
	ModelDefault *string `json:"model_default,omitempty"`
	Config       JSONMap `json:"config,omitempty"`
	Headers      JSONMap `json:"headers,omitempty"`
	Settings     JSONMap `json:"settings,omitempty"`
	IsActive     *bool   `json:"is_active,omitempty"`
	IsPrivate    *bool   `json:"is_private,omitempty"`
	ProxyMode    *string `json:"proxy_mode,omitempty"`
	ProxyURL     *string `json:"proxy_url,omitempty"`
}

// =========================================
// MODEL CATALOG (replaces model_configs)
// =========================================

type CatalogEntry struct {
	BaseModel
	ProviderConfigID string            `json:"provider_config_id" db:"provider_config_id"`
	ModelID          string            `json:"model_id" db:"model_id"`
	DisplayName      string            `json:"display_name,omitempty" db:"display_name"`
	ModelType        string            `json:"model_type" db:"model_type"` // "chat", "embedding", "image", etc.
	Capabilities     ModelCapabilities `json:"capabilities" db:"capabilities"`
	Pricing          *ModelPricing     `json:"pricing,omitempty" db:"pricing"`
	Visibility       string            `json:"visibility" db:"visibility"`
	LastSyncedAt     *time.Time        `json:"last_synced_at,omitempty" db:"last_synced_at"`
}

type ModelCapabilities struct {
	Streaming       bool `json:"streaming"`
	ToolCalling     bool `json:"tool_calling"`
	Vision          bool `json:"vision"`
	Thinking        bool `json:"thinking"`
	Reasoning       bool `json:"reasoning"`
	CodeOptimized   bool `json:"code_optimized"`
	WebSearch       bool `json:"web_search"`
	MaxContext       int  `json:"max_context"`
	MaxOutputTokens int  `json:"max_output_tokens"`
}

func (c ModelCapabilities) HasProviderData() bool {
	return c.ToolCalling || c.Vision || c.Thinking || c.Reasoning ||
		c.CodeOptimized || c.WebSearch || c.MaxContext > 0 || c.MaxOutputTokens > 0
}

type ModelPricing struct {
	InputPerM  float64 `json:"input_per_m,omitempty"`
	OutputPerM float64 `json:"output_per_m,omitempty"`
	Currency   string  `json:"currency,omitempty"`
}

// =========================================
// PERSONAS (replaces ModelPreset)
// =========================================

type Persona struct {
	BaseModel
	Name        string `json:"name" db:"name"`
	Handle      string `json:"handle,omitempty" db:"handle"`
	Description string `json:"description,omitempty" db:"description"`
	Icon        string `json:"icon,omitempty" db:"icon"`
	Avatar      string `json:"avatar,omitempty" db:"avatar"`

	BaseModelID      string  `json:"base_model_id" db:"base_model_id"`
	ProviderConfigID *string `json:"provider_config_id,omitempty" db:"provider_config_id"`

	SystemPrompt   string   `json:"system_prompt,omitempty" db:"system_prompt"`
	Temperature    *float64 `json:"temperature,omitempty" db:"temperature"`
	MaxTokens      *int     `json:"max_tokens,omitempty" db:"max_tokens"`
	ThinkingBudget *int     `json:"thinking_budget,omitempty" db:"thinking_budget"`
	TopP           *float64 `json:"top_p,omitempty" db:"top_p"`

	Scope     string  `json:"scope" db:"scope"`
	OwnerID   *string `json:"owner_id,omitempty" db:"owner_id"`
	CreatedBy string  `json:"created_by" db:"created_by"`

	IsActive bool `json:"is_active" db:"is_active"`
	IsShared bool `json:"is_shared" db:"is_shared"`

	// Loaded from persona_grants, not stored in personas table
	Grants []Grant `json:"grants,omitempty" db:"-"`

	// Loaded from persona_knowledge_bases, not stored in personas table
	KBIDs []string `json:"kb_ids,omitempty" db:"-"`

	// Memory configuration (v0.18.0)
	MemoryEnabled          bool    `json:"memory_enabled" db:"memory_enabled"`
	MemoryExtractionPrompt *string `json:"memory_extraction_prompt,omitempty" db:"memory_extraction_prompt"`
}

type PersonaPatch struct {
	Name             *string  `json:"name,omitempty"`
	Handle           *string  `json:"handle,omitempty"`
	Description      *string  `json:"description,omitempty"`
	Icon             *string  `json:"icon,omitempty"`
	Avatar           *string  `json:"avatar,omitempty"`
	BaseModelID      *string  `json:"base_model_id,omitempty"`
	ProviderConfigID *string  `json:"provider_config_id,omitempty"`
	SystemPrompt     *string  `json:"system_prompt,omitempty"`
	Temperature      *float64 `json:"temperature,omitempty"`
	MaxTokens        *int     `json:"max_tokens,omitempty"`
	ThinkingBudget   *int     `json:"thinking_budget,omitempty"`
	TopP             *float64 `json:"top_p,omitempty"`
	IsActive         *bool    `json:"is_active,omitempty"`
	IsShared         *bool    `json:"is_shared,omitempty"`
	MemoryEnabled          *bool   `json:"memory_enabled,omitempty"`
	MemoryExtractionPrompt *string `json:"memory_extraction_prompt,omitempty"`
}

// =========================================
// GRANTS
// =========================================

type Grant struct {
	ID        string    `json:"id" db:"id"`
	PersonaID string    `json:"persona_id" db:"persona_id"`
	GrantType string    `json:"grant_type" db:"grant_type"`
	GrantRef  string    `json:"grant_ref" db:"grant_ref"`
	Config    JSONMap   `json:"config,omitempty" db:"config"`
	CreatedAt time.Time `json:"created_at" db:"created_at"`
}

// =========================================
// PLATFORM POLICIES
// =========================================

var PolicyDefaults = map[string]string{
	"allow_user_byok":        "false",
	"allow_user_personas":    "false",
	"allow_raw_model_access": "false",
	"allow_registration":     "true",
	"default_user_active":    "false",
	"allow_team_providers":   "true",
	"default_model":          "",
}

// =========================================
// USER MODEL SETTINGS
// =========================================

type UserModelSetting struct {
	BaseModel
	UserID               string   `json:"user_id" db:"user_id"`
	ModelID              string   `json:"model_id" db:"model_id"`
	ProviderConfigID     *string  `json:"provider_config_id,omitempty" db:"provider_config_id"`
	Hidden               bool     `json:"hidden" db:"hidden"`
	PreferredTemperature *float64 `json:"preferred_temperature,omitempty" db:"preferred_temperature"`
	PreferredMaxTokens   *int     `json:"preferred_max_tokens,omitempty" db:"preferred_max_tokens"`
	SortOrder            int      `json:"sort_order" db:"sort_order"`
}

type UserModelSettingPatch struct {
	ProviderConfigID     *string  `json:"provider_config_id,omitempty"`
	Hidden               *bool    `json:"hidden,omitempty"`
	PreferredTemperature *float64 `json:"preferred_temperature,omitempty"`
	PreferredMaxTokens   *int     `json:"preferred_max_tokens,omitempty"`
	SortOrder            *int     `json:"sort_order,omitempty"`
}

// HiddenEntry identifies a model+provider pair for bulk visibility operations.
type HiddenEntry struct {
	ModelID          string `json:"model_id"`
	ProviderConfigID string `json:"provider_config_id"`
}

// CompositeModelKey builds the composite key used for per-provider model preferences.
func CompositeModelKey(providerConfigID, modelID string) string {
	return providerConfigID + ":" + modelID
}

// =========================================
// CHANNELS
// =========================================

type Channel struct {
	BaseModel
	UserID           string  `json:"user_id" db:"user_id"`
	Title            string  `json:"title" db:"title"`
	Description      string  `json:"description,omitempty" db:"description"`
	Type             string  `json:"type" db:"type"`
	Model            string  `json:"model,omitempty" db:"model"`
	SystemPrompt     string  `json:"system_prompt,omitempty" db:"system_prompt"`
	ProviderConfigID *string `json:"provider_config_id,omitempty" db:"provider_config_id"`
	IsArchived       bool       `json:"is_archived" db:"is_archived"`
	IsPinned         bool       `json:"is_pinned" db:"is_pinned"`
	PurgeAfter       *time.Time `json:"purge_after,omitempty" db:"purge_after"`
	FolderID         *string `json:"folder_id,omitempty" db:"folder_id"`
	TeamID           *string `json:"team_id,omitempty" db:"team_id"`
	ProjectID        *string `json:"project_id,omitempty" db:"project_id"`
	WorkspaceID      *string `json:"workspace_id,omitempty" db:"workspace_id"`
	Settings         JSONMap `json:"settings,omitempty" db:"settings"`
	AllowAnonymous   bool    `json:"allow_anonymous" db:"allow_anonymous"` // v0.24.3: session participants can join
}

// =========================================
// SESSION PARTICIPANTS (v0.24.3)
// =========================================

// SessionParticipant is an ephemeral identity for anonymous workflow
// channel visitors. Scoped to a single channel, no users row required.
type SessionParticipant struct {
	ID           string    `json:"id" db:"id"`
	SessionToken string    `json:"session_token" db:"session_token"`
	ChannelID    string    `json:"channel_id" db:"channel_id"`
	DisplayName  string    `json:"display_name" db:"display_name"`
	Fingerprint  string    `json:"fingerprint,omitempty" db:"fingerprint"`
	CreatedAt    time.Time `json:"created_at" db:"created_at"`
}

// =========================================
// MESSAGES
// =========================================

type Message struct {
	BaseModel
	ChannelID        string     `json:"channel_id" db:"channel_id"`
	Role             string     `json:"role" db:"role"`
	Content          string     `json:"content" db:"content"`
	Model            string     `json:"model,omitempty" db:"model"`
	TokensUsed       int        `json:"tokens_used,omitempty" db:"tokens_used"`
	ToolCalls        JSONMap    `json:"tool_calls,omitempty" db:"tool_calls"`
	Metadata         JSONMap    `json:"metadata,omitempty" db:"metadata"`
	ParentID         *string    `json:"parent_id,omitempty" db:"parent_id"`
	SiblingIndex     int        `json:"sibling_index" db:"sibling_index"`
	ParticipantType  string     `json:"participant_type,omitempty" db:"participant_type"`
	ParticipantID    string     `json:"participant_id,omitempty" db:"participant_id"`
	ProviderConfigID *string    `json:"provider_config_id,omitempty" db:"provider_config_id"`
	DeletedAt        *time.Time `json:"deleted_at,omitempty" db:"deleted_at"`
}

// =========================================
// CHANNEL PARTICIPANTS, MODELS, CURSORS
// =========================================

type ChannelParticipant struct {
	ID              string    `json:"id" db:"id"`
	ChannelID       string    `json:"channel_id" db:"channel_id"`
	ParticipantType string    `json:"participant_type" db:"participant_type"`
	ParticipantID   string    `json:"participant_id" db:"participant_id"`
	Role            string    `json:"role" db:"role"`
	DisplayName     *string   `json:"display_name,omitempty" db:"display_name"`
	AvatarURL       *string   `json:"avatar_url,omitempty" db:"avatar_url"`
	JoinedAt        time.Time `json:"joined_at" db:"joined_at"`
	LastReadAt      time.Time `json:"last_read_at" db:"last_read_at"`
}

type ChannelModel struct {
	ID               string  `json:"id" db:"id"`
	ChannelID        string  `json:"channel_id" db:"channel_id"`
	ModelID          string  `json:"model_id" db:"model_id"`
	ProviderConfigID string  `json:"provider_config_id,omitempty" db:"provider_config_id"`
	PersonaID        *string `json:"persona_id,omitempty" db:"persona_id"`
	Handle           string  `json:"handle,omitempty" db:"handle"`
	DisplayName      string  `json:"display_name,omitempty" db:"display_name"`
	SystemPrompt     string  `json:"system_prompt,omitempty" db:"system_prompt"`
	IsDefault        bool    `json:"is_default" db:"is_default"`
}

type ChannelCursor struct {
	ID           string    `json:"id" db:"id"`
	ChannelID    string    `json:"channel_id" db:"channel_id"`
	UserID       string    `json:"user_id" db:"user_id"`
	ActiveLeafID *string   `json:"active_leaf_id,omitempty" db:"active_leaf_id"`
	UpdatedAt    time.Time `json:"updated_at" db:"updated_at"`
}

// =========================================
// PERSONA GROUPS (v0.23.0)
// =========================================

type PersonaGroup struct {
	BaseModel
	Name        string  `json:"name" db:"name"`
	Description string  `json:"description,omitempty" db:"description"`
	OwnerID     string  `json:"owner_id" db:"owner_id"`
	Scope       string  `json:"scope" db:"scope"`
	TeamID      *string `json:"team_id,omitempty" db:"team_id"`
	Members     []PersonaGroupMember `json:"members" db:"-"`
}

type PersonaGroupMember struct {
	ID        string `json:"id" db:"id"`
	GroupID   string `json:"group_id" db:"group_id"`
	PersonaID string `json:"persona_id" db:"persona_id"`
	IsLeader  bool   `json:"is_leader" db:"is_leader"`
	SortOrder int    `json:"sort_order" db:"sort_order"`
	// Joined from personas table for API responses
	PersonaName   string `json:"persona_name,omitempty" db:"-"`
	PersonaHandle string `json:"persona_handle,omitempty" db:"-"`
	PersonaAvatar string `json:"persona_avatar,omitempty" db:"-"`
}

// HandleFromName generates a URL-safe @mention handle from a display name.
// "Veronica Sharpe" → "veronica-sharpe"
func HandleFromName(name string) string {
	h := strings.ToLower(strings.TrimSpace(name))
	h = strings.Map(func(r rune) rune {
		if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' {
			return r
		}
		if r == ' ' || r == '_' {
			return '-'
		}
		return -1
	}, h)
	// Collapse multiple hyphens
	for strings.Contains(h, "--") {
		h = strings.ReplaceAll(h, "--", "-")
	}
	h = strings.Trim(h, "-")
	if len(h) > 50 {
		h = h[:50]
	}
	return h
}

// =========================================
// ORGANIZATION
// =========================================

type Folder struct {
	BaseModel
	UserID    string  `json:"user_id" db:"user_id"`
	Name      string  `json:"name" db:"name"`
	ParentID  *string `json:"parent_id,omitempty" db:"parent_id"`
	SortOrder int     `json:"sort_order" db:"sort_order"`
}

type Project struct {
	BaseModel
	Name        string  `json:"name" db:"name"`
	Description string  `json:"description,omitempty" db:"description"`
	Color       *string `json:"color,omitempty" db:"color"`
	Icon        *string `json:"icon,omitempty" db:"icon"`
	Scope       string  `json:"scope" db:"scope"` // personal, team, global
	OwnerID     string  `json:"owner_id" db:"owner_id"`
	TeamID      *string `json:"team_id,omitempty" db:"team_id"`
	IsArchived  bool    `json:"is_archived" db:"is_archived"`
	WorkspaceID *string `json:"workspace_id,omitempty" db:"workspace_id"`
	Settings    JSONMap `json:"settings,omitempty" db:"settings"`

	// Computed fields (not DB columns)
	ChannelCount int `json:"channel_count,omitempty"`
	KBCount      int `json:"kb_count,omitempty"`
	NoteCount    int `json:"note_count,omitempty"`
}

// ProjectPatch holds optional fields for updating a project.
type ProjectPatch struct {
	Name        *string `json:"name,omitempty"`
	Description *string `json:"description,omitempty"`
	Color       *string `json:"color,omitempty"`
	Icon        *string `json:"icon,omitempty"`
	IsArchived  *bool   `json:"is_archived,omitempty"`
	WorkspaceID *string `json:"workspace_id,omitempty"`
	Settings    JSONMap  `json:"settings,omitempty"`
}

// ProjectChannel represents a channel's membership in a project.
type ProjectChannel struct {
	ProjectID string `json:"project_id" db:"project_id"`
	ChannelID string `json:"channel_id" db:"channel_id"`
	Position  int    `json:"position" db:"position"`
	Folder    string `json:"folder,omitempty" db:"folder"`
	AddedAt   string `json:"added_at" db:"added_at"`
}

// ProjectKB represents a KB's association with a project.
type ProjectKB struct {
	ProjectID  string `json:"project_id" db:"project_id"`
	KBID       string `json:"kb_id" db:"kb_id"`
	AutoSearch bool   `json:"auto_search" db:"auto_search"`
	AddedAt    string `json:"added_at" db:"added_at"`
	Name       string `json:"name,omitempty" db:"name"` // enriched via JOIN
}

// ProjectNote represents a note's association with a project.
type ProjectNote struct {
	ProjectID string `json:"project_id" db:"project_id"`
	NoteID    string `json:"note_id" db:"note_id"`
	AddedAt   string `json:"added_at" db:"added_at"`
	Title     string `json:"title,omitempty" db:"title"` // enriched via JOIN
}

// =========================================
// NOTES
// =========================================

type Note struct {
	BaseModel
	UserID          string   `json:"user_id" db:"user_id"`
	Title           string   `json:"title" db:"title"`
	Content         string   `json:"content" db:"content"`
	FolderPath      string   `json:"folder_path" db:"folder_path"`
	Tags            []string `json:"tags,omitempty" db:"tags"`
	Metadata        JSONMap  `json:"metadata,omitempty" db:"metadata"`
	SourceChannelID *string  `json:"source_channel_id,omitempty" db:"source_channel_id"`
	SourceMessageID *string  `json:"source_message_id,omitempty" db:"source_message_id"`
	TeamID          *string  `json:"team_id,omitempty" db:"team_id"`
}

// NoteLink represents a directed link extracted from [[wikilink]] syntax.
type NoteLink struct {
	TargetNoteID   *string `json:"target_note_id,omitempty"`
	TargetTitle    string  `json:"target_title"`
	DisplayText    string  `json:"display_text,omitempty"`
	IsTransclusion bool    `json:"is_transclusion"`
}

// ExportNoteLink is a note_link with source_note_id included (for export).
type ExportNoteLink struct {
	SourceNoteID   string  `json:"source_note_id" db:"source_note_id"`
	TargetNoteID   *string `json:"target_note_id,omitempty" db:"target_note_id"`
	TargetTitle    string  `json:"target_title" db:"target_title"`
	DisplayText    string  `json:"display_text,omitempty" db:"display_text"`
	IsTransclusion bool    `json:"is_transclusion" db:"is_transclusion"`
}

// NoteLinkResult represents a backlink — a note that links to a given note.
type NoteLinkResult struct {
	SourceNoteID string    `json:"id"`
	Title        string    `json:"title"`
	FolderPath   string    `json:"folder_path"`
	UpdatedAt    time.Time `json:"updated_at"`
	DisplayText  string    `json:"display_text,omitempty"`
}

// NoteGraphNode is a lightweight note representation for graph display.
type NoteGraphNode struct {
	ID         string   `json:"id"`
	Title      string   `json:"title"`
	FolderPath string   `json:"folder_path"`
	Tags       []string `json:"tags"`
	UpdatedAt  string   `json:"updated_at"`
	LinkCount  int      `json:"link_count"`
}

// NoteGraphEdge is a resolved link between two notes.
type NoteGraphEdge struct {
	Source         string `json:"source"`
	Target         string `json:"target"`
	Title          string `json:"title"`
	IsTransclusion bool   `json:"is_transclusion"`
}

// NoteGraphDangling is an unresolved [[link]] reference.
type NoteGraphDangling struct {
	Source string `json:"source"`
	Title  string `json:"title"`
}

// NoteGraph is the full graph topology for a user's notes.
type NoteGraph struct {
	Nodes      []NoteGraphNode    `json:"nodes"`
	Edges      []NoteGraphEdge    `json:"edges"`
	Unresolved []NoteGraphDangling `json:"unresolved"`
}

// =========================================
// ATTACHMENTS
// =========================================

// =========================================
// FILES
// =========================================

// FileOrigin constants
const (
	FileOriginUserUpload = "user_upload"
	FileOriginToolOutput = "tool_output"
	FileOriginSystem     = "system"
)

// FileDisplayHint constants
const (
	FileHintInline    = "inline"
	FileHintDownload  = "download"
	FileHintThumbnail = "thumbnail"
)

type File struct {
	ID            string    `json:"id" db:"id"`
	ChannelID     string    `json:"channel_id" db:"channel_id"`
	MessageID     *string   `json:"message_id,omitempty" db:"message_id"`
	UserID        string    `json:"user_id" db:"user_id"`
	ProjectID     *string   `json:"project_id,omitempty" db:"project_id"`
	Origin        string    `json:"origin" db:"origin"`         // user_upload, tool_output, system
	Filename      string    `json:"filename" db:"filename"`
	ContentType   string    `json:"content_type" db:"content_type"`
	SizeBytes     int64     `json:"size_bytes" db:"size_bytes"`
	StorageKey    string    `json:"-" db:"storage_key"`         // never expose filesystem path
	DisplayHint   string    `json:"display_hint" db:"display_hint"` // inline, download, thumbnail
	ExtractedText *string   `json:"extracted_text,omitempty" db:"extracted_text"`
	Metadata      JSONMap   `json:"metadata,omitempty" db:"metadata"`
	CreatedAt     time.Time `json:"created_at" db:"created_at"`
	UpdatedAt     time.Time `json:"updated_at" db:"updated_at"`
}



// =========================================
// AUDIT LOG
// =========================================

type AuditEntry struct {
	ID           string    `json:"id" db:"id"`
	ActorID      *string   `json:"actor_id,omitempty" db:"actor_id"`
	ActorName    *string   `json:"actor_name,omitempty" db:"-"` // resolved via JOIN, not a column
	Action       string    `json:"action" db:"action"`
	ResourceType string    `json:"resource_type" db:"resource_type"`
	ResourceID   string    `json:"resource_id,omitempty" db:"resource_id"`
	Metadata     JSONMap   `json:"metadata,omitempty" db:"metadata"`
	IPAddress    string    `json:"ip_address,omitempty" db:"ip_address"`
	UserAgent    string    `json:"user_agent,omitempty" db:"user_agent"`
	CreatedAt    time.Time `json:"created_at" db:"created_at"`
}

// =========================================
// USAGE TRACKING
// =========================================

type UsageEntry struct {
	ID                  string    `json:"id" db:"id"`
	ChannelID           *string   `json:"channel_id,omitempty" db:"channel_id"`
	UserID              string    `json:"user_id" db:"user_id"`
	ProviderConfigID    *string   `json:"provider_config_id,omitempty" db:"provider_config_id"`
	ProviderScope       string    `json:"provider_scope" db:"provider_scope"`
	ModelID             string    `json:"model_id" db:"model_id"`
	Role                *string   `json:"role,omitempty" db:"role"` // null=chat, "utility", "embedding"
	PromptTokens        int       `json:"prompt_tokens" db:"prompt_tokens"`
	CompletionTokens    int       `json:"completion_tokens" db:"completion_tokens"`
	CacheCreationTokens int       `json:"cache_creation_tokens" db:"cache_creation_tokens"`
	CacheReadTokens     int       `json:"cache_read_tokens" db:"cache_read_tokens"`
	CostInput           *float64  `json:"cost_input,omitempty" db:"cost_input"`
	CostOutput          *float64  `json:"cost_output,omitempty" db:"cost_output"`
	RoutingDecision     JSONMap    `json:"routing_decision,omitempty" db:"routing_decision"`
	CreatedAt           time.Time `json:"created_at" db:"created_at"`
}

type UsageAggregate struct {
	GroupKey     string  `json:"group_key"`
	Label        string  `json:"label"`
	Requests     int     `json:"requests"`
	InputTokens  int     `json:"input_tokens"`
	OutputTokens int     `json:"output_tokens"`
	TotalCost    float64 `json:"total_cost"`
}

type UsageTotals struct {
	Requests     int     `json:"requests"`
	InputTokens  int     `json:"input_tokens"`
	OutputTokens int     `json:"output_tokens"`
	TotalCost    float64 `json:"total_cost"`
	Period       string  `json:"period"`
}

// =========================================
// MODEL PRICING
// =========================================

type PricingEntry struct {
	ID               string    `json:"id" db:"id"`
	ProviderConfigID string    `json:"provider_config_id" db:"provider_config_id"`
	ModelID          string    `json:"model_id" db:"model_id"`
	InputPerM        *float64  `json:"input_per_m" db:"input_per_m"`
	OutputPerM       *float64  `json:"output_per_m" db:"output_per_m"`
	CacheCreatePerM  *float64  `json:"cache_create_per_m" db:"cache_create_per_m"`
	CacheReadPerM    *float64  `json:"cache_read_per_m" db:"cache_read_per_m"`
	Currency         string    `json:"currency" db:"currency"`
	Source           string    `json:"source" db:"source"` // "catalog" | "manual"
	UpdatedAt        time.Time `json:"updated_at" db:"updated_at"`
	UpdatedBy        *string   `json:"updated_by,omitempty" db:"updated_by"`
}

// =========================================
// VIEW MODELS (computed, not stored)
// =========================================

// UserModel is the view model returned by the capability resolver.
// Combines catalog entries + Personas for the frontend.
type UserModel struct {
	ID          string `json:"id"`
	DisplayName string `json:"display_name"`
	ModelID     string `json:"model_id"`
	ModelType   string `json:"model_type"` // "chat", "embedding", "image", etc.
	Source      string `json:"source"`     // "catalog", "persona", "live"

	ProviderConfigID string `json:"provider_config_id"`
	ConfigID         string `json:"config_id"` // Alias of ProviderConfigID for frontend compat
	ProviderName     string `json:"provider_name"`
	ProviderType     string `json:"provider_type"`

	Capabilities ModelCapabilities `json:"capabilities"`

	// Persona fields — always emitted so frontend can branch on is_persona.
	IsPersona       bool   `json:"is_persona"`
	PersonaID       string `json:"persona_id,omitempty"`
	PersonaHandle   string `json:"persona_handle,omitempty"`
	PersonaScope    string `json:"persona_scope,omitempty"`
	PersonaAvatar   string `json:"persona_avatar,omitempty"`
	PersonaTeamName string `json:"persona_team_name,omitempty"`

	Description  string   `json:"description,omitempty"`
	Icon         string   `json:"icon,omitempty"`
	Avatar       string   `json:"avatar,omitempty"`
	SystemPrompt string   `json:"system_prompt,omitempty"`
	Temperature  *float64 `json:"temperature,omitempty"`
	MaxTokens    *int     `json:"max_tokens,omitempty"`
	ToolGrants   []string `json:"tool_grants,omitempty"`

	Pricing *ModelPricing `json:"pricing,omitempty"`

	Scope    string  `json:"scope"`
	OwnerID  *string `json:"owner_id,omitempty"`
	TeamName string  `json:"team_name,omitempty"`

	Hidden    bool `json:"hidden"`
	SortOrder int  `json:"sort_order"`

	ProviderStatus ProviderStatus `json:"provider_status,omitempty"` // health status from provider health (v0.22.3)
}

// =========================================
// JSON HELPERS
// =========================================

// JSONMap scans from/to JSONB columns.
type JSONMap map[string]interface{}

func (m *JSONMap) Scan(src interface{}) error {
	if src == nil {
		*m = nil
		return nil
	}
	var source []byte
	switch v := src.(type) {
	case []byte:
		source = v
	case string:
		source = []byte(v)
	default:
		return nil
	}
	result := make(JSONMap)
	if err := json.Unmarshal(source, &result); err != nil {
		return err
	}
	*m = result
	return nil
}

// ── Extensions ──────────────────────────────

// Extension tier constants
const (
	ExtTierBrowser  = "browser"
	ExtTierStarlark = "starlark"
	ExtTierSidecar  = "sidecar"
)

// Extension represents an installed extension in the registry.
type Extension struct {
	ID          string          `json:"id" db:"id"`
	ExtID       string          `json:"ext_id" db:"ext_id"`           // manifest id
	Name        string          `json:"name" db:"name"`
	Version     string          `json:"version" db:"version"`
	Tier        string          `json:"tier" db:"tier"`
	Description string          `json:"description" db:"description"`
	Author      string          `json:"author" db:"author"`
	Manifest    json.RawMessage `json:"manifest" db:"manifest"`
	IsSystem    bool            `json:"is_system" db:"is_system"`
	IsEnabled   bool            `json:"is_enabled" db:"is_enabled"`
	Scope       string          `json:"scope" db:"scope"`
	TeamID      *string         `json:"team_id,omitempty" db:"team_id"`
	InstalledBy *string         `json:"installed_by,omitempty" db:"installed_by"`
	CreatedAt   time.Time       `json:"created_at" db:"created_at"`
	UpdatedAt   time.Time       `json:"updated_at" db:"updated_at"`
}

// ExtensionUserSettings stores per-user overrides for an extension.
type ExtensionUserSettings struct {
	ExtensionID string          `json:"extension_id" db:"extension_id"`
	UserID      string          `json:"user_id" db:"user_id"`
	Settings    json.RawMessage `json:"settings" db:"settings"`
	IsEnabled   bool            `json:"is_enabled" db:"is_enabled"`
}

// UserExtension combines extension info with user-specific settings for API responses.
type UserExtension struct {
	Extension
	UserEnabled  *bool            `json:"user_enabled,omitempty"`
	UserSettings *json.RawMessage `json:"user_settings,omitempty"`
}

func NullString(s *string) sql.NullString {
	if s == nil {
		return sql.NullString{}
	}
	return sql.NullString{String: *s, Valid: true}
}

func NullFloat(f *float64) sql.NullFloat64 {
	if f == nil {
		return sql.NullFloat64{}
	}
	return sql.NullFloat64{Float64: *f, Valid: true}
}

func NullInt(i *int) sql.NullInt64 {
	if i == nil {
		return sql.NullInt64{}
	}
	return sql.NullInt64{Int64: int64(*i), Valid: true}
}

func StringPtr(s string) *string    { return &s }
func Float64Ptr(f float64) *float64 { return &f }
func IntPtr(i int) *int             { return &i }
func BoolPtr(b bool) *bool          { return &b }

// =========================================
// GROUPS (v0.16.0)
// =========================================

// Group scope constants (reuses ScopeGlobal, ScopeTeam from above)

// ResourceType constants for resource_grants
const (
	ResourceTypePersona       = "persona"
	ResourceTypeKnowledgeBase = "knowledge_base"
)

// GrantScope constants for resource_grants
const (
	GrantScopeTeamOnly = "team_only"
	GrantScopeGlobal   = "global"
	GrantScopeGroups   = "groups"
)

// Group is an access-control list. Decouples resource visibility from teams.
type Group struct {
	BaseModel
	Name               string   `json:"name" db:"name"`
	Description        string   `json:"description" db:"description"`
	Scope              string   `json:"scope" db:"scope"`      // global, team
	TeamID             *string  `json:"team_id,omitempty" db:"team_id"`
	CreatedBy          *string  `json:"created_by,omitempty" db:"created_by"`
	Source             string   `json:"source" db:"source"`    // manual (default), oidc, system
	Permissions        []string `json:"permissions" db:"permissions"`
	TokenBudgetDaily   *int64   `json:"token_budget_daily,omitempty" db:"token_budget_daily"`
	TokenBudgetMonthly *int64   `json:"token_budget_monthly,omitempty" db:"token_budget_monthly"`
	AllowedModels      []string `json:"allowed_models,omitempty" db:"allowed_models"` // nil = unrestricted
	MemberCount        int      `json:"member_count,omitempty"` // computed, not a DB column
}

// GroupPatch holds optional fields for updating a group.
type GroupPatch struct {
	Name               *string   `json:"name,omitempty"`
	Description        *string   `json:"description,omitempty"`
	Permissions        *[]string `json:"permissions,omitempty"`
	TokenBudgetDaily   *int64    `json:"token_budget_daily,omitempty"`
	TokenBudgetMonthly *int64    `json:"token_budget_monthly,omitempty"`
	AllowedModels      *[]string `json:"allowed_models,omitempty"` // &[]string{} = restrict to none; nil = no change
	ClearBudgetDaily   bool      `json:"clear_budget_daily,omitempty"`
	ClearBudgetMonthly bool      `json:"clear_budget_monthly,omitempty"`
	ClearAllowedModels bool      `json:"clear_allowed_models,omitempty"`
}

// GroupMember links a user to a group.
type GroupMember struct {
	ID      string    `json:"id" db:"id"`
	GroupID string    `json:"group_id" db:"group_id"`
	UserID  string    `json:"user_id" db:"user_id"`
	AddedBy string    `json:"added_by" db:"added_by"`
	AddedAt time.Time `json:"added_at" db:"added_at"`
	// Joined fields from users table (for list responses)
	Username    string `json:"username,omitempty"`
	Email       string `json:"email,omitempty"`
	DisplayName string `json:"display_name,omitempty"`
}

// ResourceGrant controls who can USE a resource (personas, KBs) via groups.
// One row per resource. Not to be confused with persona_grants (what a Persona can DO).
type ResourceGrant struct {
	BaseModel
	ResourceType  string   `json:"resource_type" db:"resource_type"`
	ResourceID    string   `json:"resource_id" db:"resource_id"`
	GrantScope    string   `json:"grant_scope" db:"grant_scope"`
	GrantedGroups []string `json:"granted_groups" db:"granted_groups"` // UUID array
	CreatedBy     string   `json:"created_by" db:"created_by"`
}

// ── Knowledge Bases ────────────────────────────

// KnowledgeBase is a named collection of documents with vector embeddings.
type KnowledgeBase struct {
	ID              string    `json:"id" db:"id"`
	Name            string    `json:"name" db:"name"`
	Description     string    `json:"description" db:"description"`
	Scope           string    `json:"scope" db:"scope"` // global, team, personal
	OwnerID         *string   `json:"owner_id,omitempty" db:"owner_id"`
	TeamID          *string   `json:"team_id,omitempty" db:"team_id"`
	EmbeddingConfig JSONMap   `json:"embedding_config" db:"embedding_config"`
	DocumentCount   int       `json:"document_count" db:"document_count"`
	ChunkCount      int       `json:"chunk_count" db:"chunk_count"`
	TotalBytes      int64     `json:"total_bytes" db:"total_bytes"`
	Discoverable    bool      `json:"discoverable" db:"discoverable"`
	Status          string    `json:"status" db:"status"` // active, processing, error
	CreatedAt       time.Time `json:"created_at" db:"created_at"`
	UpdatedAt       time.Time `json:"updated_at" db:"updated_at"`
}

// PersonaKB binds a knowledge base to a persona.
type PersonaKB struct {
	PersonaID  string    `json:"persona_id" db:"persona_id"`
	KBID       string    `json:"kb_id" db:"kb_id"`
	AutoSearch bool      `json:"auto_search" db:"auto_search"`
	AddedAt    time.Time `json:"added_at" db:"added_at"`

	// Joined fields (not in persona_knowledge_bases table)
	KBName        string `json:"kb_name,omitempty" db:"kb_name"`
	DocumentCount int    `json:"document_count,omitempty" db:"document_count"`
	ChunkCount    int    `json:"chunk_count,omitempty" db:"chunk_count"`
}

// KBDocument is a single uploaded file within a knowledge base.
type KBDocument struct {
	ID            string    `json:"id" db:"id"`
	KBID          string    `json:"kb_id" db:"kb_id"`
	Filename      string    `json:"filename" db:"filename"`
	ContentType   string    `json:"content_type" db:"content_type"`
	SizeBytes     int64     `json:"size_bytes" db:"size_bytes"`
	StorageKey    string    `json:"storage_key" db:"storage_key"`
	ExtractedText *string   `json:"-" db:"extracted_text"`
	ChunkCount    int       `json:"chunk_count" db:"chunk_count"`
	Status        string    `json:"status" db:"status"` // pending, chunking, embedding, ready, error
	Error         *string   `json:"error,omitempty" db:"error"`
	UploadedBy    string    `json:"uploaded_by" db:"uploaded_by"`
	CreatedAt     time.Time `json:"created_at" db:"created_at"`
	UpdatedAt     time.Time `json:"updated_at" db:"updated_at"`
}

// KBChunk is a text segment from a document with its embedding vector.
type KBChunk struct {
	ID          string    `json:"id" db:"id"`
	KBID        string    `json:"kb_id" db:"kb_id"`
	DocumentID  string    `json:"document_id" db:"document_id"`
	ChunkIndex  int       `json:"chunk_index" db:"chunk_index"`
	Content     string    `json:"content" db:"content"`
	TokenCount  int       `json:"token_count" db:"token_count"`
	Embedding   []float64 `json:"-" db:"embedding"` // never serialized to API
	Metadata    JSONMap   `json:"metadata" db:"metadata"`
	CreatedAt   time.Time `json:"created_at" db:"created_at"`
}

// KBSearchResult is a single result from similarity search.
type KBSearchResult struct {
	Content    string  `json:"content"`
	Filename   string  `json:"source"`
	KBName     string  `json:"kb"`
	Similarity float64 `json:"similarity"`
	Metadata   JSONMap  `json:"metadata,omitempty"`
}

// ChannelKB represents a knowledge base linked to a channel.
type ChannelKB struct {
	KBID          string `json:"kb_id" db:"kb_id"`
	KBName        string `json:"kb_name" db:"name"`
	Enabled       bool   `json:"enabled" db:"enabled"`
	DocumentCount int    `json:"document_count" db:"document_count"`
}

// =========================================
// PROVIDER HEALTH (v0.22.0)
// =========================================

// ProviderStatus represents the derived health state.
type ProviderStatus string

const (
	StatusHealthy  ProviderStatus = "healthy"
	StatusDegraded ProviderStatus = "degraded"
	StatusDown     ProviderStatus = "down"
	StatusUnknown  ProviderStatus = "unknown"
)

// ProviderHealthWindow is one hourly bucket of health metrics.
type ProviderHealthWindow struct {
	ID               string    `json:"id" db:"id"`
	ProviderConfigID string    `json:"provider_config_id" db:"provider_config_id"`
	WindowStart      time.Time `json:"window_start" db:"window_start"`
	RequestCount     int       `json:"request_count" db:"request_count"`
	ErrorCount       int       `json:"error_count" db:"error_count"`
	TimeoutCount     int       `json:"timeout_count" db:"timeout_count"`
	RateLimitCount   int       `json:"rate_limit_count" db:"rate_limit_count"` // v0.22.4
	TotalLatencyMs   int64     `json:"total_latency_ms" db:"total_latency_ms"`
	MaxLatencyMs     int       `json:"max_latency_ms" db:"max_latency_ms"`
	LastError        *string   `json:"last_error,omitempty" db:"last_error"`
	LastErrorAt      *string   `json:"last_error_at,omitempty" db:"last_error_at"`
}

// AvgLatencyMs returns the average latency, or 0 if no requests.
func (w *ProviderHealthWindow) AvgLatencyMs() int {
	if w.RequestCount == 0 {
		return 0
	}
	return int(w.TotalLatencyMs / int64(w.RequestCount))
}

// ErrorRate returns the error fraction, or 0 if no requests.
func (w *ProviderHealthWindow) ErrorRate() float64 {
	if w.RequestCount == 0 {
		return 0
	}
	return float64(w.ErrorCount) / float64(w.RequestCount)
}

// ProviderHealthSummary is the API response for a provider's current health.
type ProviderHealthSummary struct {
	ProviderConfigID string         `json:"provider_config_id"`
	ProviderName     string         `json:"provider_name,omitempty"`
	Status           ProviderStatus `json:"status"`
	RequestCount     int            `json:"request_count"`      // last hour
	ErrorRate        float64        `json:"error_rate"`         // last hour
	ErrorCount       int            `json:"error_count"`        // last hour
	RateLimitCount   int            `json:"rate_limit_count"`   // last hour (v0.22.4)
	TimeoutCount     int            `json:"timeout_count"`      // last hour
	AvgLatencyMs     int            `json:"avg_latency_ms"`     // last hour
	MaxLatencyMs     int            `json:"max_latency_ms"`     // last hour
	LastError        *string        `json:"last_error,omitempty"`
	LastErrorAt      *string        `json:"last_error_at,omitempty"`
}

// ToolHealthWindow tracks health of built-in tools (web_search, url_fetch, etc.)
// in hourly buckets, analogous to ProviderHealthWindow.
type ToolHealthWindow struct {
	ID             string    `json:"id" db:"id"`
	ToolName       string    `json:"tool_name" db:"tool_name"`
	WindowStart    time.Time `json:"window_start" db:"window_start"`
	RequestCount   int       `json:"request_count" db:"request_count"`
	ErrorCount     int       `json:"error_count" db:"error_count"`
	TotalLatencyMs int64     `json:"total_latency_ms" db:"total_latency_ms"`
	MaxLatencyMs   int       `json:"max_latency_ms" db:"max_latency_ms"`
	LastError      *string   `json:"last_error,omitempty" db:"last_error"`
	LastErrorAt    *string   `json:"last_error_at,omitempty" db:"last_error_at"`
}

// ToolHealthSummary is the API response for a tool's health.
type ToolHealthSummary struct {
	ToolName     string  `json:"tool_name"`
	Status       string  `json:"status"` // healthy, degraded, down, unknown
	RequestCount int     `json:"request_count"`
	ErrorRate    float64 `json:"error_rate"`
	AvgLatencyMs int     `json:"avg_latency_ms"`
}

// =========================================
// CAPABILITY OVERRIDES (v0.22.0)
// =========================================

// CapabilityOverride is an admin correction for a model's capabilities.
type CapabilityOverride struct {
	ID               string  `json:"id" db:"id"`
	ProviderConfigID *string `json:"provider_config_id,omitempty" db:"provider_config_id"`
	ModelID          string  `json:"model_id" db:"model_id"`
	Field            string  `json:"field" db:"field"`
	Value            string  `json:"value" db:"value"`
	SetBy            *string `json:"set_by,omitempty" db:"set_by"`
	CreatedAt        string  `json:"created_at" db:"created_at"`
}

// =========================================
// ROUTING POLICIES (v0.22.2)
// =========================================

// RoutingPolicy is one routing rule that controls how requests are
// dispatched to provider configs. Stored in the routing_policies table.
type RoutingPolicy struct {
	ID        string    `json:"id" db:"id"`
	Name      string    `json:"name" db:"name"`
	Scope     string    `json:"scope" db:"scope"`            // "global" or "team"
	TeamID    *string   `json:"team_id,omitempty" db:"team_id"`
	Priority  int       `json:"priority" db:"priority"`      // lower = first
	Type      string    `json:"policy_type" db:"policy_type"` // provider_prefer, team_route, cost_limit, model_alias
	Config    JSONMap   `json:"config" db:"config"`           // type-specific JSONB
	IsActive  bool      `json:"is_active" db:"is_active"`
	CreatedAt time.Time `json:"created_at" db:"created_at"`
	UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}

