package store

import (
	"context"
	"encoding/json"
	"errors"
	"time"

	"chat-switchboard/models"
)

// ── Sentinel Errors ─────────────────────────

// ErrSystemGroup is returned when attempting to delete a system-sourced group.
var ErrSystemGroup = errors.New("system groups cannot be deleted")

// =========================================
// STORES — Data Access Layer
// =========================================
// Every database operation goes through these interfaces.
// Handlers never touch SQL directly. This makes the DB
// portable (Postgres today, SQLite/MySQL later) and
// handlers testable with in-memory implementations.
// =========================================

// Stores bundles all store interfaces for dependency injection.
type Stores struct {
	Providers     ProviderStore
	Catalog       CatalogStore
	Personas      PersonaStore
	Policies      PolicyStore
	UserSettings  UserModelSettingsStore
	Users         UserStore
	Teams         TeamStore
	Channels      ChannelStore
	Messages      MessageStore
	Audit         AuditStore
	Notes         NoteStore
	NoteLinks     NoteLinkStore
	GlobalConfig  GlobalConfigStore
	Usage         UsageStore
	Pricing       PricingStore
	Files         FileStore
	KnowledgeBases KnowledgeBaseStore
	Groups         GroupStore
	ResourceGrants ResourceGrantStore
	Memories       MemoryStore
	Projects       ProjectStore
	Notifications  NotificationStore
	NotifPrefs     NotificationPreferenceStore
	Workspaces     WorkspaceStore
	GitCredentials GitCredentialStore
	CapOverrides   CapabilityOverrideStore
	RoutingPolicies RoutingPolicyStore
	Sessions        SessionStore
	Packages        PackageStore           // v0.28.7: Unified package registry (surfaces + extensions)
	Workflows       WorkflowStore          // v0.26.1: Workflow definitions + stages
	Tasks           TaskStore              // v0.27.1: Task scheduling + run history
	Presence        PresenceStore          // v0.29.0: User online/offline status
	PersonaGroups   PersonaGroupStore      // v0.29.0: Persona group (roster template) CRUD
	Folders         FolderStore            // v0.29.0: Chat folder CRUD
	Health          HealthStore            // v0.29.0: Provider health window management
	ExtPermissions  ExtensionPermissionStore // v0.29.0: Extension declared/granted capabilities
	ExtData         ExtDataStore             // v0.29.2: Extension namespaced table catalog
	Tickets         TicketStore              // v0.32.0: WS auth tickets (PG-backed for cross-pod)
	RateLimits      RateLimitStore           // v0.32.0: Distributed rate limiting
	Export          ExportStore              // v0.34.0: Data portability batch reads + GDPR ops
}

// =========================================
// PROVIDER STORE
// =========================================

type ProviderStore interface {
	Create(ctx context.Context, cfg *models.ProviderConfig) error
	GetByID(ctx context.Context, id string) (*models.ProviderConfig, error)
	Update(ctx context.Context, id string, patch models.ProviderConfigPatch) error
	Delete(ctx context.Context, id string) error

	// Scoped queries
	ListGlobal(ctx context.Context) ([]models.ProviderConfig, error)
	ListForTeam(ctx context.Context, teamID string) ([]models.ProviderConfig, error)
	ListForUser(ctx context.Context, userID string) ([]models.ProviderConfig, error) // personal scope
	ListAccessible(ctx context.Context, userID string) ([]models.ProviderConfig, error) // all user can access

	// Access check
	UserCanAccess(ctx context.Context, userID, configID string) (bool, error)

	// ── CS4 additions (v0.29.0) ──

	// DeletePersonalByOwner removes all personal-scope provider configs for a user.
	DeletePersonalByOwner(ctx context.Context, ownerID string) (int64, error)

	// ── CS6 additions (v0.29.0) ──

	// ListAllForTeam returns all provider configs (including inactive) for a team.
	ListAllForTeam(ctx context.Context, teamID string) ([]models.ProviderConfig, error)

	// DeleteByIDAndTeam deletes a team-scoped provider by ID and team. Returns rows affected.
	DeleteByIDAndTeam(ctx context.Context, id, teamID string) (int64, error)

	// ── CS7a additions (v0.29.0) ──

	// FindFirstForUser returns the ID of the first active provider config
	// accessible to a user (personal first, then global; excludes team).
	// Returns sql.ErrNoRows if none found.
	FindFirstForUser(ctx context.Context, userID string) (string, error)

	// LoadAccessible loads a provider config by ID, verifying the user has access
	// (global, personal owned by user, or team the user belongs to).
	// Returns sql.ErrNoRows if not found or not accessible.
	LoadAccessible(ctx context.Context, configID, userID string) (*models.ProviderConfig, error)
}

// =========================================
// CATALOG STORE
// =========================================

type CatalogStore interface {
	// Sync from provider API
	UpsertFromSync(ctx context.Context, providerConfigID string, entries []CatalogSyncEntry) (added, updated int, err error)

	// Queries
	GetByID(ctx context.Context, id string) (*models.CatalogEntry, error)
	GetByModelID(ctx context.Context, providerConfigID, modelID string) (*models.CatalogEntry, error)
	GetByModelIDAny(ctx context.Context, modelID string) (*models.CatalogEntry, error) // any provider, most recently synced
	ListVisible(ctx context.Context) ([]models.CatalogEntry, error)
	ListEnabledForProvider(ctx context.Context, providerConfigID string) ([]models.CatalogEntry, error)
	ListForProvider(ctx context.Context, providerConfigID string) ([]models.CatalogEntry, error)
	ListAll(ctx context.Context) ([]models.CatalogEntry, error)       // everything (internal use)
	ListAllGlobal(ctx context.Context) ([]models.CatalogEntry, error) // admin view — global-scope providers only

	// Visibility management
	SetVisibility(ctx context.Context, id string, visibility string) error
	BulkSetVisibility(ctx context.Context, providerConfigID string, visibility string) error
	BulkSetVisibilityAll(ctx context.Context, visibility string) error

	// Delete
	Delete(ctx context.Context, id string) error
	DeleteForProvider(ctx context.Context, providerConfigID string) error

	// ── Mention resolution (v0.29.0 — moved from handler raw SQL) ──

	// FindEnabledByModelID finds an enabled catalog entry by exact model_id
	// (case-insensitive). Returns (modelID, providerConfigID, nil).
	// Prefers global > team > personal scope providers.
	FindEnabledByModelID(ctx context.Context, modelID string) (string, string, error)

	// FindEnabledByModelIDPrefix finds by unambiguous model_id prefix.
	// Returns (modelID, configID, matchCount, nil).
	FindEnabledByModelIDPrefix(ctx context.Context, prefix string) (string, string, int, error)

	// ── CS2 additions (v0.29.0) ──

	// GetCapabilities returns the raw capabilities JSON for a specific provider+model.
	GetCapabilities(ctx context.Context, modelID, configID string) ([]byte, error)

	// GetCapabilitiesAny returns capabilities for a model from any provider (most recent sync).
	GetCapabilitiesAny(ctx context.Context, modelID string) ([]byte, error)

	// ── CS6 additions (v0.29.0) ──

	// ListTeamAvailable returns catalog entries with visibility 'enabled' or 'team'
	// from active global providers. Used by team model selector.
	ListTeamAvailable(ctx context.Context) ([]TeamAvailableModel, error)
}

// TeamAvailableModel is returned by CatalogStore.ListTeamAvailable.
type TeamAvailableModel struct {
	ID           string
	ModelID      string
	DisplayName  *string
	Visibility   string
	Provider     string
	ProviderName string
}

