package services import ( "context" "database/sql" "fmt" "time" ) type ChatService struct { DB *sql.DB Appwrite *AppwriteService } func NewChatService(db *sql.DB, appwrite *AppwriteService) *ChatService { return &ChatService{ DB: db, Appwrite: appwrite, } } type Message struct { ID string `json:"id"` ConversationID string `json:"conversationId"` SenderID string `json:"senderId"` Content string `json:"content"` CreatedAt time.Time `json:"createdAt"` IsMine bool `json:"isMine"` // Populated by handler/frontend logic } type Conversation struct { ID string `json:"id"` CandidateID string `json:"candidateId"` CompanyID string `json:"companyId"` JobID *string `json:"jobId"` LastMessage *string `json:"lastMessage"` LastMessageAt *time.Time `json:"lastMessageAt"` ParticipantName string `json:"participantName"` ParticipantAvatar string `json:"participantAvatar"` UnreadCount int `json:"unreadCount"` } func (s *ChatService) SendMessage(ctx context.Context, senderID, conversationID, content string) (*Message, error) { // 1. Insert into Postgres var msgID string var createdAt time.Time query := ` INSERT INTO messages (conversation_id, sender_id, content) VALUES ($1, $2, $3) RETURNING id, created_at ` err := s.DB.QueryRowContext(ctx, query, conversationID, senderID, content).Scan(&msgID, &createdAt) if err != nil { return nil, fmt.Errorf("failed to insert message: %w", err) } // 2. Update Conversation (async-ish if needed, but important for sorting) updateQuery := ` UPDATE conversations SET last_message = $1, last_message_at = $2, updated_at = $2 WHERE id = $3 ` if _, err := s.DB.ExecContext(ctx, updateQuery, content, createdAt, conversationID); err != nil { // Log error but assume message is sent fmt.Printf("Failed to update conversation: %v\n", err) } // 3. Push to Appwrite go func() { // Fire and forget for realtime err := s.Appwrite.PushMessage(context.Background(), msgID, conversationID, senderID, content) if err != nil { fmt.Printf("Appwrite push failed: %v\n", err) } }() return &Message{ ID: msgID, ConversationID: conversationID, SenderID: senderID, Content: content, CreatedAt: createdAt, }, nil } func (s *ChatService) ListMessages(ctx context.Context, conversationID string) ([]Message, error) { query := ` SELECT id, conversation_id, sender_id, content, created_at FROM messages WHERE conversation_id = $1 ORDER BY created_at ASC ` rows, err := s.DB.QueryContext(ctx, query, conversationID) if err != nil { return nil, err } defer rows.Close() var msgs []Message for rows.Next() { var m Message if err := rows.Scan(&m.ID, &m.ConversationID, &m.SenderID, &m.Content, &m.CreatedAt); err != nil { return nil, err } msgs = append(msgs, m) } return msgs, nil } // ListConversations lists conversations for a user (candidate or company admin) func (s *ChatService) ListConversations(ctx context.Context, userID, tenantID string, isCandidate bool) ([]Conversation, error) { var query string var args []interface{} if isCandidate { // User is candidate -> Fetch company info query = ` SELECT c.id, c.candidate_id, c.company_id, c.job_id, c.last_message, c.last_message_at, comp.name as participant_name FROM conversations c JOIN companies comp ON c.company_id = comp.id WHERE c.candidate_id = $1 ORDER BY c.last_message_at DESC NULLS LAST ` args = append(args, userID) } else if tenantID != "" { // User is company admin -> Fetch candidate info query = ` SELECT c.id, c.candidate_id, c.company_id, c.job_id, c.last_message, c.last_message_at, COALESCE(u.name, u.full_name, u.identifier) as participant_name FROM conversations c JOIN users u ON c.candidate_id = u.id WHERE c.company_id = $1 ORDER BY c.last_message_at DESC NULLS LAST ` args = append(args, tenantID) } else { return nil, fmt.Errorf("invalid context for listing conversations") } rows, err := s.DB.QueryContext(ctx, query, args...) if err != nil { return nil, err } defer rows.Close() var convs []Conversation for rows.Next() { var c Conversation if err := rows.Scan(&c.ID, &c.CandidateID, &c.CompanyID, &c.JobID, &c.LastMessage, &c.LastMessageAt, &c.ParticipantName); err != nil { return nil, err } convs = append(convs, c) } return convs, nil }