package mail

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"log/slog"

	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgxpool"

	"github.com/ultisuite/ulti-backend/internal/api/query"
	"github.com/ultisuite/ulti-backend/internal/filescan"
	"github.com/ultisuite/ulti-backend/internal/mail/credentials"
	"github.com/ultisuite/ulti-backend/internal/mail/imap"
	"github.com/ultisuite/ulti-backend/internal/mail/sanitize"
	"github.com/ultisuite/ulti-backend/internal/mail/storage"
	"github.com/ultisuite/ulti-backend/internal/mail/threading"
	"github.com/ultisuite/ulti-backend/internal/securityaudit"
)

var (
	ErrNotFound                  = errors.New("not found")
	ErrUserNotProvisioned        = errors.New("user not provisioned")
	ErrAccountNotFound           = errors.New("account not found")
	ErrCredentialsUnavailable    = errors.New("credentials encryption unavailable")
	ErrOAuthPasswordNotAllowed   = errors.New("password cannot be set on oauth account")
	ErrInvalidAccountCredentials = errors.New("account credentials invalid")
	ErrInvalidFolderScope        = errors.New("invalid folder scope")
	ErrFolderHasChildren         = errors.New("folder has children")
	ErrMalware                   = filescan.ErrMalicious
)

type Service struct {
	db                *pgxpool.Pool // exported via DB() for api token handlers
	credentials       *credentials.Manager
	audit             *securityaudit.Logger
	storage           *storage.Client
	attachmentsBucket string
	driveUploader     DriveUploader
	scanner           *filescan.Scanner
	logger            *slog.Logger
}

func NewService(db *pgxpool.Pool, audit *securityaudit.Logger, credentialManager *credentials.Manager, objectStorage *storage.Client, attachmentsBucket string) *Service {
	return &Service{
		db:                db,
		credentials:       credentialManager,
		audit:             audit,
		storage:           objectStorage,
		attachmentsBucket: attachmentsBucket,
		logger:            slog.Default().With("component", "mail-service"),
	}
}

func (s *Service) DB() *pgxpool.Pool {
	return s.db
}

func (s *Service) ResolveUserID(ctx context.Context, externalID string) (string, error) {
	var userID string
	err := s.db.QueryRow(ctx, `SELECT id FROM users WHERE external_id = $1`, externalID).Scan(&userID)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return "", ErrUserNotProvisioned
		}
		return "", err
	}
	return userID, nil
}

type AccountsList struct {
	Accounts   []map[string]any     `json:"accounts"`
	Pagination query.PaginationMeta `json:"pagination,omitempty"`
}

func (s *Service) ListAccounts(ctx context.Context, externalID string, params query.ListParams) (AccountsList, error) {
	var total int64
	err := s.db.QueryRow(ctx, `
		SELECT COUNT(*) FROM mail_accounts
		WHERE user_id = (SELECT id FROM users WHERE external_id = $1)
	`, externalID).Scan(&total)
	if err != nil {
		return AccountsList{}, err
	}

	rows, err := s.db.Query(ctx, `
		SELECT id, name, email, provider, imap_host, smtp_host, is_active, last_sync_at, created_at
		FROM mail_accounts WHERE user_id = (SELECT id FROM users WHERE external_id = $1)
		ORDER BY created_at ASC
		LIMIT $2 OFFSET $3
	`, externalID, params.Limit(), params.Offset())
	if err != nil {
		return AccountsList{}, err
	}
	defer rows.Close()

	accounts := make([]map[string]any, 0)
	for rows.Next() {
		var id, name, email, provider, imapHost, smtpHost string
		var isActive bool
		var lastSync, createdAt any
		if err := rows.Scan(&id, &name, &email, &provider, &imapHost, &smtpHost, &isActive, &lastSync, &createdAt); err != nil {
			return AccountsList{}, err
		}
		accounts = append(accounts, map[string]any{
			"id": id, "name": name, "email": email, "provider": provider,
			"imap_host": imapHost, "smtp_host": smtpHost, "is_active": isActive,
			"last_sync_at": lastSync, "created_at": createdAt,
		})
	}
	if err := rows.Err(); err != nil {
		return AccountsList{}, err
	}

	return AccountsList{
		Accounts:   accounts,
		Pagination: params.Meta(&total),
	}, nil
}