// CatalogSyncEntry is the input format from provider FetchModels.
type CatalogSyncEntry struct {
	ModelID      string
	DisplayName  string
	ModelType    string // "chat", "embedding", "image" — from provider API
	Capabilities models.ModelCapabilities
	Pricing      *models.ModelPricing
}

// =========================================
// PERSONA STORE
// =========================================

type PersonaStore interface {
	Create(ctx context.Context, p *models.Persona) error
	GetByID(ctx context.Context, id string) (*models.Persona, error)
	Update(ctx context.Context, id string, patch models.PersonaPatch) error
	Delete(ctx context.Context, id string) error

	// Scoped queries
	ListForUser(ctx context.Context, userID string) ([]models.Persona, error) // all visible to user
	ListForTeam(ctx context.Context, teamID string) ([]models.Persona, error)
	ListGlobal(ctx context.Context) ([]models.Persona, error)

	// Grants
	SetGrants(ctx context.Context, personaID string, grants []models.Grant) error
	GetGrants(ctx context.Context, personaID string) ([]models.Grant, error)
	GetToolGrants(ctx context.Context, personaID string) ([]string, error)

	// Access check
	UserCanAccess(ctx context.Context, userID, personaID string) (bool, error)

	// Knowledge base bindings
	SetKBs(ctx context.Context, personaID string, kbIDs []string, autoSearch map[string]bool) error
	GetKBs(ctx context.Context, personaID string) ([]models.PersonaKB, error)
	GetKBIDs(ctx context.Context, personaID string) ([]string, error)

	// ── Mention resolution (v0.29.0 — moved from handler raw SQL) ──

	// FindActiveByHandle returns an active persona's ID by exact handle match (case-insensitive).
	// Returns "" if not found.
	FindActiveByHandle(ctx context.Context, handle string) (string, error)

	// FindActiveByHandlePrefix returns a persona ID by unambiguous handle prefix.
	// Returns ("", count, nil) if ambiguous or no match.
	FindActiveByHandlePrefix(ctx context.Context, prefix string) (string, int, error)

	// GetNameByID returns the persona's display name. Returns "" if not found.
	GetNameByID(ctx context.Context, id string) (string, error)

	// GetNamesByIDs returns a map of persona ID → name for batch resolution.
	GetNamesByIDs(ctx context.Context, ids []string) (map[string]string, error)

	// GetDisplayInfoByIDs returns name + avatar for batch sender resolution.
	GetDisplayInfoByIDs(ctx context.Context, ids []string) (map[string]UserDisplayInfo, error)
}

// =========================================
// POLICY STORE
// =========================================

type PolicyStore interface {
	Get(ctx context.Context, key string) (string, error)
	GetBool(ctx context.Context, key string) (bool, error)
	Set(ctx context.Context, key, value string, updatedBy string) error
	GetAll(ctx context.Context) (map[string]string, error)
}

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

type UserModelSettingsStore interface {
	GetForUser(ctx context.Context, userID string) ([]models.UserModelSetting, error)
	GetHiddenModelIDs(ctx context.Context, userID string) (map[string]bool, error)
	Set(ctx context.Context, userID, modelID string, providerConfigID *string, patch models.UserModelSettingPatch) error
	BulkSetHidden(ctx context.Context, userID string, entries []models.HiddenEntry, hidden bool) error
}

// =========================================
// USER STORE
// =========================================

type UserStore interface {
	Create(ctx context.Context, u *models.User) error
	GetByID(ctx context.Context, id string) (*models.User, error)
	GetByUsername(ctx context.Context, username string) (*models.User, error)
	GetByEmail(ctx context.Context, email string) (*models.User, error)
	GetByLogin(ctx context.Context, login string) (*models.User, error) // username or email
	GetByHandle(ctx context.Context, handle string) (*models.User, error)
	GetByExternalID(ctx context.Context, authSource, externalID string) (*models.User, error)
	Update(ctx context.Context, id string, fields map[string]interface{}) error
	Delete(ctx context.Context, id string) error
	List(ctx context.Context, opts ListOptions) ([]models.User, int, error)
	UpdateLastLogin(ctx context.Context, id string) error
	SetActive(ctx context.Context, id string, active bool) error
	ListActiveUserIDs(ctx context.Context) ([]string, error)

	// Refresh tokens
	CreateRefreshToken(ctx context.Context, userID, tokenHash string, expiresAt time.Time) error
	GetRefreshToken(ctx context.Context, tokenHash string) (userID string, err error)
	RevokeRefreshToken(ctx context.Context, tokenHash string) error
	RevokeAllRefreshTokens(ctx context.Context, userID string) error
	CleanExpiredTokens(ctx context.Context) error

	// ── Mention resolution (v0.29.0 — moved from handler raw SQL) ──

	// FindActiveByHandle returns a user ID by exact handle match (case-insensitive).
	// excludeUserID prevents self-mention resolution.
	FindActiveByHandle(ctx context.Context, handle, excludeUserID string) (string, error)

	// FindActiveByHandlePrefix returns a user ID by unambiguous handle prefix.
	// Returns ("", count, nil) if ambiguous or no match.
	FindActiveByHandlePrefix(ctx context.Context, prefix, excludeUserID string) (string, int, error)

	// GetDisplayInfoByIDs returns name + avatar for batch sender resolution.
	GetDisplayInfoByIDs(ctx context.Context, ids []string) (map[string]UserDisplayInfo, error)

	// ── CS1 additions (v0.29.0) ──

	// Exists returns true if a user with the given ID exists and is active.
	Exists(ctx context.Context, userID string) (bool, error)

	// SearchActive returns users matching a query (username, display_name, handle).
	// Excludes the calling user. Max 20 results.
	SearchActive(ctx context.Context, excludeUserID, query string) ([]UserSearchResult, error)

	// ── CS2 additions (v0.29.0) ──

	// CountByRole returns the number of users with a given role.
	CountByRole(ctx context.Context, role string) (int, error)

	// CountAll returns the total number of users.
	CountAll(ctx context.Context) (int, error)

	// MergeSettings performs a dialect-aware JSON merge into the user's settings column.
	MergeSettings(ctx context.Context, userID string, patch []byte) error

	// GetVaultKeys returns the user's vault encryption state.
	GetVaultKeys(ctx context.Context, userID string) (vaultSet bool, encUEK, salt, nonce []byte, err error)

	// UpdateVaultKeys re-wraps the user's vault keys (password change).
	UpdateVaultKeys(ctx context.Context, userID string, encUEK, salt, nonce []byte) error

	// ── CS4 additions (v0.29.0) ──

	// ClearVaultKeys nulls out vault columns and sets vault_set = false.
	ClearVaultKeys(ctx context.Context, userID string) error

	// InitVaultKeys stores vault keys and sets vault_set = true (first-time init).
	InitVaultKeys(ctx context.Context, userID string, encUEK, salt, nonce []byte) error
}

// UserSearchResult is a lightweight result for user search.
type UserSearchResult struct {
	ID          string `json:"id"`
	Username    string `json:"username"`
	DisplayName string `json:"display_name"`
	Handle      string `json:"handle"`
}

// =========================================
// TEAM STORE
// =========================================

