package models

import (
	"time"

	"gorm.io/gorm"
)

// Agent merepresentasikan satu sesi WhatsApp yang tertaut — satu CS/AI per nomor.
type Agent struct {
	Permissions        *WorkspacePermissions `gorm:"-" json:"permissions,omitempty"`
	ID                 uint                  `gorm:"primaryKey" json:"id"`
	TenantID           uint                  `gorm:"index;not null" json:"tenant_id"`
	Name               string                `json:"name"`
	SystemPrompt       string                `gorm:"type:text" json:"system_prompt"`
	Tone               string                `gorm:"default:ramah" json:"tone"`
	ResponseLength     string                `gorm:"size:16;default:balanced" json:"response_length"`
	AgenticEnabled     bool                  `gorm:"not null;default:false" json:"agentic_enabled"`
	AIEnabled          bool                  `gorm:"not null" json:"ai_enabled"` // master switch balasan AI — default OFF, diaktifkan user setelah setup
	AutoRead           bool                  `gorm:"not null;default:false" json:"auto_read"`
	AIReplyDelayMin    int                   `gorm:"not null;default:4" json:"ai_reply_delay_min"`
	AIReplyDelayMax    int                   `gorm:"not null;default:8" json:"ai_reply_delay_max"`
	ResponseSLAMinutes int                   `gorm:"not null;default:15" json:"response_sla_minutes"`
	DeviceJID          string                `json:"device_jid"`
	Number             string                `json:"number"`
	// InboxOwnerNumber tetap disimpan saat logout. Kolom ini dipakai untuk
	// mendeteksi bila slot agent yang sama ditautkan ke akun WhatsApp berbeda,
	// sehingga riwayat lokal akun lama tidak tercampur dengan akun baru.
	InboxOwnerNumber string `gorm:"size:32;index" json:"inbox_owner_number,omitempty"`

	GreetingEnabled bool   `gorm:"not null;default:false" json:"greeting_enabled"`
	GreetingMessage string `gorm:"type:text" json:"greeting_message"`

	BusinessHoursEnabled bool   `gorm:"not null;default:false" json:"business_hours_enabled"`
	BusinessStart        string `gorm:"size:5;default:'08:00'" json:"business_start"`
	BusinessEnd          string `gorm:"size:5;default:'21:00'" json:"business_end"`
	AwayMessage          string `gorm:"type:text" json:"away_message"`

	// Deprecated: ringkasan percakapan kini disimpan per-kontak di ConversationMemory
	// (dulu global per agent -> bocor antar-customer). Kolom ini tidak lagi dibaca/ditulis.
	ConversationSummary string     `gorm:"type:text" json:"conversation_summary"`
	LastSummaryAt       *time.Time `json:"last_summary_at"`

	// Integrasi Google Sheets untuk export data closing otomatis.
	SpreadsheetURL       string `gorm:"type:text" json:"spreadsheet_url"`
	SpreadsheetSheetName string `gorm:"size:80;default:'Leads'" json:"spreadsheet_sheet_name"`
	SheetSyncEnabled     bool   `gorm:"not null;default:false" json:"sheet_sync_enabled"`

	// Cek ongkir realtime via RajaOngkir.
	OriginCityID      int    `gorm:"default:0" json:"origin_city_id"`
	OriginCityName    string `gorm:"size:100" json:"origin_city_name"`
	DefaultWeightGram int    `gorm:"default:1000" json:"default_weight_gram"`
	EnabledCouriers   string `gorm:"size:100;default:'jne,jnt,sicepat'" json:"enabled_couriers"`

	// REST API publik + Webhook (per-nomor). APIKey & WebhookSecret tidak pernah
	// diserialkan ke JSON (json:"-") — hanya ditampilkan tersamar / sekali saat dibuat.
	// APIKey adalah legacy plaintext untuk migrasi satu kali; APIKeyHash dipakai setelah key dibuat/terpakai kembali.
	APIKey        string `gorm:"index;size:80" json:"-"`
	APIKeyHash    string `gorm:"index;size=64" json:"-"`
	APIKeyPrefix  string `gorm:"size=16" json:"-"`
	WebhookURL    string `gorm:"type:text" json:"webhook_url"`
	WebhookSecret string `gorm:"size:80" json:"-"`

	HandoffNotifyEnabled bool   `gorm:"not null;default:false" json:"-"`
	HandoffNotifyNumber  string `gorm:"size:32" json:"-"`
	HandoffInboxURL      string `gorm:"size:512" json:"-"`

	CreatedAt time.Time `json:"created_at"`
}