func (s *Service) CreateAccount(ctx context.Context, externalID string, req *createAccountRequest) (string, error) {
	return s.CreateAccountWithCredential(ctx, externalID, req, credentials.Credential{
		AuthType: credentials.AuthPassword,
		Username: req.Username,
		Password: req.Password,
	})
}

func (s *Service) CreateAccountWithCredential(ctx context.Context, externalID string, req *createAccountRequest, cred credentials.Credential) (string, error) {
	if s.credentials == nil {
		return "", ErrCredentialsUnavailable
	}
	encrypted, err := s.credentials.EncryptCredential(cred)
	if err != nil {
		return "", err
	}

	var id string
	err = s.db.QueryRow(ctx, `
		INSERT INTO mail_accounts (user_id, name, email, provider, imap_host, imap_port, imap_tls, smtp_host, smtp_port, smtp_tls, credentials)
		VALUES ((SELECT id FROM users WHERE external_id = $1), $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
		RETURNING id
	`, externalID, req.Name, req.Email, req.Provider, req.IMAPHost, req.IMAPPort, req.IMAPTLS, req.SMTPHost, req.SMTPPort, req.SMTPTLS, encrypted).Scan(&id)
	if err != nil {
		return "", err
	}
	if err := s.bootstrapAccountDefaults(ctx, externalID, id, req.Email, req.Name); err != nil {
		_, _ = s.db.Exec(ctx, `DELETE FROM mail_accounts WHERE id = $1`, id)
		return "", fmt.Errorf("bootstrap account defaults: %w", err)
	}
	return id, nil
}

func (s *Service) DeleteAccount(ctx context.Context, externalID, accountID string) error {
	userID, err := s.ResolveUserID(ctx, externalID)
	if err != nil {
		return err
	}
	result, err := s.db.Exec(ctx, `DELETE FROM mail_accounts WHERE id = $1 AND user_id = $2`, accountID, userID)
	if err != nil {
		return err
	}
	if result.RowsAffected() == 0 {
		return ErrNotFound
	}
	if s.audit != nil {
		s.audit.Log(ctx, externalID, securityaudit.ActionCriticalDeletion, map[string]any{
			"target": "mail_account", "account_id": accountID,
		})
	}
	return nil
}

type MessageListFilter struct {
	Folder            string
	AccountID         string
	ScopedAccountIDs  []string
}

type MessagesList struct {
	Messages   []map[string]any     `json:"messages"`
	Page       int                  `json:"page"`
	Pagination query.PaginationMeta `json:"pagination,omitempty"`
}