type TeamStore interface {
	Create(ctx context.Context, t *models.Team) error
	GetByID(ctx context.Context, id string) (*models.Team, error)
	Update(ctx context.Context, id string, fields map[string]interface{}) error
	Delete(ctx context.Context, id string) error
	List(ctx context.Context) ([]models.Team, error)
	ListForUser(ctx context.Context, userID string) ([]models.Team, error) // active teams with MyRole

	// Members
	AddMember(ctx context.Context, teamID, userID, role string) error
	RemoveMember(ctx context.Context, teamID, userID string) error
	UpdateMemberRole(ctx context.Context, teamID, userID, role string) error
	ListMembers(ctx context.Context, teamID string) ([]models.TeamMember, error)
	GetMember(ctx context.Context, teamID, userID string) (*models.TeamMember, error)
	GetUserTeamIDs(ctx context.Context, userID string) ([]string, error)
	IsTeamAdmin(ctx context.Context, teamID, userID string) (bool, error)
	IsMember(ctx context.Context, teamID, userID string) (bool, error)

	// ── CS1 additions (v0.29.0) ──

	// Exists returns true if a team with the given ID exists.
	Exists(ctx context.Context, teamID string) (bool, error)

	// UpdateMemberRoleByID updates a member's role using the member row ID.
	UpdateMemberRoleByID(ctx context.Context, memberID, teamID, role string) (int64, error)

	// DeleteMemberByID deletes a member using the member row ID.
	DeleteMemberByID(ctx context.Context, memberID, teamID string) (int64, error)

	// ListTeamAuditActions returns distinct audit actions for a team's members.
	ListTeamAuditActions(ctx context.Context, teamID string) ([]string, error)

	// ── CS5b additions (v0.29.0) ──

	// GetFirstTeamIDForUser returns the first team_id the user belongs to, or "" if none.
	GetFirstTeamIDForUser(ctx context.Context, userID string) (string, error)

	// ── CS6 additions (v0.29.0) ──

	// AddMemberReturningID inserts a team member and returns the row ID.
	AddMemberReturningID(ctx context.Context, teamID, userID, role string) (string, error)

	// HasPrivateProviderRequirement checks if a user belongs to any active team
	// with the require_private_providers setting enabled.
	HasPrivateProviderRequirement(ctx context.Context, userID string) (bool, error)

	// MergeSettings merges a JSON string into the team's settings column.
	MergeSettings(ctx context.Context, teamID, settingsJSON string) error
}

// =========================================
// CHANNEL STORE
// =========================================

type ChannelStore interface {
	Create(ctx context.Context, ch *models.Channel) error
	GetByID(ctx context.Context, id string) (*models.Channel, error)
	Update(ctx context.Context, id string, fields map[string]interface{}) error
	Delete(ctx context.Context, id string) error
	ListForUser(ctx context.Context, userID string, opts ListOptions) ([]models.Channel, int, error)
	Search(ctx context.Context, userID, query string, opts ListOptions) ([]models.Channel, int, error)

	// Cursor management (conversation forking)
	GetCursor(ctx context.Context, channelID, userID string) (*models.ChannelCursor, error)
	SetCursor(ctx context.Context, channelID, userID, leafID string) error

	// Channel models
	SetModel(ctx context.Context, cm *models.ChannelModel) error
	GetModels(ctx context.Context, channelID string) ([]models.ChannelModel, error)
	GetModelByID(ctx context.Context, id string) (*models.ChannelModel, error)
	UpdateModel(ctx context.Context, id string, fields map[string]interface{}) error
	DeleteModel(ctx context.Context, id string) error

	// Ownership check
	UserOwns(ctx context.Context, channelID, userID string) (bool, error)

	// Channel participants (ICD §3.7)
	AddParticipant(ctx context.Context, p *models.ChannelParticipant) error
	ListParticipants(ctx context.Context, channelID string) ([]models.ChannelParticipant, error)
	GetParticipantByID(ctx context.Context, id string) (*models.ChannelParticipant, error)
	UpdateParticipantRole(ctx context.Context, id, role string) error
	RemoveParticipant(ctx context.Context, id string) error
	IsParticipant(ctx context.Context, channelID, pType, pID string) (bool, error)
	GetParticipantRole(ctx context.Context, channelID, pType, pID string) (string, error)
	CountParticipantsByRole(ctx context.Context, channelID, role string) (int, error)

	// Remove model roster entries auto-created by a persona participant
	DeleteModelByPersona(ctx context.Context, channelID, personaID string) error

	// Workspace resolution: channel workspace_id > project workspace_id
	ResolveWorkspaceID(ctx context.Context, channelID string) (string, error)

	// Admin: archived channel management (v0.23.2)
	ListArchived(ctx context.Context, opts ListOptions) ([]ArchivedChannel, int, error)
	Purge(ctx context.Context, id string) error // hard-delete; channel must be archived

	// ── Single-field helpers (v0.29.0 — moved from handler raw SQL) ──

	// GetAIMode returns the channel's ai_mode ("auto", "off", "mention_only").
	GetAIMode(ctx context.Context, channelID string) (string, error)

	// GetTypeAndTeamID returns (channel_type, team_id) in one query.
	GetTypeAndTeamID(ctx context.Context, channelID string) (string, *string, error)

	// GetSystemPrompt returns the channel's system_prompt (may be nil).
	GetSystemPrompt(ctx context.Context, channelID string) (*string, error)

	// GetDefaultModel returns the channel's model field (may be nil).
	GetDefaultModel(ctx context.Context, channelID string) (*string, error)

	// TouchUpdatedAt bumps the channel's updated_at to now.
	TouchUpdatedAt(ctx context.Context, channelID string) error

	// ListUserParticipantIDs returns user participant IDs, excluding excludeUserID.
	ListUserParticipantIDs(ctx context.Context, channelID, excludeUserID string) ([]string, error)

	// ListPersonaParticipantIDs returns persona participant IDs ordered by created_at.
	ListPersonaParticipantIDs(ctx context.Context, channelID string) ([]string, error)

	// GetLeaderPersonaID returns the persona group leader for a channel.
	// Falls back to the first persona participant if no leader flag is set.
	// Returns empty string if no persona participants exist.
	GetLeaderPersonaID(ctx context.Context, channelID string) (string, error)

	// GetWorkflowInfo returns (workflow_id, current_stage) for workflow-type channels.
	// Returns nil workflow_id if the channel is not a workflow channel.
	GetWorkflowInfo(ctx context.Context, channelID string) (*string, int, error)

	// ── DM + lifecycle helpers (v0.29.0-cs1) ──

	// FindExistingDM returns the channel ID of an existing DM between two users.
	// Returns "" if no DM exists.
	FindExistingDM(ctx context.Context, userID1, userID2 string) (string, error)

	// GetUnreadCount returns the number of unread messages for a user in a channel.
	GetUnreadCount(ctx context.Context, channelID, userID string) (int, error)

	// DeleteByOwner deletes a channel if the user is the owner.
	// Returns rows affected (0 = not found or not owner).
	DeleteByOwner(ctx context.Context, channelID, userID string) (int64, error)

	// ArchiveForRetention archives a channel and sets a purge_after timestamp.
	ArchiveForRetention(ctx context.Context, channelID string, purgeAfter time.Time) error

	// ListPurgeable returns IDs of channels whose purge_after has passed.
	ListPurgeable(ctx context.Context) ([]string, error)

	// MarkRead updates last_read_at and last_read_message_id for a user.
	MarkRead(ctx context.Context, channelID, userID string) error

	// CountParticipantsByType returns count of participants of a given type.
	CountParticipantsByType(ctx context.Context, channelID, pType string) (int, error)

	// CountAll returns the total number of channels.
	CountAll(ctx context.Context) (int, error)

	// ── Workflow instance state (v0.29.0-cs3) ──

	// SetWorkflowInstance initializes workflow columns on a channel.
	SetWorkflowInstance(ctx context.Context, channelID, workflowID string, version int, stageData json.RawMessage, status string) error

	// GetWorkflowStatus returns the full workflow state for a workflow channel.
	GetWorkflowStatus(ctx context.Context, channelID string) (*WorkflowChannelStatus, error)

	// AdvanceWorkflowStage updates current_stage + stage_data + last_activity_at.
	AdvanceWorkflowStage(ctx context.Context, channelID string, nextStage int, stageData json.RawMessage) error

	// CompleteWorkflow sets workflow_status='completed' and ai_mode='off'.
	CompleteWorkflow(ctx context.Context, channelID string, finalStage int, stageData json.RawMessage) error

	// CancelWorkflow sets workflow_status='cancelled' and ai_mode='off'. (v0.37.15)
	CancelWorkflow(ctx context.Context, channelID string) error

	// RejectWorkflowToStage resets current_stage (no stage_data change).
	RejectWorkflowToStage(ctx context.Context, channelID string, stage int) error

	// GetStageData returns the current stage_data JSON from a channel.
	GetStageData(ctx context.Context, channelID string) (json.RawMessage, error)

	// ── Background job helpers (v0.29.0-cs4) ──

	// MarkStaleWorkflows sets workflow_status='stale' on active workflow channels
	// with last_activity_at before cutoff. Returns rows affected.
	MarkStaleWorkflows(ctx context.Context, cutoff time.Time) (int64, error)

	// EnforceWorkflowRetention deletes completed workflow channels whose parent
	// workflow has retention.mode="delete" and the retention period has elapsed.
	// PG-only (uses JSON operators). Returns rows affected. No-op on SQLite.
	EnforceWorkflowRetention(ctx context.Context) (int64, error)

	// GetTypeAndAllowAnonymous returns (type, allow_anonymous) for session auth.
	GetTypeAndAllowAnonymous(ctx context.Context, channelID string) (string, bool, error)

	// ── CS5b additions (v0.29.0) ──

	// FindCompactionCandidates returns direct channels eligible for auto-compaction.
	// Filters by activity gap, max age, minimum message count, and minimum total chars.
	// PG-only (uses settings::text cast). Returns empty slice on SQLite.
	FindCompactionCandidates(ctx context.Context, activityBefore, createdAfter time.Time, minMessages, minChars, limit int) ([]models.Channel, error)

	// ── CS7a additions (v0.29.0) ──

	// GetProviderConfigID returns the channel's provider_config_id (may be nil).
	GetProviderConfigID(ctx context.Context, channelID string) (*string, error)

	// UserCanAccess checks if a user is the channel owner or a participant.
	UserCanAccess(ctx context.Context, channelID, userID string) (bool, error)

	// ── CS7b additions (v0.29.0) ──

	// ListFiltered returns paginated channels with message counts, applying
	// complex filters (types, folder, search, project). Includes channels
	// owned by the user or where the user is a participant.
	ListFiltered(ctx context.Context, userID string, filter ChannelListFilter) ([]ChannelListItem, int, error)

	// GetForUser loads a single channel with message count, verifying the
	// user is the owner or a participant. Returns sql.ErrNoRows if not found.
	GetForUser(ctx context.Context, channelID, userID string) (*ChannelListItem, error)

	// CreateFull atomically creates a channel, adds the owner as a participant,
	// optionally adds DM partner participants, and creates a default channel_model.
	// The channel model (ch) is populated with ID, CreatedAt, UpdatedAt on return.
	// folder, tags, and aiMode are passed separately as they're not in models.Channel.
	CreateFull(ctx context.Context, ch *models.Channel, folder string, tags []string, aiMode string,
		ownerUserID string, dmPartnerIDs []string, defaultModel, defaultConfigID string) error

	// MergeSettings merges a JSON object into the channel's settings column.
	MergeSettings(ctx context.Context, channelID string, settingsJSON json.RawMessage) error

	// ── Monitoring (v0.35.0) ──

	// ListByType returns all channels of a given type (e.g. "workflow").
	ListByType(ctx context.Context, channelType string) ([]models.Channel, error)
}