// CallLog is protocol metadata, not a customer message or downloadable attachment.
type CallLog struct {
	Source          string `json:"source,omitempty"`
	Kind            string `json:"kind"`
	Status          string `json:"status"`
	Incoming        bool   `json:"incoming"`
	DurationSeconds *int64 `json:"duration_seconds,omitempty"`
}

const ChatReplyKindHandoff = "handoff_notice"

type ChatHistory struct {
	Call                    *CallLog   `gorm:"column:call_log;serializer:json;type:text" json:"call,omitempty"`
	ParticipantJID          string     `gorm:"size:64" json:"participant_jid,omitempty"` // Original sender identity for native quotes, including groups/LID.
	ParticipantName         string     `gorm:"size:128" json:"participant_name,omitempty"`
	ID                      uint       `gorm:"primaryKey;index:idx_chat_agent_live_cursor,priority:3" json:"id"`
	AgentID                 uint       `gorm:"index;index:idx_chat_agent_live_cursor,priority:1;index:idx_chat_agent_wa_msg,priority:1;index:idx_chat_agent_sender_time,priority:1" json:"agent_id"`
	Sender                  string     `gorm:"index;size:32;index:idx_chat_agent_sender_time,priority:2" json:"sender"`
	Message                 string     `json:"message"`
	Reply                   string     `json:"reply"`
	ReplyKind               string     `gorm:"size:24;not null;default:''" json:"reply_kind,omitempty"`
	HandoffID               uint       `gorm:"not null;default:0" json:"handoff_id,omitempty"` // Links a notice to its original handoff, including failed sends.
	FromHuman               bool       `gorm:"not null;default:false" json:"from_human"`
	LiveIncoming            bool       `gorm:"not null;default:false;index:idx_chat_agent_live_cursor,priority:2" json:"-"`
	MediaType               string     `gorm:"size:16" json:"media_type"`
	MediaPath               string     `json:"-"`
	MediaMetadata           []byte     `gorm:"type:blob" json:"-"`
	MediaFetchStatus        string     `gorm:"size:24" json:"media_fetch_status,omitempty"`
	FileName                string     `json:"file_name"`
	Mimetype                string     `json:"mimetype"`
	ImageAnalysis           string     `gorm:"type:text" json:"image_analysis,omitempty"`
	ImageAnalysisStatus     string     `gorm:"size:24;index" json:"image_analysis_status,omitempty"` // completed, failed
	ImageAnalysisModel      string     `gorm:"size:120" json:"image_analysis_model,omitempty"`
	ImageAnalysisConfidence float64    `json:"image_analysis_confidence,omitempty"`
	ImageAnalysisAnswer     string     `gorm:"type:text" json:"image_analysis_answer,omitempty"`
	ImageAnalysisProductID  uint       `gorm:"index" json:"image_analysis_product_id,omitempty"`
	ImageAnalysisNeedsHuman bool       `gorm:"not null;default:false;index" json:"image_analysis_needs_human,omitempty"`
	WAMsgID                 string     `gorm:"size:64;index:idx_chat_agent_wa_msg,priority:2" json:"wa_msg_id"`
	ReplyTo                 string     `json:"reply_to"`
	ReplyText               string     `gorm:"size:200" json:"reply_text"`
	Revoked                 bool       `gorm:"default:false" json:"revoked"`
	DeliveryStatus          string     `gorm:"size:24;index;default:sent" json:"delivery_status"` // sent, delivered, read, read_inferred, played, pending_retry, failed_send
	SendError               string     `gorm:"type:text" json:"send_error,omitempty"`
	RetryCount              int        `gorm:"not null;default:0" json:"retry_count"`
	NextRetryAt             *time.Time `gorm:"index" json:"next_retry_at,omitempty"`
	CreatedAt               time.Time  `gorm:"index:idx_chat_agent_sender_time,priority:3" json:"created_at"`
	MediaAvailable          bool       `gorm:"-" json:"media_available"`
	MediaDownloadable       bool       `gorm:"-" json:"media_downloadable"`
}