func (s *Service) ListMessages(ctx context.Context, externalID string, filter MessageListFilter, params query.ListParams) (MessagesList, error) {
	baseQuery := `
		FROM messages m
		JOIN mail_accounts ma ON m.account_id = ma.id
		WHERE ma.user_id = (SELECT id FROM users WHERE external_id = $1)
	`
	args := []any{externalID}
	argIdx := 2

	baseQuery, args, argIdx = appendMessageAccountScope(baseQuery, args, argIdx, filter.AccountID, filter.ScopedAccountIDs)
	if clause, arg, ok := folderFilterClause(filter.Folder, argIdx); ok {
		baseQuery += clause
		args = append(args, arg)
		argIdx++
	}

	var total int64
	countQuery := "SELECT COUNT(*) " + baseQuery
	if err := s.db.QueryRow(ctx, countQuery, args...).Scan(&total); err != nil {
		return MessagesList{}, err
	}

	listQuery := `
		SELECT m.id, m.message_id, m.thread_id, m.subject, m.from_addr, m.to_addrs, m.date, m.snippet, m.flags, m.labels, m.has_attachments,
		       left(m.body_text, 8192), left(m.body_html, 8192)
	` + baseQuery + fmt.Sprintf(" ORDER BY m.date DESC LIMIT $%d OFFSET $%d", argIdx, argIdx+1)
	args = append(args, params.Limit(), params.Offset())

	rows, err := s.db.Query(ctx, listQuery, args...)
	if err != nil {
		return MessagesList{}, err
	}
	defer rows.Close()

	messages := make([]map[string]any, 0)
	for rows.Next() {
		var id, messageID, subject, snippet string
		var threadID *string
		var fromAddr, toAddrs []byte
		var bodyTextSample, bodyHTMLSample string
		var date any
		var flags, labels []string
		var hasAttachments bool
		if err := rows.Scan(&id, &messageID, &threadID, &subject, &fromAddr, &toAddrs, &date, &snippet, &flags, &labels, &hasAttachments, &bodyTextSample, &bodyHTMLSample); err != nil {
			return MessagesList{}, err
		}
		bodyTextSample, bodyHTMLSample = imap.RepairStoredBodies(bodyTextSample, bodyHTMLSample)
		preview := imap.RepairSnippetWithBodies(snippet, bodyTextSample, bodyHTMLSample)
		if preview == "" {
			preview = imap.SnippetFromBodies(bodyTextSample, bodyHTMLSample, 200)
		}
		entry := map[string]any{
			"id": id, "message_id": messageID,
			"subject": imap.RepairSubject(subject, bodyTextSample, bodyHTMLSample, nil),
			"from":    json.RawMessage(fromAddr),
			"to":      json.RawMessage(toAddrs),
			"date":    date,
			"snippet": preview,
			"flags":   flags, "labels": labels, "has_attachments": hasAttachments,
		}
		if threadID != nil {
			entry["thread_id"] = *threadID
		}
		messages = append(messages, entry)
	}
	if err := rows.Err(); err != nil {
		return MessagesList{}, err
	}

	return MessagesList{
		Messages:   messages,
		Page:       params.Page,
		Pagination: params.Meta(&total),
	}, nil
}

func (s *Service) MessageAccountID(ctx context.Context, externalID, messageID string) (string, error) {
	var accountID string
	err := s.db.QueryRow(ctx, `
		SELECT m.account_id::text
		FROM messages m
		JOIN mail_accounts ma ON m.account_id = ma.id
		WHERE m.id = $1 AND ma.user_id = (SELECT id FROM users WHERE external_id = $2)
	`, messageID, externalID).Scan(&accountID)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return "", ErrNotFound
		}
		return "", err
	}
	return accountID, nil
}

func (s *Service) AttachmentAccountID(ctx context.Context, externalID, attachmentID string) (string, error) {
	var accountID string
	err := s.db.QueryRow(ctx, `
		SELECT m.account_id::text
		FROM attachments a
		JOIN messages m ON a.message_id = m.id
		JOIN mail_accounts ma ON m.account_id = ma.id
		WHERE a.id = $1 AND ma.user_id = (SELECT id FROM users WHERE external_id = $2)
	`, attachmentID, externalID).Scan(&accountID)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return "", ErrAttachmentNotFound
		}
		return "", err
	}
	return accountID, nil
}

func (s *Service) ThreadAccessible(ctx context.Context, externalID, threadID string, scopedAccountIDs []string) (bool, error) {
	if len(scopedAccountIDs) == 0 {
		return false, nil
	}
	var ok bool
	err := s.db.QueryRow(ctx, `
		SELECT EXISTS (
			SELECT 1
			FROM messages m
			JOIN mail_accounts ma ON m.account_id = ma.id
			WHERE m.thread_id = $1
			  AND ma.user_id = (SELECT id FROM users WHERE external_id = $2)
			  AND m.account_id = ANY($3)
		)
	`, threadID, externalID, scopedAccountIDs).Scan(&ok)
	return ok, err
}