// ChannelListFilter holds filter options for ChannelStore.ListFiltered.
type ChannelListFilter struct {
	ListOptions
	Archived  bool
	Types     []string // empty = all
	Folder    string
	FolderID  string
	Search    string // title ILIKE/LIKE
	ProjectID string // "none" = NULL, uuid = specific, "" = no filter
}

// ChannelListItem is returned by ListFiltered and GetForUser.
type ChannelListItem struct {
	ID               string
	UserID           string
	Title            string
	Type             string
	AiMode           string
	Topic            *string
	Description      *string
	Model            *string
	ProviderConfigID *string
	SystemPrompt     *string
	IsArchived       bool
	IsPinned         bool
	Folder           *string
	FolderID         *string
	ProjectID        *string
	WorkspaceID      *string
	Tags             []string
	Settings         json.RawMessage
	MessageCount     int
	UnreadCount      int
	CreatedAt        string
	UpdatedAt        string
	CreatedAtTime    time.Time       `json:"-"` // SQLite scan target
	UpdatedAtTime    time.Time       `json:"-"` // SQLite scan target
}

// ArchivedChannel is a view model for the admin archived channels list.
type ArchivedChannel struct {
	ID           string `json:"id"`
	Title        string `json:"title"`
	Type         string `json:"type"`
	OwnerName    string `json:"owner_name"`
	UpdatedAt    string `json:"updated_at"`
	MessageCount int    `json:"message_count"`
}

// =========================================
// MESSAGE STORE
// =========================================

type MessageStore interface {
	Create(ctx context.Context, m *models.Message) error
	GetByID(ctx context.Context, id string) (*models.Message, error)
	Update(ctx context.Context, id string, fields map[string]interface{}) error
	Delete(ctx context.Context, id string) error // soft delete
	ListForChannel(ctx context.Context, channelID string, opts ListOptions) ([]models.Message, error)

	// Tree operations
	GetChildren(ctx context.Context, parentID string) ([]models.Message, error)
	GetSiblings(ctx context.Context, messageID string) ([]models.Message, error)
	GetPathToRoot(ctx context.Context, messageID string) ([]models.Message, error)
	GetNextSiblingIndex(ctx context.Context, parentID string) (int, error)

	// Count
	CountForChannel(ctx context.Context, channelID string) (int, error)

	// ── Tree operations (v0.29.0 — moved from treepath package) ──

	// GetActiveLeaf returns the cursor's active_leaf_id for a user in a channel.
	// Falls back to the chronologically latest live message if no cursor exists.
	GetActiveLeaf(ctx context.Context, channelID, userID string) (*string, error)

	// GetPathToLeaf walks from a leaf up to root via parent_id, returning
	// root-first order with sibling counts and sender info resolved.
	GetPathToLeaf(ctx context.Context, channelID, leafID string) ([]PathMessage, error)

	// GetActivePath is GetActiveLeaf + GetPathToLeaf combined.
	GetActivePath(ctx context.Context, channelID, userID string) ([]PathMessage, error)

	// GetSiblingsList returns all live siblings of a message with the current index.
	GetSiblingsList(ctx context.Context, messageID string) ([]SiblingInfo, int, error)

	// GetSiblingCount returns how many live siblings share the same parent.
	GetSiblingCount(ctx context.Context, channelID string, parentID *string) (int, error)

	// FindLeafFromMessage walks down from a message to find the deepest descendant.
	FindLeafFromMessage(ctx context.Context, messageID string) (string, error)

	// NextSiblingIndexForParent returns the next sibling_index for children of parentID.
	NextSiblingIndexForParent(ctx context.Context, channelID string, parentID *string) (int, error)

	// HasPersonaMessages checks if any assistant message in the channel was
	// generated by a persona.
	HasPersonaMessages(ctx context.Context, channelID string) (bool, error)

	// CreateWithCursor inserts a message, updates the cursor, and touches
	// channel.updated_at. Handles dialect-specific ID generation internally.
	CreateWithCursor(ctx context.Context, m *models.Message, cursorUserID string) error

	// ResolveSenderInfo batch-resolves sender_name and sender_avatar for
	// user and persona participants. Updates the PathMessage slice in-place.
	ResolveSenderInfo(ctx context.Context, path []PathMessage) error

	// CountAll returns the total number of messages (for admin stats).
	CountAll(ctx context.Context) (int, error)

	// ── CS5c additions (v0.29.0) ──

	// SearchInChannel performs full-text search within a single channel's messages.
	// PG uses to_tsvector/ts_headline; SQLite falls back to LIKE.
	// roleFilter is "user", "assistant", or "" for all.
	SearchInChannel(ctx context.Context, channelID, query, roleFilter string, limit int) ([]ChannelSearchResult, error)

	// ── CS7a additions (v0.29.0) ──

	// ListWithSenderInfo returns paginated messages with sender name/avatar
	// resolved via JOINs to users and personas tables.
	ListWithSenderInfo(ctx context.Context, channelID string, limit, offset int) ([]MessageWithSender, int, error)

	// GetParentAndRole loads just the parent_id and role of a message,
	// verifying it belongs to the given channel and is not deleted.
	GetParentAndRole(ctx context.Context, messageID, channelID string) (parentID *string, role string, err error)
}