// InboxReadState menyimpan posisi baca operator di Inbox, terpisah dari read receipt WhatsApp.
type InboxReadState struct {
	ID         uint       `gorm:"primaryKey" json:"id"`
	AgentID    uint       `gorm:"uniqueIndex:idx_inbox_read_agent_sender;index:idx_inbox_read_agent_last,priority:1;not null" json:"agent_id"`
	Sender     string     `gorm:"uniqueIndex:idx_inbox_read_agent_sender;size:32;not null" json:"sender"`
	LastReadAt *time.Time `json:"last_read_at,omitempty"`
	// LastMsgAt = waktu pesan terakhir menurut WhatsApp (LastMsgTimestamp / timestamp pesan).
	// Dipakai untuk mengurutkan daftar chat agar selaras WhatsApp Web. Jangan ditimpa
	// oleh read-receipt (itu domain WhatsAppStateAt).
	LastMsgAt           *time.Time `gorm:"index;index:idx_inbox_read_agent_last,priority:2" json:"last_msg_at,omitempty"`
	UpdatedAt           time.Time  `json:"updated_at"`
	WhatsAppUnreadCount int        `gorm:"not null;default:0" json:"-"`
	WhatsAppSynced      bool       `gorm:"not null;default:false" json:"-"`
	WhatsAppStateAt     *time.Time `json:"-"`
	Pinned              bool       `gorm:"not null;default:false" json:"pinned"`
	PinChangedAt        *time.Time `json:"pin_changed_at,omitempty"`
}

type AITurn struct {
	ID                 uint    `gorm:"primaryKey" json:"id"`
	AgentID            uint    `gorm:"index;not null" json:"agent_id"`
	Sender             string  `gorm:"index;size:32" json:"sender"`
	UserMessage        string  `gorm:"type:text" json:"user_message"`
	AIReply            string  `gorm:"type:text" json:"ai_reply"`
	Model              string  `gorm:"size:80" json:"model"`
	ToolNames          string  `gorm:"type:text" json:"tool_names"`
	ToolCalls          int     `gorm:"not null;default:0" json:"tool_calls"`
	PromptVersion      string  `gorm:"size:40;default:'legacy';index" json:"prompt_version"`
	KnowledgeUsedCount int     `json:"knowledge_used_count"`
	KnowledgeIDs       string  `gorm:"size:255" json:"knowledge_ids"` // "12,45,90"
	TopSimilarity      float64 `json:"top_similarity"`                // 0..1, 0 bila keyword-only
	AnswerOverlap      float64 `json:"answer_overlap"`                // 0..1 overlap jawaban vs knowledge
	ProductUsedCount   int     `json:"product_used_count"`
	ProductIDs         string  `gorm:"size:255" json:"product_ids"`
	RetrievalMode      string  `gorm:"size:24;index" json:"retrieval_mode"` // none|keyword|semantic|hybrid
	// RetrievalQuery = query efektif ke knowledge (bukan selalu sama dengan user_message).
	RetrievalQuery    string    `gorm:"type:text" json:"retrieval_query"`
	GroundingRetried  bool      `gorm:"not null;default:false" json:"grounding_retried"`
	GroundingFallback bool      `gorm:"not null;default:false;index" json:"grounding_fallback"`
	ResponsePolicy    string    `gorm:"size:24;index" json:"response_policy"`
	ResponseRetried   bool      `gorm:"not null;default:false;index" json:"response_retried"`
	ResponseChars     int       `gorm:"not null;default:0" json:"response_chars"`
	UsedShippingTool  bool      `gorm:"not null;default:false;index" json:"used_shipping_tool"`
	Escalated         bool      `gorm:"not null;default:false;index" json:"escalated"`
	Error             string    `gorm:"type:text" json:"error"`
	LatencyMs         int64     `json:"latency_ms"`
	CreatedAt         time.Time `gorm:"index" json:"created_at"`
}