func (s *Service) GetMessage(ctx context.Context, externalID, messageID string) (map[string]any, error) {
	var msg struct {
		ID         string
		AccountID  string
		MessageID  string
		ThreadID   *string
		InReplyTo  string
		References []string
		Subject    string
		From       []byte
		To         []byte
		Cc         []byte
		ReplyTo    []byte
		AuthInfo   []byte
		Date       any
		Text       string
		HTML       string
		Flags      []string
		Labels     []string
	}
	err := s.db.QueryRow(ctx, `
		SELECT m.id, m.account_id::text, m.message_id, m.thread_id, m.in_reply_to, m.references_header,
		       m.subject, m.from_addr, m.to_addrs, m.cc_addrs, m.reply_to, m.auth_info, m.date,
		       m.body_text, m.body_html, m.flags, m.labels
		FROM messages m JOIN mail_accounts ma ON m.account_id = ma.id
		WHERE m.id = $1 AND ma.user_id = (SELECT id FROM users WHERE external_id = $2)
	`, messageID, externalID).Scan(
		&msg.ID, &msg.AccountID, &msg.MessageID, &msg.ThreadID, &msg.InReplyTo, &msg.References,
		&msg.Subject, &msg.From, &msg.To, &msg.Cc, &msg.ReplyTo, &msg.AuthInfo, &msg.Date,
		&msg.Text, &msg.HTML, &msg.Flags, &msg.Labels,
	)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, ErrNotFound
		}
		return nil, err
	}
	bodyText, bodyHTML := imap.RepairStoredBodies(msg.Text, msg.HTML)
	subject := imap.RepairSubject(msg.Subject, bodyText, bodyHTML, nil)
	repairedSnippet := imap.RepairSnippetWithBodies(imap.SnippetFromBodies(bodyText, bodyHTML, 200), bodyText, bodyHTML)
	if bodyText != msg.Text || bodyHTML != msg.HTML || subject != msg.Subject {
		_, _ = s.db.Exec(ctx, `
			UPDATE messages SET body_text = $1, body_html = $2, snippet = $3, subject = $4, updated_at = NOW()
			WHERE id = $5
		`, bodyText, bodyHTML, repairedSnippet, subject, msg.ID)
	}
	out := map[string]any{
		"id": msg.ID, "account_id": msg.AccountID, "message_id": msg.MessageID, "subject": subject,
		"from": json.RawMessage(msg.From), "to": json.RawMessage(msg.To), "cc": json.RawMessage(msg.Cc),
		"reply_to": json.RawMessage(msg.ReplyTo), "auth_info": json.RawMessage(msg.AuthInfo),
		"date": msg.Date, "body_text": bodyText, "body_html": sanitize.SanitizeHTML(bodyHTML),
		"flags": msg.Flags, "labels": msg.Labels,
		"in_reply_to": msg.InReplyTo, "references": msg.References,
	}
	if msg.ThreadID != nil {
		out["thread_id"] = *msg.ThreadID
	}
	return out, nil
}

func (s *Service) UpdateLabels(ctx context.Context, externalID, messageID string, labels []string) error {
	result, err := s.db.Exec(ctx, `
		UPDATE messages m
		SET labels = $1, updated_at = NOW()
		FROM mail_accounts ma
		WHERE m.id = $2
		  AND m.account_id = ma.id
		  AND ma.user_id = (SELECT id FROM users WHERE external_id = $3)
	`, labels, messageID, externalID)
	if err != nil {
		return err
	}
	if result.RowsAffected() == 0 {
		return ErrNotFound
	}
	return nil
}

func (s *Service) UpdateFlags(ctx context.Context, externalID, messageID string, flags []string) error {
	result, err := s.db.Exec(ctx, `
		UPDATE messages m
		SET flags = $1, updated_at = NOW()
		FROM mail_accounts ma
		WHERE m.id = $2
		  AND m.account_id = ma.id
		  AND ma.user_id = (SELECT id FROM users WHERE external_id = $3)
	`, flags, messageID, externalID)
	if err != nil {
		return err
	}
	if result.RowsAffected() == 0 {
		return ErrNotFound
	}
	return nil
}