// MessageWithSender is returned by MessageStore.ListWithSenderInfo.
type MessageWithSender struct {
	ID              string
	ChannelID       string
	Role            string
	Content         string
	Model           *string
	TokensUsed      *int
	ParentID        *string
	SiblingIndex    int
	ParticipantType *string
	ParticipantID   *string
	SenderName      *string
	SenderAvatar    *string
	CreatedAt       string
}

// ChannelSearchResult is returned by MessageStore.SearchInChannel.
type ChannelSearchResult struct {
	MessageID string
	Role      string
	Excerpt   string    // ts_headline or content substring
	Rank      float64
	Timestamp time.Time
}

// =========================================
// AUDIT STORE
// =========================================

type AuditStore interface {
	Log(ctx context.Context, entry *models.AuditEntry) error
	List(ctx context.Context, opts AuditListOptions) ([]models.AuditEntry, int, error)
	ListActions(ctx context.Context) ([]string, error)
}

type AuditListOptions struct {
	ListOptions
	ActorID      string
	Action       string
	ResourceType string
	ResourceID   string
	Since        *time.Time
	Until        *time.Time
	TeamID       string // CS6: scope to team members
}

// =========================================
// NOTE STORE
// =========================================

type NoteStore interface {
	Create(ctx context.Context, n *models.Note) error
	GetByID(ctx context.Context, id string) (*models.Note, error)
	Update(ctx context.Context, id string, fields map[string]interface{}) error
	Delete(ctx context.Context, id string) error
	ListForUser(ctx context.Context, userID string, opts NoteListOptions) ([]models.Note, int, error)
	Search(ctx context.Context, userID, query string, opts ListOptions) ([]models.Note, int, error)
	SearchTitles(ctx context.Context, userID, query string, limit int) ([]models.Note, error)
	BulkDelete(ctx context.Context, ids []string, userID string) (int, error)

	// ── CS5c additions (v0.29.0) ──

	// SetEmbedding stores a pgvector embedding on a note. PG-only; SQLite no-ops.
	SetEmbedding(ctx context.Context, noteID, vecStr string) error

	// SearchKeyword performs full-text keyword search with ranking and headlines.
	// PG uses ts_rank/ts_headline; SQLite falls back to LIKE.
	SearchKeyword(ctx context.Context, userID, query string, limit int) ([]NoteSearchResult, error)

	// SearchSemantic performs vector similarity search against note embeddings.
	// PG-only; SQLite returns empty results.
	SearchSemantic(ctx context.Context, userID, vecStr string, limit int) ([]NoteSearchResult, error)

	// ListFolders returns distinct folder paths and note counts for a user.
	ListFolders(ctx context.Context, userID string) ([]FolderInfo, error)
}

type NoteListOptions struct {
	ListOptions
	FolderPath string
	Tag        string
	TeamID     string
}

// NoteSearchResult is returned by NoteStore.SearchKeyword and SearchSemantic.
type NoteSearchResult struct {
	ID         string
	Title      string
	FolderPath string
	Tags       []string
	Excerpt    string  // truncated content preview
	Headline   string  // ts_headline markup (empty on SQLite)
	Rank       float64 // ts_rank or similarity score
}

// FolderInfo is returned by NoteStore.ListFolders.
type FolderInfo struct {
	Path  string
	Count int
}

// =========================================
// NOTE LINK STORE
// =========================================

// NoteLinkStore manages wikilink edges between notes.
type NoteLinkStore interface {
	// ReplaceLinks deletes existing links for sourceNoteID and inserts new ones.
	ReplaceLinks(ctx context.Context, sourceNoteID string, links []models.NoteLink) error

	// ResolveByTitle sets target_note_id on dangling links matching the title for a user.
	ResolveByTitle(ctx context.Context, userID, targetNoteID, title string) error

	// Backlinks returns notes that link to the given note.
	Backlinks(ctx context.Context, noteID string) ([]models.NoteLinkResult, error)

	// Graph returns all nodes and edges for a user's note graph.
	Graph(ctx context.Context, userID string) (*models.NoteGraph, error)
}

// =========================================
// GLOBAL CONFIG STORE
// =========================================

type GlobalConfigStore interface {
	Get(ctx context.Context, key string) (models.JSONMap, error)
	Set(ctx context.Context, key string, value models.JSONMap, updatedBy string) error
	GetAll(ctx context.Context) (map[string]models.JSONMap, error)

	// ── OIDC state (v0.29.0-cs4) ──

	// SaveOIDCState stores a state+nonce pair for OIDC callback verification.
	SaveOIDCState(ctx context.Context, state, nonce, redirectTo string) error

	// ConsumeOIDCState retrieves and deletes an OIDC state (one-time use).
	// Returns (nonce, redirectTo, error). Returns sql.ErrNoRows if not found.
	ConsumeOIDCState(ctx context.Context, state string) (nonce, redirectTo string, err error)

	// CleanupOIDCState removes stale OIDC states older than 10 minutes.
	CleanupOIDCState(ctx context.Context) error

	// ── CS6 additions (v0.29.0) ──

	// GetString returns the raw value column as a string for simple settings
	// (e.g. bare JSON booleans like "true"/"false").
	GetString(ctx context.Context, key string) (string, error)
}

// =========================================
// USAGE STORE
// =========================================

type UsageStore interface {
	Log(ctx context.Context, entry *models.UsageEntry) error
	CountRecentByRole(ctx context.Context, userID, role string, since time.Time) (int, error)
	QueryByUser(ctx context.Context, userID string, opts UsageQueryOptions) ([]models.UsageAggregate, error)
	QueryByUserPersonal(ctx context.Context, userID string, opts UsageQueryOptions) ([]models.UsageAggregate, error)
	QueryByTeam(ctx context.Context, teamID string, opts UsageQueryOptions) ([]models.UsageAggregate, error)
	QueryByTeamProviders(ctx context.Context, teamID string, opts UsageQueryOptions) ([]models.UsageAggregate, error)
	QueryByModel(ctx context.Context, opts UsageQueryOptions) ([]models.UsageAggregate, error)
	GetTotals(ctx context.Context, opts UsageQueryOptions) (*models.UsageTotals, error)
	GetTeamProviderTotals(ctx context.Context, teamID string, opts UsageQueryOptions) (*models.UsageTotals, error)
	GetPersonalTotals(ctx context.Context, userID string, opts UsageQueryOptions) (*models.UsageTotals, error)
}

type UsageQueryOptions struct {
	Since       *time.Time
	Until       *time.Time
	GroupBy     string // "day", "model", "user", "provider"
	ExcludeBYOK bool   // true for admin views — filters provider_scope != 'personal'
	Limit       int
}