func (AITurn) TableName() string { return "ai_turns" }

type Contact struct {
	ID                      uint       `gorm:"primaryKey" json:"id"`
	AgentID                 uint       `gorm:"uniqueIndex:idx_contact_agent_number;not null" json:"agent_id"`
	Number                  string     `gorm:"uniqueIndex:idx_contact_agent_number;size:32;not null" json:"number"`
	Name                    string     `json:"name"`
	Notes                   string     `gorm:"type:text" json:"notes"`
	Tags                    string     `gorm:"type:text" json:"tags"`
	LeadStage               string     `gorm:"size:24;not null;default:new;index" json:"lead_stage"`
	LeadStageSource         string     `gorm:"size:24;not null;default:system;index" json:"lead_stage_source"`
	LeadStageReason         string     `gorm:"size:500" json:"lead_stage_reason"`
	LeadStageConfidence     float64    `gorm:"not null;default:0" json:"lead_stage_confidence"`
	LeadStageLocked         bool       `gorm:"not null;default:false;index" json:"lead_stage_locked"`
	LeadStageAnalyzedChatID uint       `gorm:"not null;default:0;index" json:"lead_stage_analyzed_chat_id"`
	LeadStageUpdatedAt      *time.Time `json:"lead_stage_updated_at"`
	ManualPauseUntil        *time.Time `gorm:"index" json:"manual_pause_until,omitempty"`
	UpdatedAt               time.Time  `json:"updated_at"`
}

// ConversationMemory menyimpan ringkasan percakapan jangka panjang per (agent, kontak).
// Dipisah per pengirim agar konteks satu customer tidak bocor ke customer lain.
type ConversationMemory struct {
	ID              uint       `gorm:"primaryKey" json:"id"`
	AgentID         uint       `gorm:"uniqueIndex:idx_convmem_agent_sender;not null" json:"agent_id"`
	Sender          string     `gorm:"uniqueIndex:idx_convmem_agent_sender;size:32;not null" json:"sender"`
	Summary         string     `gorm:"type:text" json:"summary"`
	LastChatID      uint       `gorm:"not null;default:0;index" json:"last_chat_id"`
	LastSummaryAt   *time.Time `json:"last_summary_at"`
	SummaryCursorAt *time.Time `json:"-"`
	HistoryRevision uint       `gorm:"not null;default:0" json:"-"`
	SummaryRevision uint       `gorm:"not null;default:0" json:"-"`
	SummaryComplete bool       `gorm:"not null;default:false" json:"-"`
	// BriefJSON = cache ringkasan inbox CS (structured). BriefChatID = id chat terakhir yang dianalisis.
	BriefJSON   string     `gorm:"type:text" json:"-"`
	BriefChatID uint       `gorm:"not null;default:0" json:"-"`
	BriefAt     *time.Time `json:"-"`
	UpdatedAt   time.Time  `json:"updated_at"`
}

type Handoff struct {
	ID             uint       `gorm:"primaryKey" json:"id"`
	AgentID        uint       `gorm:"index" json:"agent_id"`
	Sender         string     `gorm:"index;size:32" json:"sender"`
	LastMsg        string     `gorm:"type:text" json:"last_msg"`
	Status         string     `gorm:"size:24;default:waiting;index" json:"status"`
	Reason         string     `gorm:"size:64" json:"reason"`
	Summary        string     `gorm:"type:text" json:"summary"`
	AssignedUserID uint       `gorm:"index" json:"assigned_user_id"`
	ClaimedAt      *time.Time `json:"claimed_at,omitempty"`
	CreatedAt      time.Time  `json:"created_at"`
}