func (s *Service) DeleteMessage(ctx context.Context, externalID, messageID string) error {
	result, err := s.db.Exec(ctx, `
		DELETE FROM messages m
		USING mail_accounts ma
		WHERE m.id = $1
		  AND m.account_id = ma.id
		  AND ma.user_id = (SELECT id FROM users WHERE external_id = $2)
	`, messageID, externalID)
	if err != nil {
		return err
	}
	if result.RowsAffected() == 0 {
		return ErrNotFound
	}
	if s.audit != nil {
		s.audit.Log(ctx, externalID, securityaudit.ActionCriticalDeletion, map[string]any{
			"target": "message", "message_id": messageID,
		})
	}
	return nil
}

func (s *Service) GetThread(ctx context.Context, externalID, threadID string, scopedAccountIDs []string) (map[string]any, error) {
	base := `
		SELECT m.id, m.subject, m.from_addr, m.to_addrs, m.cc_addrs, m.date, m.snippet, m.flags, m.labels, m.has_attachments
		FROM messages m JOIN mail_accounts ma ON m.account_id = ma.id
		WHERE m.thread_id = $1 AND ma.user_id = (SELECT id FROM users WHERE external_id = $2)
	`
	args := []any{threadID, externalID}
	if scopedAccountIDs != nil {
		if len(scopedAccountIDs) == 0 {
			return map[string]any{"thread_id": threadID, "messages": []map[string]any{}}, nil
		}
		base += " AND m.account_id = ANY($3)"
		args = append(args, scopedAccountIDs)
	}
	base += " ORDER BY m.date ASC"
	rows, err := s.db.Query(ctx, base, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	messages := make([]map[string]any, 0)
	for rows.Next() {
		var id, subject, snippet string
		var from, toAddrs, ccAddrs []byte
		var date any
		var flags, labels []string
		var hasAttachments bool
		if err := rows.Scan(&id, &subject, &from, &toAddrs, &ccAddrs, &date, &snippet, &flags, &labels, &hasAttachments); err != nil {
			return nil, err
		}
		messages = append(messages, map[string]any{
			"id": id, "subject": subject, "from": json.RawMessage(from),
			"to": json.RawMessage(toAddrs), "cc": json.RawMessage(ccAddrs),
			"date": date, "snippet": snippet, "flags": flags, "labels": labels,
			"has_attachments": hasAttachments,
		})
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return map[string]any{"thread_id": threadID, "messages": messages}, nil
}

type replyParent struct {
	MessageID  string
	References []string
}

func (s *Service) loadReplyParent(ctx context.Context, userID, replyToMessageID string) (*replyParent, error) {
	var parent replyParent
	err := s.db.QueryRow(ctx, `
		SELECT m.message_id, m.references_header
		FROM messages m
		JOIN mail_accounts ma ON m.account_id = ma.id
		WHERE m.id = $1 AND ma.user_id = $2
	`, replyToMessageID, userID).Scan(&parent.MessageID, &parent.References)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, ErrNotFound
		}
		return nil, err
	}
	return &parent, nil
}