// =========================================
// PRICING STORE
// =========================================

type PricingStore interface {
	GetForModel(ctx context.Context, providerConfigID, modelID string) (*models.PricingEntry, error)
	Upsert(ctx context.Context, entry *models.PricingEntry) error
	UpsertFromCatalog(ctx context.Context, providerConfigID, modelID string, pricing *models.ModelPricing) error
	List(ctx context.Context) ([]models.PricingEntry, error)
	Delete(ctx context.Context, providerConfigID, modelID string) error
}

// =========================================
// FILE STORE
// =========================================

type FileStore interface {
	Create(ctx context.Context, f *models.File) error
	GetByID(ctx context.Context, id string) (*models.File, error)
	GetByChannel(ctx context.Context, channelID string, origin string) ([]models.File, error)
	GetByMessage(ctx context.Context, messageID string) ([]models.File, error)
	GetByProject(ctx context.Context, projectID string) ([]models.File, error)
	GetByUser(ctx context.Context, userID string, page, perPage int) ([]models.File, int, error) // paginated, returns total
	SetMessageID(ctx context.Context, fileID, messageID string) error
	UpdateMetadata(ctx context.Context, id string, metadata map[string]interface{}) error
	SetExtractedText(ctx context.Context, id string, text string) error
	Delete(ctx context.Context, id string) (*models.File, error) // returns deleted row for storage cleanup
	DeleteByChannel(ctx context.Context, channelID string) ([]string, error) // returns storage_keys
	UserUsageBytes(ctx context.Context, userID string) (int64, error)
	ListOrphans(ctx context.Context, olderThan time.Duration) ([]models.File, error)

	// ── CS5c additions (v0.29.0) ──

	// UpdateStorageKey sets the storage key after initial file creation.
	UpdateStorageKey(ctx context.Context, id, key string) error
}



// =========================================
// KNOWLEDGE BASE STORE
// =========================================

type KnowledgeBaseStore interface {
	// KB CRUD
	Create(ctx context.Context, kb *models.KnowledgeBase) error
	GetByID(ctx context.Context, id string) (*models.KnowledgeBase, error)
	Update(ctx context.Context, id string, fields map[string]interface{}) error
	Delete(ctx context.Context, id string) error

	// Scoped listing
	ListForUser(ctx context.Context, userID string, teamIDs []string) ([]models.KnowledgeBase, error)
	ListPersonal(ctx context.Context, userID string) ([]models.KnowledgeBase, error)

	// Documents
	CreateDocument(ctx context.Context, doc *models.KBDocument) error
	GetDocument(ctx context.Context, id string) (*models.KBDocument, error)
	ListDocuments(ctx context.Context, kbID string) ([]models.KBDocument, error)
	UpdateDocumentStatus(ctx context.Context, id string, status string, errMsg *string) error
	UpdateDocumentText(ctx context.Context, id string, text string, chunkCount int) error
	UpdateDocumentStorageKey(ctx context.Context, id string, storageKey string) error
	DeleteDocument(ctx context.Context, id string) (*models.KBDocument, error) // returns for storage cleanup

	// Chunks
	InsertChunks(ctx context.Context, chunks []models.KBChunk) error
	DeleteChunksForDocument(ctx context.Context, documentID string) error
	SimilaritySearch(ctx context.Context, kbIDs []string, queryVec []float64,
		threshold float64, limit int) ([]models.KBSearchResult, error)

	// Channel links
	SetChannelKBs(ctx context.Context, channelID string, kbIDs []string) error
	GetChannelKBs(ctx context.Context, channelID string) ([]models.ChannelKB, error)
	GetActiveKBIDs(ctx context.Context, channelID string, userID string,
		teamIDs []string) ([]string, error) // enabled + user has access
	GetActiveKBIDsWithPersona(ctx context.Context, channelID string, userID string,
		teamIDs []string, personaID string) ([]string, error) // includes persona-bound KBs
	ListDiscoverable(ctx context.Context, userID string, teamIDs []string) ([]models.KnowledgeBase, error)

	// Stats — recount document_count/chunk_count/total_bytes from child tables
	UpdateStats(ctx context.Context, kbID string) error

	// Discoverable management
	SetDiscoverable(ctx context.Context, kbID string, discoverable bool) error
}

// =========================================
// GROUP STORE (v0.16.0)
// =========================================

type GroupStore interface {
	Create(ctx context.Context, g *models.Group) error
	GetByID(ctx context.Context, id string) (*models.Group, error)
	Update(ctx context.Context, id string, patch models.GroupPatch) error
	Delete(ctx context.Context, id string) error

	// Scoped listing
	ListAll(ctx context.Context) ([]models.Group, error)           // admin
	ListForTeam(ctx context.Context, teamID string) ([]models.Group, error) // team admin
	ListForUser(ctx context.Context, userID string) ([]models.Group, error) // groups I belong to

	// Members
	AddMember(ctx context.Context, groupID, userID, addedBy string) error
	RemoveMember(ctx context.Context, groupID, userID string) error
	ListMembers(ctx context.Context, groupID string) ([]models.GroupMember, error)
	IsMember(ctx context.Context, groupID, userID string) (bool, error)

	// For access resolution: returns group IDs a user belongs to
	GetUserGroupIDs(ctx context.Context, userID string) ([]string, error)
}

// =========================================
// RESOURCE GRANT STORE (v0.16.0)
// =========================================

type ResourceGrantStore interface {
	Set(ctx context.Context, grant *models.ResourceGrant) error
	Get(ctx context.Context, resourceType, resourceID string) (*models.ResourceGrant, error)
	Delete(ctx context.Context, resourceType, resourceID string) error

	// Access check: does userID have group-based access to this resource?
	UserHasGroupAccess(ctx context.Context, userID, resourceType, resourceID string) (bool, error)
}

// =========================================
// NOTIFICATION STORE (v0.20.0)
// =========================================

type NotificationStore interface {
	Create(ctx context.Context, n *models.Notification) error
	ListByUser(ctx context.Context, userID string, limit, offset int, unreadOnly bool) ([]models.Notification, int, error)
	MarkRead(ctx context.Context, id, userID string) error
	MarkAllRead(ctx context.Context, userID string) error
	Delete(ctx context.Context, id, userID string) error
	UnreadCount(ctx context.Context, userID string) (int, error)
	DeleteOlderThan(ctx context.Context, before time.Time) (int64, error)
}

// =========================================
// NOTIFICATION PREFERENCES STORE (v0.20.0)
// =========================================

type NotificationPreferenceStore interface {
	// Get returns the preference for a specific user + type. Returns nil if not set.
	Get(ctx context.Context, userID, notifType string) (*models.NotificationPreference, error)
	// ListForUser returns all preferences set by a user.
	ListForUser(ctx context.Context, userID string) ([]models.NotificationPreference, error)
	// Upsert creates or updates a preference row.
	Upsert(ctx context.Context, pref *models.NotificationPreference) error
	// Delete removes a preference (falls back to default).
	Delete(ctx context.Context, userID, notifType string) error
}

// =========================================
// WORKSPACE STORE (v0.21.0)
// =========================================