type Setting struct {
	ID           uint   `gorm:"primaryKey" json:"id"`
	SystemPrompt string `gorm:"type:text" json:"system_prompt"`
	AIModel      string `gorm:"default:deepseek-v4-pro" json:"ai_model"`
	Tone         string `gorm:"default:ramah" json:"tone"`
}

type Knowledge struct {
	ID        uint   `gorm:"primaryKey" json:"id"`
	AgentID   uint   `gorm:"index" json:"agent_id"`
	Question  string `gorm:"type:text" json:"question"`
	Answer    string `gorm:"type:text" json:"answer"`
	Tags      string `json:"tags"`
	Embedding string `gorm:"type:longtext" json:"-"`
	// EmbeddingModel = tanda tangan model+dimensi saat vektor dibuat (mis. "text-embedding-3-small"
	// atau "...:512"). Dipakai mendeteksi perubahan model agar knowledge di-embed ulang otomatis.
	EmbeddingModel string `gorm:"size:80" json:"-"`
	// Source = asal knowledge: manual, wizard, web, dokumen. SourceURL = URL halaman asal (untuk web).
	// Dipakai mengelompokkan & menghapus knowledge per sumber (mis. hapus semua dari 1 website).
	Source    string `gorm:"size:16;default:manual;index" json:"source"`
	SourceURL string `gorm:"type:text" json:"source_url"`
	// Derived from the reviewed revision lineage, never stored or client supplied.
	SubjectURL string `gorm:"-" json:"-"`
	ImagePath  string `gorm:"size:512" json:"image_path"`
	ImageMime  string `gorm:"size:64" json:"image_mime"`
	ImageURL   string `gorm:"-" json:"image_url,omitempty"`
	CharCount  int    `gorm:"not null;default:0" json:"char_count"` // panjang Answer, untuk hitung kuota karakter
	// Lifecycle fakta mencegah informasi lama/temporer tetap ikut retrieval.
	Active         bool       `gorm:"not null;default:true;index" json:"active"`
	ReviewStatus   string     `gorm:"size:24;not null;default:published;index" json:"review_status"`
	Scope          string     `gorm:"size:120;not null;default:general;index" json:"scope"`
	SupersedesID   uint       `gorm:"index" json:"supersedes_id"`
	ReviewedBy     uint       `json:"reviewed_by"`
	SourceExcerpt  string     `gorm:"type:text" json:"source_excerpt,omitempty"`
	Priority       int        `gorm:"not null;default:0;index" json:"priority"`
	VerifiedAt     *time.Time `gorm:"index" json:"verified_at,omitempty"`
	EffectiveFrom  *time.Time `gorm:"index" json:"effective_from,omitempty"`
	EffectiveUntil *time.Time `gorm:"index" json:"effective_until,omitempty"`
	CreatedAt      time.Time  `json:"created_at"`
	UpdatedAt      time.Time  `json:"updated_at"`
}

// BeforeSave menjaga CharCount selalu = panjang Answer (dipakai untuk kuota karakter),
// otomatis di semua jalur Create/Save tanpa perlu set manual di tiap handler.
func (k *Knowledge) BeforeSave(*gorm.DB) error {
	k.CharCount = len([]rune(k.Answer))
	return nil
}

// CrawlJob = satu sesi crawl website untuk satu agent (nomor). Berjalan di background;
// frontend polling statusnya. Semua data crawl di-scope ke agent_id agar tidak kecampur antar-nomor.
type CrawlJob struct {
	ID             uint       `gorm:"primaryKey" json:"id"`
	AgentID        uint       `gorm:"index;not null" json:"agent_id"`
	RootURL        string     `gorm:"type:text" json:"root_url"`
	Domain         string     `gorm:"size:255" json:"domain"`
	Status         string     `gorm:"size:16;index;default:pending" json:"status"` // pending, crawling, training, done, failed
	PagesFound     int        `gorm:"not null;default:0" json:"pages_found"`
	DraftCount     int64      `gorm:"-" json:"draft_count"`
	Error          string     `gorm:"type:text" json:"error"`
	PersonaUpdated bool       `gorm:"not null;default:false" json:"persona_updated"`
	PersonaError   string     `gorm:"type:text" json:"persona_error"`
	CreatedAt      time.Time  `json:"created_at"`
	FinishedAt     *time.Time `json:"finished_at"`
}