func (s *Service) SendMessage(ctx context.Context, userID string, req *sendMessageRequest) (id, status string, err error) {
	if req.IdempotencyKey != "" {
		err = s.db.QueryRow(ctx, `
			SELECT id, status FROM outbox
			WHERE user_id = $1 AND idempotency_key = $2
			  AND created_at > NOW() - INTERVAL '24 hours'
			ORDER BY created_at DESC
			LIMIT 1
		`, userID, req.IdempotencyKey).Scan(&id, &status)
		if err == nil {
			return id, status, nil
		}
		if !errors.Is(err, pgx.ErrNoRows) {
			return "", "", err
		}
	}

	toJSON, _ := json.Marshal(req.To)
	ccJSON, _ := json.Marshal(req.Cc)
	bccJSON, _ := json.Marshal(req.Bcc)

	inReplyTo := threading.NormalizeMessageID(req.InReplyTo)
	var references []string

	if req.ReplyToMessageID != "" {
		parent, err := s.loadReplyParent(ctx, userID, req.ReplyToMessageID)
		if err != nil {
			return "", "", err
		}
		inReplyTo = threading.NormalizeMessageID(parent.MessageID)
		references = threading.BuildReferences(parent.MessageID, parent.References)
	}

	status = "queued"
	if req.ScheduleAt != nil {
		status = "scheduled"
	}

	err = s.db.QueryRow(ctx, `
		INSERT INTO outbox (
			user_id, account_id, to_addrs, cc_addrs, bcc_addrs, subject,
			body_text, body_html, in_reply_to, references_header, status, scheduled_at, idempotency_key
		)
		SELECT $1, ma.id, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13
		FROM mail_accounts ma
		WHERE ma.id = $2 AND ma.user_id = $1
		RETURNING id
	`, userID, req.AccountID, toJSON, ccJSON, bccJSON, req.Subject, req.BodyText, req.BodyHTML,
		inReplyTo, references, status, req.ScheduleAt, req.IdempotencyKey).Scan(&id)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return "", "", ErrAccountNotFound
		}
		if req.IdempotencyKey != "" && isUniqueViolation(err) {
			err = s.db.QueryRow(ctx, `
				SELECT id, status FROM outbox
				WHERE user_id = $1 AND idempotency_key = $2
				ORDER BY created_at DESC
				LIMIT 1
			`, userID, req.IdempotencyKey).Scan(&id, &status)
			if err == nil {
				return id, status, nil
			}
		}
		return "", "", err
	}
	return id, status, nil
}

type RulesList struct {
	Rules      []map[string]any     `json:"rules"`
	Pagination query.PaginationMeta `json:"pagination,omitempty"`
}

func (s *Service) ListRules(ctx context.Context, externalID string, params query.ListParams) (RulesList, error) {
	var total int64
	err := s.db.QueryRow(ctx, `
		SELECT COUNT(*) FROM mail_rules WHERE user_id = (SELECT id FROM users WHERE external_id = $1)
	`, externalID).Scan(&total)
	if err != nil {
		return RulesList{}, err
	}

	rows, err := s.db.Query(ctx, `
		SELECT id, name, priority, conditions, actions, is_active, match_count, rule_kind, workflow
		FROM mail_rules WHERE user_id = (SELECT id FROM users WHERE external_id = $1)
		ORDER BY priority ASC
		LIMIT $2 OFFSET $3
	`, externalID, params.Limit(), params.Offset())
	if err != nil {
		return RulesList{}, err
	}
	defer rows.Close()

	rules := make([]map[string]any, 0)
	for rows.Next() {
		var id, name string
		var priority int
		var conditions, actions []byte
		var workflow []byte
		var ruleKind string
		var isActive bool
		var matchCount int64
		if err := rows.Scan(&id, &name, &priority, &conditions, &actions, &isActive, &matchCount, &ruleKind, &workflow); err != nil {
			return RulesList{}, err
		}
		rules = append(rules, map[string]any{
			"id": id, "name": name, "priority": priority,
			"conditions": json.RawMessage(conditions), "actions": json.RawMessage(actions),
			"is_active": isActive, "match_count": matchCount,
			"rule_kind": ruleKind, "workflow": json.RawMessage(workflow),
		})
	}
	if err := rows.Err(); err != nil {
		return RulesList{}, err
	}

	return RulesList{
		Rules:      rules,
		Pagination: params.Meta(&total),
	}, nil
}