type WorkspaceStore interface {
	// CRUD
	Create(ctx context.Context, w *models.Workspace) error
	GetByID(ctx context.Context, id string) (*models.Workspace, error)
	Update(ctx context.Context, id string, patch models.WorkspacePatch) error
	Delete(ctx context.Context, id string) error

	// Ownership lookup
	GetByOwner(ctx context.Context, ownerType, ownerID string) (*models.Workspace, error)
	ListByOwner(ctx context.Context, ownerType, ownerID string) ([]models.Workspace, error)

	// File index
	UpsertFile(ctx context.Context, f *models.WorkspaceFile) error
	DeleteFile(ctx context.Context, workspaceID, path string) error
	DeleteFilesByPrefix(ctx context.Context, workspaceID, prefix string) error
	GetFile(ctx context.Context, workspaceID, path string) (*models.WorkspaceFile, error)
	ListFiles(ctx context.Context, workspaceID, prefix string, recursive bool) ([]models.WorkspaceFile, error)
	DeleteAllFiles(ctx context.Context, workspaceID string) error

	// Stats
	GetStats(ctx context.Context, workspaceID string) (*models.WorkspaceStats, error)

	// Chunks (v0.21.2 — workspace indexing)
	InsertChunks(ctx context.Context, chunks []models.WorkspaceChunk) error
	DeleteChunksByFile(ctx context.Context, fileID string) error
	SimilaritySearch(ctx context.Context, workspaceID string, queryVec []float64, threshold float64, limit int) ([]models.WorkspaceChunkResult, error)
	UpdateFileIndexStatus(ctx context.Context, fileID, status string, chunkCount int) error

	// Git (v0.21.4)
	SetGitLastSync(ctx context.Context, workspaceID string, t time.Time) error
}

// =========================================
// GIT CREDENTIAL STORE (v0.21.4)
// =========================================

type GitCredentialStore interface {
	Create(ctx context.Context, cred *models.GitCredential) error
	GetByID(ctx context.Context, id string) (*models.GitCredential, error)
	ListByUser(ctx context.Context, userID string) ([]models.GitCredential, error)
	Delete(ctx context.Context, id, userID string) error
}

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

type CapabilityOverrideStore interface {
	// Set creates or updates an override for (provider, model, field).
	Set(ctx context.Context, o *models.CapabilityOverride) error

	// Delete removes a specific override.
	Delete(ctx context.Context, id string) error

	// ListForModel returns all overrides for a model ID (across all providers + global).
	ListForModel(ctx context.Context, modelID string) ([]models.CapabilityOverride, error)

	// ListForProviderModel returns overrides for a specific provider+model combination.
	ListForProviderModel(ctx context.Context, providerConfigID, modelID string) ([]models.CapabilityOverride, error)

	// ListAll returns every override (admin view).
	ListAll(ctx context.Context) ([]models.CapabilityOverride, error)

	// DeleteForProvider removes all overrides for a provider (cascade cleanup).
	DeleteForProvider(ctx context.Context, providerConfigID string) error
}

// =========================================
// ROUTING POLICY STORE (v0.22.2)
// =========================================

type RoutingPolicyStore interface {
	// Create adds a new routing policy.
	Create(ctx context.Context, p *models.RoutingPolicy) error

	// Update modifies an existing routing policy.
	Update(ctx context.Context, p *models.RoutingPolicy) error

	// Delete removes a routing policy by ID.
	Delete(ctx context.Context, id string) error

	// GetByID returns a single policy.
	GetByID(ctx context.Context, id string) (*models.RoutingPolicy, error)

	// ListActive returns all active policies, ordered by priority ASC.
	ListActive(ctx context.Context) ([]models.RoutingPolicy, error)

	// ListAll returns all policies (including inactive), for admin listing.
	ListAll(ctx context.Context) ([]models.RoutingPolicy, error)

	// ListForTeam returns active policies applicable to a team (team-scoped + global).
	ListForTeam(ctx context.Context, teamID string) ([]models.RoutingPolicy, error)
}

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

type SessionStore interface {
	Create(ctx context.Context, s *models.SessionParticipant) error
	GetByToken(ctx context.Context, token string) (*models.SessionParticipant, error)
	GetByID(ctx context.Context, id string) (*models.SessionParticipant, error)
	ListForChannel(ctx context.Context, channelID string) ([]models.SessionParticipant, error)
	CountForChannel(ctx context.Context, channelID string) (int, error)
	Delete(ctx context.Context, id string) error

	// DeleteExpired removes sessions older than the given time that have
	// no associated messages. Returns the number of deleted rows.
	// Used by the background session cleanup job (v0.26.0).
	DeleteExpired(ctx context.Context, olderThan time.Time) (int64, error)
}

// =========================================
// PERSONA GROUP STORE (v0.29.0)
// =========================================

// PersonaGroupStore manages persona_groups and their members.
type PersonaGroupStore interface {
	// CRUD
	List(ctx context.Context, ownerID string) ([]models.PersonaGroup, error)
	Get(ctx context.Context, id, ownerID string) (*models.PersonaGroup, error)
	Create(ctx context.Context, g *models.PersonaGroup) error
	Update(ctx context.Context, id string, fields map[string]interface{}) error
	Delete(ctx context.Context, id, ownerID string) (int64, error)

	// Ownership check
	GetOwnerID(ctx context.Context, id string) (string, error)

	// Members
	AddMember(ctx context.Context, groupID, personaID string, isLeader bool) error
	RemoveMember(ctx context.Context, memberID, groupID string) error
	ListMembers(ctx context.Context, groupID string) ([]models.PersonaGroupMember, error)
}

// =========================================
// FOLDER STORE (v0.29.0)
// =========================================

// FolderStore manages user chat folders.
type FolderStore interface {
	List(ctx context.Context, userID string) ([]models.Folder, error)
	Create(ctx context.Context, f *models.Folder) error
	Update(ctx context.Context, folderID, userID string, name string, sortOrder *int, parentID **string) (int64, error)
	Delete(ctx context.Context, folderID, userID string) (int64, error)
	// UnassignChannels removes folder_id from all channels in this folder.
	UnassignChannels(ctx context.Context, folderID, userID string) error
}

// =========================================
// HEALTH STORE (v0.29.0)
// =========================================

// HealthStore manages provider health windows. The full interface is used
// by the health accumulator and status querier; the scheduler only needs Prune.
type HealthStore interface {
	// Prune deletes health windows older than the given time. Returns rows affected.
	Prune(ctx context.Context, before time.Time) (int64, error)
}

// =========================================
// PRESENCE STORE (v0.29.0)
// =========================================

// PresenceStore manages user online/offline status.
type PresenceStore interface {
	// Heartbeat upserts the user's last_seen timestamp to now.
	Heartbeat(ctx context.Context, userID string) error

	// GetLastSeen returns the user's last heartbeat time, or nil if never seen.
	GetLastSeen(ctx context.Context, userID string) (*time.Time, error)

	// GetStatuses returns online/offline status for a list of user IDs.
	// Users with last_seen after threshold are "online", otherwise "offline".
	GetStatuses(ctx context.Context, userIDs []string, threshold time.Time) (map[string]string, error)
}

// =========================================
// EXPORT STORE (v0.34.0)
// =========================================