// CrawlPage = satu halaman hasil crawl. content disimpan agar bisa dilatih nanti tanpa fetch ulang.
type CrawlPage struct {
	ID        uint   `gorm:"primaryKey" json:"id"`
	JobID     uint   `gorm:"index;not null" json:"job_id"`
	AgentID   uint   `gorm:"index;not null" json:"agent_id"`
	URL       string `gorm:"type:text" json:"url"`
	Title     string `gorm:"type:text" json:"title"`
	Status    string `gorm:"size:16;index;default:found" json:"status"` // found, crawled, failed, training, trained, skipped
	CharCount int    `gorm:"not null;default:0" json:"char_count"`
	Content   string `gorm:"type:longtext" json:"-"` // teks bersih (tidak dikirim ke frontend, bisa besar)
	Error     string `gorm:"type:text" json:"error"`
	// Computed on read; no schema change or rewrite of old crawl content.
	CanTrain       bool   `gorm:"-" json:"can_train"`
	ContentIssue   string `gorm:"-" json:"content_issue,omitempty"`
	ContentPreview string `gorm:"-" json:"content_preview,omitempty"`
	// Recommended = layak auto-centang (skor multi-sinyal CS ≥ ambang).
	Recommended bool `gorm:"not null;default:false" json:"recommended"`
	// RecommendScore 0–100 dari algoritma ScorePageForCSTraining.
	RecommendScore int `gorm:"not null;default:0" json:"recommend_score"`
	// RecommendTier: skip | weak | good | strong
	RecommendTier string `gorm:"size:16;default:''" json:"recommend_tier"`
	// RecommendReason: alasan singkat dipisah " · " untuk UI.
	RecommendReason string     `gorm:"size:400" json:"recommend_reason"`
	TrainedAt       *time.Time `json:"trained_at"`
	CreatedAt       time.Time  `json:"created_at"`
}

type User struct {
	ID                  uint       `gorm:"primaryKey" json:"id"`
	Username            string     `gorm:"uniqueIndex;size:64;not null" json:"username"`
	Password            string     `json:"-"`
	TokenVersion        int        `gorm:"not null;default:0" json:"-"`
	Role                string     `gorm:"size:24;default:owner" json:"role"`
	Name                string     `json:"name"`
	Email               string     `gorm:"size:255" json:"email"`
	EmailVerified       bool       `gorm:"default:false" json:"email_verified"`
	EmailVerifyToken    string     `gorm:"size:128" json:"-"`
	EmailVerifyExpiry   *time.Time `json:"-"`
	Phone               string     `gorm:"size:32;index" json:"phone"`
	TenantID            *uint      `gorm:"index" json:"tenant_id"`
	IsSuperAdmin        bool       `gorm:"default:false" json:"is_super_admin"`
	Active              bool       `gorm:"not null;default:true" json:"active"`
	CanConnect          bool       `gorm:"not null;default:false" json:"can_connect"`
	CanManageAI         bool       `gorm:"not null;default:false" json:"can_manage_ai"`
	PasswordResetToken  string     `gorm:"size:128" json:"-"`
	PasswordResetExpiry *time.Time `json:"-"`
}

// WorkspacePermissions is computed for the signed-in user, never stored on an agent.
type WorkspacePermissions struct {
	Connect  bool `json:"connect"`
	ManageAI bool `json:"manage_ai"`
}