func (s *Service) CreateRule(ctx context.Context, userID string, req *createRuleRequest) (string, error) {
	condJSON, _ := json.Marshal(req.Conditions)
	if req.Conditions == nil {
		condJSON = []byte("[]")
	}
	actJSON, _ := json.Marshal(req.Actions)
	if req.Actions == nil {
		actJSON = []byte("[]")
	}
	wfJSON, _ := json.Marshal(req.Workflow)
	ruleKind := req.RuleKind
	if ruleKind == "" {
		ruleKind = "rule"
	}

	if req.AccountID != "" {
		var exists bool
		err := s.db.QueryRow(ctx, `
			SELECT EXISTS(SELECT 1 FROM mail_accounts WHERE id = $1 AND user_id = $2)
		`, req.AccountID, userID).Scan(&exists)
		if err != nil {
			return "", err
		}
		if !exists {
			return "", ErrAccountNotFound
		}
	}

	var id string
	err := s.db.QueryRow(ctx, `
		INSERT INTO mail_rules (user_id, account_id, name, priority, conditions, actions, rule_kind, workflow)
		VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
		RETURNING id
	`, userID, nilIfEmpty(req.AccountID), req.Name, req.Priority, condJSON, actJSON, ruleKind, wfJSON).Scan(&id)
	if err != nil {
		return "", err
	}
	return id, nil
}

func (s *Service) UpdateRule(ctx context.Context, externalID, ruleID string, req *updateRuleRequest) error {
	condJSON, _ := json.Marshal(req.Conditions)
	if req.Conditions == nil {
		condJSON = []byte("[]")
	}
	actJSON, _ := json.Marshal(req.Actions)
	if req.Actions == nil {
		actJSON = []byte("[]")
	}
	wfJSON, _ := json.Marshal(req.Workflow)
	ruleKind := req.RuleKind
	if ruleKind == "" {
		ruleKind = "rule"
	}

	result, err := s.db.Exec(ctx, `
		UPDATE mail_rules SET name=$1, priority=$2, is_active=$3, conditions=$4, actions=$5, rule_kind=$6, workflow=$7, updated_at=NOW()
		WHERE id=$8 AND user_id=(SELECT id FROM users WHERE external_id=$9)
	`, req.Name, req.Priority, req.IsActive, condJSON, actJSON, ruleKind, wfJSON, ruleID, externalID)
	if err != nil {
		return err
	}
	if result.RowsAffected() == 0 {
		return ErrNotFound
	}
	return nil
}

func (s *Service) DeleteRule(ctx context.Context, externalID, ruleID string) error {
	result, err := s.db.Exec(ctx, `
		DELETE FROM mail_rules
		WHERE id = $1 AND user_id = (SELECT id FROM users WHERE external_id = $2)
	`, ruleID, externalID)
	if err != nil {
		return err
	}
	if result.RowsAffected() == 0 {
		return ErrNotFound
	}
	if s.audit != nil {
		s.audit.Log(ctx, externalID, securityaudit.ActionCriticalDeletion, map[string]any{
			"target": "mail_rule", "rule_id": ruleID,
		})
	}
	return nil
}

type WebhooksList struct {
	Webhooks   []map[string]any     `json:"webhooks"`
	Pagination query.PaginationMeta `json:"pagination,omitempty"`
}