// ExportStore provides batch-read queries for data export, import,
// and GDPR operations. These methods are optimized for bulk retrieval
// without N+1 queries. Embeddings are excluded at query level.
type ExportStore interface {
	// ── User-scoped export ──

	// UserChannels returns all channels owned by userID.
	UserChannels(ctx context.Context, userID string) ([]models.Channel, error)

	// UserMessages returns all non-deleted messages in the given channels, ordered by created_at ASC.
	UserMessages(ctx context.Context, channelIDs []string) ([]models.Message, error)

	// UserChannelParticipants returns participants for the given channels.
	UserChannelParticipants(ctx context.Context, channelIDs []string) ([]models.ChannelParticipant, error)

	// UserChannelModels returns channel_models for the given channels.
	UserChannelModels(ctx context.Context, channelIDs []string) ([]models.ChannelModel, error)

	// UserChannelCursors returns cursors for the user in the given channels.
	UserChannelCursors(ctx context.Context, userID string, channelIDs []string) ([]models.ChannelCursor, error)

	// UserNotes returns all notes owned by userID.
	UserNotes(ctx context.Context, userID string) ([]models.Note, error)

	// UserNoteLinks returns note_links with source_note_id for the given note IDs.
	UserNoteLinks(ctx context.Context, noteIDs []string) ([]models.ExportNoteLink, error)

	// UserMemories returns active memories for userID (scope=user).
	UserMemories(ctx context.Context, userID string) ([]models.Memory, error)

	// UserProjects returns personal-scope projects owned by userID.
	UserProjects(ctx context.Context, userID string) ([]models.Project, error)

	// UserProjectChannels returns project_channels for the given projects.
	UserProjectChannels(ctx context.Context, projectIDs []string) ([]models.ProjectChannel, error)

	// UserProjectKBs returns project_knowledge_bases for the given projects.
	UserProjectKBs(ctx context.Context, projectIDs []string) ([]models.ProjectKB, error)

	// UserProjectNotes returns project_notes for the given projects.
	UserProjectNotes(ctx context.Context, projectIDs []string) ([]models.ProjectNote, error)

	// UserWorkspaces returns workspaces owned by user (owner_type=user).
	UserWorkspaces(ctx context.Context, userID string) ([]models.Workspace, error)

	// UserWorkspaceFiles returns file index for the given workspaces.
	UserWorkspaceFiles(ctx context.Context, workspaceIDs []string) ([]models.WorkspaceFile, error)

	// UserFiles returns file metadata for files uploaded by userID.
	UserFiles(ctx context.Context, userID string) ([]models.File, error)

	// UserFolders returns all folders for userID.
	UserFolders(ctx context.Context, userID string) ([]models.Folder, error)

	// UserModelSettings returns user_model_settings for userID.
	UserModelSettings(ctx context.Context, userID string) ([]models.UserModelSetting, error)

	// UserNotifPrefs returns notification preferences for userID.
	UserNotifPrefs(ctx context.Context, userID string) ([]models.NotificationPreference, error)

	// UserUsageEntries returns usage entries for userID (last 90 days).
	UserUsageEntries(ctx context.Context, userID string) ([]models.UsageEntry, error)

	// UserPersonaGroups returns persona groups owned by userID.
	UserPersonaGroups(ctx context.Context, userID string) ([]models.PersonaGroup, error)

	// UserPersonaGroupMembers returns members for the given groups.
	UserPersonaGroupMembers(ctx context.Context, groupIDs []string) ([]models.PersonaGroupMember, error)

	// ── Team-scoped export ──

	// TeamChannels returns all channels belonging to teamID.
	TeamChannels(ctx context.Context, teamID string) ([]models.Channel, error)

	// TeamMembers returns team membership records.
	TeamMembers(ctx context.Context, teamID string) ([]models.TeamMember, error)

	// TeamPersonas returns team-scoped personas.
	TeamPersonas(ctx context.Context, teamID string) ([]models.Persona, error)

	// TeamPersonaKBs returns persona_knowledge_bases for given persona IDs.
	TeamPersonaKBs(ctx context.Context, personaIDs []string) ([]models.PersonaKB, error)

	// TeamKnowledgeBases returns team-scoped knowledge bases.
	TeamKnowledgeBases(ctx context.Context, teamID string) ([]models.KnowledgeBase, error)

	// TeamKBDocuments returns KB document metadata for given KB IDs (no chunks/embeddings).
	TeamKBDocuments(ctx context.Context, kbIDs []string) ([]models.KBDocument, error)

	// TeamWorkflows returns team-scoped workflows.
	TeamWorkflows(ctx context.Context, teamID string) ([]models.Workflow, error)

	// TeamWorkflowVersions returns versions for given workflow IDs.
	TeamWorkflowVersions(ctx context.Context, workflowIDs []string) ([]models.WorkflowVersion, error)

	// TeamWorkflowStages returns stages for given workflow IDs.
	TeamWorkflowStages(ctx context.Context, workflowIDs []string) ([]models.WorkflowStage, error)

	// TeamGroups returns team-scoped groups.
	TeamGroups(ctx context.Context, teamID string) ([]models.Group, error)

	// TeamGroupMembers returns members for given group IDs.
	TeamGroupMembers(ctx context.Context, groupIDs []string) ([]models.GroupMember, error)

	// TeamResourceGrants returns resource grants for team resources.
	TeamResourceGrants(ctx context.Context, teamID string) ([]models.ResourceGrant, error)

	// TeamProjects returns team-scoped projects.
	TeamProjects(ctx context.Context, teamID string) ([]models.Project, error)

	// ── Import (CS1) ──
	// Each ImportXxx method inserts entities preserving their original IDs.
	// Uses INSERT ON CONFLICT DO NOTHING (PG) / INSERT OR IGNORE (SQLite)
	// to skip duplicates. Returns (imported, skipped) counts.

	ImportFolders(ctx context.Context, folders []models.Folder) (int, int, error)
	ImportProjects(ctx context.Context, projects []models.Project) (int, int, error)
	ImportWorkspaces(ctx context.Context, workspaces []models.Workspace) (int, int, error)
	ImportChannels(ctx context.Context, channels []models.Channel) (int, int, error)
	ImportChannelParticipants(ctx context.Context, cps []models.ChannelParticipant) (int, int, error)
	ImportChannelModels(ctx context.Context, cms []models.ChannelModel) (int, int, error)
	ImportChannelCursors(ctx context.Context, cursors []models.ChannelCursor) (int, int, error)
	ImportMessages(ctx context.Context, msgs []models.Message) (int, int, error)
	ImportNotes(ctx context.Context, notes []models.Note) (int, int, error)
	ImportNoteLinks(ctx context.Context, links []models.ExportNoteLink) (int, int, error)
	ImportMemories(ctx context.Context, mems []models.Memory) (int, int, error)
	ImportProjectChannels(ctx context.Context, pcs []models.ProjectChannel) (int, int, error)
	ImportProjectKBs(ctx context.Context, pks []models.ProjectKB) (int, int, error)
	ImportProjectNotes(ctx context.Context, pns []models.ProjectNote) (int, int, error)
	ImportWorkspaceFiles(ctx context.Context, wfs []models.WorkspaceFile) (int, int, error)
	ImportFiles(ctx context.Context, files []models.File) (int, int, error)
	ImportUserModelSettings(ctx context.Context, ums []models.UserModelSetting) (int, int, error)
	ImportNotifPrefs(ctx context.Context, nps []models.NotificationPreference) (int, int, error)
	ImportPersonaGroups(ctx context.Context, pgs []models.PersonaGroup) (int, int, error)
	ImportPersonaGroupMembers(ctx context.Context, pgms []models.PersonaGroupMember) (int, int, error)

	// ── GDPR ──

	// CountActiveAdmins returns the number of active admin users.
	CountActiveAdmins(ctx context.Context) (int, error)

	// AnonymizeUser replaces PII with "deleted-user-{hash}" placeholders.
	AnonymizeUser(ctx context.Context, userID string, anonHash string) error

	// SoftDeleteUserData cascade-deletes/soft-deletes all user-owned data.
	// Returns entity counts by type for the audit trail.
	SoftDeleteUserData(ctx context.Context, userID string) (map[string]int64, error)

	// DeleteUserTokens revokes all refresh tokens and WS tickets.
	DeleteUserTokens(ctx context.Context, userID string) error
}

// =========================================
// SHARED TYPES
// =========================================

// ListOptions provides standard pagination/sort for list queries.
type ListOptions struct {
	Limit  int
	Offset int
	Sort   string // column name
	Order  string // "asc" or "desc"
}

// DefaultListOptions returns sensible defaults.
func DefaultListOptions() ListOptions {
	return ListOptions{
		Limit: 50,
		Order: "desc",
	}
}