// UserAgentAssignment menentukan nomor WhatsApp mana yang boleh diakses akun CS.
// Owner/admin tenant tidak memerlukan assignment karena dapat mengakses semua agent.
type UserAgentAssignment struct {
	ID        uint      `gorm:"primaryKey" json:"id"`
	TenantID  uint      `gorm:"index;not null;uniqueIndex:idx_user_agent_assignment,priority:1" json:"tenant_id"`
	UserID    uint      `gorm:"index;not null;uniqueIndex:idx_user_agent_assignment,priority:2" json:"user_id"`
	AgentID   uint      `gorm:"index;not null;uniqueIndex:idx_user_agent_assignment,priority:3" json:"agent_id"`
	CreatedAt time.Time `json:"created_at"`
}

// CSActivityLog adalah audit trail tindakan operator manusia di Inbox.
type CSActivityLog struct {
	ID        uint      `gorm:"primaryKey" json:"id"`
	TenantID  uint      `gorm:"index;not null" json:"tenant_id"`
	UserID    uint      `gorm:"index;not null" json:"user_id"`
	AgentID   uint      `gorm:"index;not null" json:"agent_id"`
	Sender    string    `gorm:"size:32;index" json:"sender"`
	Action    string    `gorm:"size:32;index;not null" json:"action"`
	Detail    string    `gorm:"size:500" json:"detail"`
	CreatedAt time.Time `gorm:"index" json:"created_at"`
}

// LoginThrottle menyimpan rate-limit login secara persistent agar tidak hilang saat restart.
type LoginThrottle struct {
	ID          uint      `gorm:"primaryKey" json:"id"`
	Key         string    `gorm:"size:255;uniqueIndex;not null" json:"key"`
	Failures    int       `gorm:"not null;default:0" json:"failures"`
	FirstSeen   time.Time `gorm:"index" json:"first_seen"`
	LockedUntil time.Time `gorm:"index" json:"locked_until"`
	UpdatedAt   time.Time `json:"updated_at"`
}

// ClosingForm = skema data closing yang dikumpulkan AI per agent.
type ClosingForm struct {
	ID         uint      `gorm:"primaryKey" json:"id"`
	AgentID    uint      `gorm:"uniqueIndex;not null" json:"agent_id"`
	SchemaJSON string    `gorm:"type:text" json:"schema_json"` // JSON definisi field
	Enabled    bool      `gorm:"not null;default:true" json:"enabled"`
	CreatedAt  time.Time `json:"created_at"`
	UpdatedAt  time.Time `json:"updated_at"`
}

// ClosingRecord = satu data closing yang berhasil diekstrak AI.
type ClosingRecord struct {
	ID             uint       `gorm:"primaryKey" json:"id"`
	AgentID        uint       `gorm:"index;not null" json:"agent_id"`
	Sender         string     `gorm:"index;size:32" json:"sender"`
	Status         string     `gorm:"size:20;default:'detected'" json:"status"` // detected, exported, failed, duplicate
	Confidence     float64    `json:"confidence"`
	DataJSON       string     `gorm:"type:text" json:"data_json"`
	RawSummary     string     `gorm:"type:text" json:"raw_summary"`
	SheetError     string     `json:"sheet_error"`
	IdempotencyKey string     `gorm:"size:128;uniqueIndex" json:"idempotency_key"`
	SheetRow       int        `gorm:"default:0" json:"sheet_row"` // nomor baris di Google Sheet (untuk update-in-place)
	ExportedAt     *time.Time `json:"exported_at"`
	CreatedAt      time.Time  `json:"created_at"`
}

// ShippingCity = daftar kota/kabupaten dari RajaOngkir (cache lokal).
type ShippingCity struct {
	ID           uint   `gorm:"primaryKey" json:"id"`
	RajaOngkirID int    `gorm:"uniqueIndex" json:"rajaongkir_id"`
	Province     string `gorm:"size:100" json:"province"`
	Type         string `gorm:"size:20" json:"type"` // Kota / Kabupaten
	CityName     string `gorm:"size:100" json:"city_name"`
	FullName     string `gorm:"size:200" json:"full_name"` // "Kota Bandung"
	SearchText   string `gorm:"type:text" json:"-"`        // lowercase untuk search
}