func (s *Service) ListWebhooks(ctx context.Context, externalID string, params query.ListParams) (WebhooksList, error) {
	var total int64
	err := s.db.QueryRow(ctx, `
		SELECT COUNT(*) FROM webhook_templates
		WHERE user_id = (SELECT id FROM users WHERE external_id = $1)
	`, externalID).Scan(&total)
	if err != nil {
		return WebhooksList{}, err
	}

	rows, err := s.db.Query(ctx, `
		SELECT id, name, url, method, version, is_active, body_template, event_types, mail_scope, drive_scope, contacts_scope
		FROM webhook_templates
		WHERE user_id = (SELECT id FROM users WHERE external_id = $1)
		ORDER BY created_at ASC
		LIMIT $2 OFFSET $3
	`, externalID, params.Limit(), params.Offset())
	if err != nil {
		return WebhooksList{}, err
	}
	defer rows.Close()

	webhooks := make([]map[string]any, 0)
	for rows.Next() {
		var id, name, url, method, bodyTemplate string
		var version int
		var isActive bool
		var eventTypes, mailScope, driveScope, contactsScope []byte
		if err := rows.Scan(&id, &name, &url, &method, &version, &isActive, &bodyTemplate, &eventTypes, &mailScope, &driveScope, &contactsScope); err != nil {
			return WebhooksList{}, err
		}
		webhooks = append(webhooks, map[string]any{
			"id": id, "name": name, "url": url, "method": method, "version": version, "is_active": isActive,
			"body_template": bodyTemplate,
			"event_types":    jsonRawOrEmptyArray(eventTypes),
			"mail_scope":     jsonRawOrEmptyObject(mailScope),
			"drive_scope":    jsonRawOrEmptyObject(driveScope),
			"contacts_scope": jsonRawOrEmptyObject(contactsScope),
		})
	}
	if err := rows.Err(); err != nil {
		return WebhooksList{}, err
	}

	return WebhooksList{
		Webhooks:   webhooks,
		Pagination: params.Meta(&total),
	}, nil
}

func jsonRawOrEmptyArray(raw []byte) json.RawMessage {
	if len(raw) == 0 {
		return json.RawMessage("[]")
	}
	return json.RawMessage(raw)
}

func jsonRawOrEmptyObject(raw []byte) json.RawMessage {
	if len(raw) == 0 {
		return json.RawMessage("{}")
	}
	return json.RawMessage(raw)
}

func (s *Service) CreateWebhook(ctx context.Context, externalID string, req *createWebhookRequest, method string, maxRetries int) (string, error) {
	headersJSON, _ := json.Marshal(req.Headers)
	eventTypesJSON, err := marshalWebhookEventTypes(req.EventTypes)
	if err != nil {
		return "", err
	}
	mailScopeJSON, err := marshalWebhookMailScope(req.MailScope)
	if err != nil {
		return "", err
	}
	driveScopeJSON, err := marshalWebhookDriveScope(req.DriveScope)
	if err != nil {
		return "", err
	}
	contactsScopeJSON, err := marshalWebhookContactsScope(req.ContactsScope)
	if err != nil {
		return "", err
	}

	var id string
	err = s.db.QueryRow(ctx, `
		INSERT INTO webhook_templates (
			user_id, name, url, method, headers, body_template, version, signing_secret, max_retries,
			event_types, mail_scope, drive_scope, contacts_scope
		)
		VALUES (
			(SELECT id FROM users WHERE external_id = $1), $2, $3, $4, $5, $6, 1, $7, $8,
			$9, $10, $11, $12
		)
		RETURNING id
	`, externalID, req.Name, req.URL, method, headersJSON, req.BodyTemplate, req.SigningSecret, maxRetries,
		eventTypesJSON, mailScopeJSON, driveScopeJSON, contactsScopeJSON).Scan(&id)
	if err != nil {
		return "", err
	}
	if _, err := s.db.Exec(ctx, `
		INSERT INTO webhook_template_versions (template_id, version, method, headers, body_template)
		VALUES ($1, 1, $2, $3, $4)
	`, id, method, headersJSON, req.BodyTemplate); err != nil {
		return "", err
	}
	return id, nil
}

func (s *Service) DeleteWebhook(ctx context.Context, externalID, webhookID string) error {
	result, err := s.db.Exec(ctx, `
		DELETE FROM webhook_templates
		WHERE id = $1 AND user_id = (SELECT id FROM users WHERE external_id = $2)
	`, webhookID, externalID)
	if err != nil {
		return err
	}
	if result.RowsAffected() == 0 {
		return ErrNotFound
	}
	if s.audit != nil {
		s.audit.Log(ctx, externalID, securityaudit.ActionCriticalDeletion, map[string]any{
			"target": "webhook_template", "webhook_id": webhookID,
		})
	}
	return nil
}

func nilIfEmpty(s string) any {
	if s == "" {
		return nil
	}
	return s
}
