package db

import (
	"database/sql"
	"errors"
	"strings"
	"time"
)

// InsertMessage records a stored message's index row (the ciphertext itself already
// lives at storagePath — see internal/mailstore). Returns the new row's id, which
// doubles as the IMAP UID in later milestones.
func (d *DB) InsertMessage(mailboxID int64, folder, messageIDHeader, flags string, internalDate time.Time, sizeBytes int64, storagePath string, nonce []byte, cachedFrom, cachedTo, cachedSubject, cachedPreview string) (int64, error) {
	res, err := d.Exec(`INSERT INTO esrv_mailbox_messages
		(mailbox_id, folder, message_id_header, flags, internal_date, size_bytes, storage_path, nonce, cached_from, cached_to, cached_subject, cached_preview)
		VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		mailboxID, folder, messageIDHeader, flags, internalDate, sizeBytes, storagePath, nonce, cachedFrom, cachedTo, cachedSubject, cachedPreview)
	if err != nil {
		return 0, err
	}
	return res.LastInsertId()
}

const mailboxMessageColumns = `id, mailbox_id, folder, message_id_header, flags, internal_date, size_bytes, cached_from, cached_to, cached_subject, cached_preview, storage_path, nonce, created_at`

func scanMailboxMessage(scan func(dest ...any) error) (MailboxMessage, error) {
	var m MailboxMessage
	var internalDate, createdAt string
	err := scan(&m.ID, &m.MailboxID, &m.Folder, &m.MessageIDHeader, &m.Flags, &internalDate, &m.SizeBytes, &m.CachedFrom, &m.CachedTo, &m.CachedSubject, &m.CachedPreview, &m.StoragePath, &m.Nonce, &createdAt)
	if err != nil {
		return m, err
	}
	m.InternalDate, _ = parseTime(internalDate)
	m.CreatedAt, _ = parseTime(createdAt)
	return m, nil
}

func (d *DB) GetMessageByUID(mailboxID, uid int64) (*MailboxMessage, error) {
	row := d.QueryRow(`SELECT `+mailboxMessageColumns+` FROM esrv_mailbox_messages WHERE id = ? AND mailbox_id = ?`, uid, mailboxID)
	m, err := scanMailboxMessage(row.Scan)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, nil
		}
		return nil, err
	}
	return &m, nil
}

func (d *DB) DeleteMessage(mailboxID, uid int64) error {
	_, err := d.Exec(`DELETE FROM esrv_mailbox_messages WHERE id = ? AND mailbox_id = ?`, uid, mailboxID)
	return err
}

// MoveMessage reassigns a message to a different folder — pure metadata change, the
// on-disk ciphertext at storage_path never moves.
func (d *DB) MoveMessage(mailboxID, uid int64, newFolder string) error {
	_, err := d.Exec(`UPDATE esrv_mailbox_messages SET folder = ? WHERE id = ? AND mailbox_id = ?`, newFolder, uid, mailboxID)
	return err
}

// MoveMessageToTrash moves a message to Trash, first capturing its current folder
// into restore_folder so RestoreMessage can put it back later (mirrors
// db.MoveFolderToTrash's identical capture for folders).
func (d *DB) MoveMessageToTrash(mailboxID, uid int64) error {
	var folder string
	if err := d.QueryRow(`SELECT folder FROM esrv_mailbox_messages WHERE id = ? AND mailbox_id = ?`, uid, mailboxID).Scan(&folder); err != nil {
		return err
	}
	_, err := d.Exec(`UPDATE esrv_mailbox_messages SET restore_folder = ?, folder = 'Trash' WHERE id = ? AND mailbox_id = ?`, folder, uid, mailboxID)
	return err
}

// RestoreMessage moves a message back to the folder it was in immediately before
// being moved to Trash (see MoveMessageToTrash), or INBOX if that's unknown (e.g. a
// message trashed before this feature existed, or moved to Trash some other way).
func (d *DB) RestoreMessage(mailboxID, uid int64) error {
	var restoreFolder string
	if err := d.QueryRow(`SELECT restore_folder FROM esrv_mailbox_messages WHERE id = ? AND mailbox_id = ?`, uid, mailboxID).Scan(&restoreFolder); err != nil {
		return err
	}
	if restoreFolder == "" {
		restoreFolder = "INBOX"
	}
	_, err := d.Exec(`UPDATE esrv_mailbox_messages SET folder = ?, restore_folder = '' WHERE id = ? AND mailbox_id = ?`, restoreFolder, uid, mailboxID)
	return err
}

// ListMessageUIDsForMailbox returns every stored message's UID for mailboxID — used by
// mailbox removal to delete each one's on-disk ciphertext via mailstore before the
// mailbox row itself is removed.
func (d *DB) ListMessageUIDsForMailbox(mailboxID int64) ([]int64, error) {
	rows, err := d.Query(`SELECT id FROM esrv_mailbox_messages WHERE mailbox_id = ? ORDER BY id ASC`, mailboxID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []int64
	for rows.Next() {
		var id int64
		if err := rows.Scan(&id); err != nil {
			return nil, err
		}
		out = append(out, id)
	}
	return out, rows.Err()
}

func scanMailboxMessages(rows *sql.Rows) ([]MailboxMessage, error) {
	defer rows.Close()
	var out []MailboxMessage
	for rows.Next() {
		m, err := scanMailboxMessage(rows.Scan)
		if err != nil {
			return nil, err
		}
		out = append(out, m)
	}
	return out, rows.Err()
}

// ListMessagesForMailbox returns every stored message's full row for mailboxID,
// ordered ascending by UID (id) — this ordering IS the IMAP sequence-number mapping
// (index+1 == seqNum) that internal/imapserver relies on.
func (d *DB) ListMessagesForMailbox(mailboxID int64) ([]MailboxMessage, error) {
	rows, err := d.Query(`SELECT `+mailboxMessageColumns+` FROM esrv_mailbox_messages WHERE mailbox_id = ? ORDER BY id ASC`, mailboxID)
	if err != nil {
		return nil, err
	}
	return scanMailboxMessages(rows)
}

// UpdateMessageCachedFields overwrites a message's cached_from/cached_to/
// cached_subject/cached_preview — the display-only fields derived from the message's
// own content at store time. Used by mailstore.RebuildMessageCache to re-derive them
// for messages stored before a caching fix/addition landed (those fields are
// otherwise only ever computed once, at delivery time, never retroactively).
func (d *DB) UpdateMessageCachedFields(id int64, cachedFrom, cachedTo, cachedSubject, cachedPreview string) error {
	_, err := d.Exec(`UPDATE esrv_mailbox_messages SET cached_from = ?, cached_to = ?, cached_subject = ?, cached_preview = ? WHERE id = ?`,
		cachedFrom, cachedTo, cachedSubject, cachedPreview, id)
	return err
}

// SetMessageFlags overwrites a message's stored IMAP flags (space-separated), scoped
// to mailboxID so a session can't touch another mailbox's message by guessing a UID.
func (d *DB) SetMessageFlags(mailboxID, uid int64, flags string) error {
	_, err := d.Exec(`UPDATE esrv_mailbox_messages SET flags = ? WHERE id = ? AND mailbox_id = ?`, flags, uid, mailboxID)
	return err
}

// ListMessagesInFolder is ListMessagesForMailbox scoped to one folder — internal/imapserver
// uses this (not the unscoped version) so a filter rule's move_to_folder action produces
// mail that's actually browsable in its own folder, not mixed into every SELECT.
func (d *DB) ListMessagesInFolder(mailboxID int64, folder string) ([]MailboxMessage, error) {
	rows, err := d.Query(`SELECT `+mailboxMessageColumns+` FROM esrv_mailbox_messages WHERE mailbox_id = ? AND folder = ? ORDER BY id ASC`, mailboxID, folder)
	if err != nil {
		return nil, err
	}
	return scanMailboxMessages(rows)
}

// ListMessagesInFolderPage is ListMessagesInFolder with newest-first pagination, for
// the webmail client's folder view — a mailbox can accumulate far more mail than is
// reasonable to render in one page.
// sortColumnAndDir maps the folder view's ?sort=/&dir= query params to a safe,
// hardcoded SQL ORDER BY fragment — never interpolates the raw query values
// themselves, only picks between two known-safe literals, so this stays injection-safe
// however sort/dir arrive from the URL. "date" (the default) sorts by id, which tracks
// insertion/received order — the same ordering ListMessagesInFolderPage always used,
// just now also selectable ascending.
func sortColumnAndDir(sortBy, sortDir string) string {
	col := "id"
	if sortBy == "from" {
		col = "cached_from"
	}
	dir := "DESC"
	if sortDir == "asc" {
		dir = "ASC"
	}
	// Tie-break on id in the same direction so same-sender/same-instant rows still
	// have a stable, deterministic order across pages.
	return col + " " + dir + ", id " + dir
}

func (d *DB) ListMessagesInFolderPage(mailboxID int64, folder string, unreadOnly, starredOnly bool, sortBy, sortDir string, offset, limit int) ([]MailboxMessage, error) {
	query := `SELECT ` + mailboxMessageColumns + ` FROM esrv_mailbox_messages WHERE mailbox_id = ? AND folder = ?`
	args := []any{mailboxID, folder}
	if unreadOnly {
		query += ` AND flags NOT LIKE '%\Seen%' ESCAPE '\'`
	}
	if starredOnly {
		query += ` AND flags LIKE '%\Flagged%' ESCAPE '\'`
	}
	query += ` ORDER BY ` + sortColumnAndDir(sortBy, sortDir) + ` LIMIT ? OFFSET ?`
	args = append(args, limit, offset)
	rows, err := d.Query(query, args...)
	if err != nil {
		return nil, err
	}
	return scanMailboxMessages(rows)
}

// CountMessagesInFolder backs ListMessagesInFolderPage's pagination controls.
func (d *DB) CountMessagesInFolder(mailboxID int64, folder string, unreadOnly, starredOnly bool) (int, error) {
	query := `SELECT COUNT(*) FROM esrv_mailbox_messages WHERE mailbox_id = ? AND folder = ?`
	args := []any{mailboxID, folder}
	if unreadOnly {
		query += ` AND flags NOT LIKE '%\Seen%' ESCAPE '\'`
	}
	if starredOnly {
		query += ` AND flags LIKE '%\Flagged%' ESCAPE '\'`
	}
	var n int
	err := d.QueryRow(query, args...).Scan(&n)
	return n, err
}

// MarkAllReadInFolder adds \Seen to every currently-unread message in one folder —
// the folder-context-menu "Mark all as read" action's bulk equivalent of opening each
// message individually. Messages that already have other flags (e.g. \Flagged) keep
// them; only \Seen is added.
func (d *DB) MarkAllReadInFolder(mailboxID int64, folder string) error {
	_, err := d.Exec(`UPDATE esrv_mailbox_messages SET flags = TRIM(flags || ' \Seen')
		WHERE mailbox_id = ? AND folder = ? AND flags NOT LIKE '%\Seen%' ESCAPE '\'`, mailboxID, folder)
	return err
}

// ToggleMessageStarred flips \Flagged (IMAP's standard "starred/important" flag) on
// one message — reusing the existing flags column instead of a new schema column
// keeps this consistent with a desktop IMAP client's own star/flag button acting on
// the same message.
func (d *DB) ToggleMessageStarred(mailboxID, uid int64) error {
	msg, err := d.GetMessageByUID(mailboxID, uid)
	if err != nil || msg == nil {
		return err
	}
	var newFlags string
	if isStarred(msg.Flags) {
		newFlags = strings.TrimSpace(strings.ReplaceAll(msg.Flags, `\Flagged`, ""))
	} else {
		newFlags = strings.TrimSpace(msg.Flags + ` \Flagged`)
	}
	return d.SetMessageFlags(mailboxID, uid, newFlags)
}

// isStarred reports whether flags (the same space-separated IMAP flags string
// isUnread checks in internal/webui) includes \Flagged.
func isStarred(flags string) bool {
	for _, f := range strings.Fields(flags) {
		if f == `\Flagged` {
			return true
		}
	}
	return false
}

// escapeLike backslash-escapes a user-supplied LIKE pattern's own special characters
// (%, _, and the escape character itself) so a search for e.g. "50% off" or a
// filename with an underscore doesn't get interpreted as a wildcard.
func escapeLike(s string) string {
	r := strings.NewReplacer(`\`, `\\`, `%`, `\%`, `_`, `\_`)
	return r.Replace(s)
}

// SearchMessagesInFolder finds messages whose cached subject/from/to contain query
// (case-insensitive substring, not a full-text index — see the webmail search
// handler's doc comment for why that's the deliberate scope here), newest first.
// folder == "" searches every folder in the mailbox.
func (d *DB) SearchMessagesInFolder(mailboxID int64, folder, query string, offset, limit int) ([]MailboxMessage, error) {
	like := "%" + escapeLike(query) + "%"
	args := []any{mailboxID}
	folderClause := ""
	if folder != "" {
		folderClause = "AND folder = ? "
		args = append(args, folder)
	}
	args = append(args, like, like, like, limit, offset)
	rows, err := d.Query(`SELECT `+mailboxMessageColumns+` FROM esrv_mailbox_messages
		WHERE mailbox_id = ? `+folderClause+`AND (cached_subject LIKE ? ESCAPE '\' OR cached_from LIKE ? ESCAPE '\' OR cached_to LIKE ? ESCAPE '\')
		ORDER BY id DESC LIMIT ? OFFSET ?`, args...)
	if err != nil {
		return nil, err
	}
	return scanMailboxMessages(rows)
}

// CountSearchMessagesInFolder backs SearchMessagesInFolder's pagination controls.
func (d *DB) CountSearchMessagesInFolder(mailboxID int64, folder, query string) (int, error) {
	like := "%" + escapeLike(query) + "%"
	args := []any{mailboxID}
	folderClause := ""
	if folder != "" {
		folderClause = "AND folder = ? "
		args = append(args, folder)
	}
	args = append(args, like, like, like)
	var n int
	err := d.QueryRow(`SELECT COUNT(*) FROM esrv_mailbox_messages
		WHERE mailbox_id = ? `+folderClause+`AND (cached_subject LIKE ? ESCAPE '\' OR cached_from LIKE ? ESCAPE '\' OR cached_to LIKE ? ESCAPE '\')`, args...).Scan(&n)
	return n, err
}

// CountUnreadByFolder returns every folder's unread count in one query (GROUP BY,
// not one query per folder) — mirrors isUnread's own check
// (internal/webui/webmail_mail.go) but done in SQL so the sidebar's badge counts are
// cheap to compute on every folder-view render without a full row fetch. A folder
// with zero unread messages simply has no entry in the returned map.
func (d *DB) CountUnreadByFolder(mailboxID int64) (map[string]int, error) {
	rows, err := d.Query(`SELECT folder, COUNT(*) FROM esrv_mailbox_messages
		WHERE mailbox_id = ? AND flags NOT LIKE '%\Seen%' GROUP BY folder`, mailboxID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	out := map[string]int{}
	for rows.Next() {
		var folder string
		var n int
		if err := rows.Scan(&folder, &n); err != nil {
			return nil, err
		}
		out[folder] = n
	}
	return out, rows.Err()
}

// CountMessagesByFolder returns every folder's total message count in one query — the
// total half of the sidebar's "total / unread" display, mirroring CountUnreadByFolder's
// shape exactly (a folder with zero messages simply has no entry in the returned map).
func (d *DB) CountMessagesByFolder(mailboxID int64) (map[string]int, error) {
	rows, err := d.Query(`SELECT folder, COUNT(*) FROM esrv_mailbox_messages
		WHERE mailbox_id = ? GROUP BY folder`, mailboxID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	out := map[string]int{}
	for rows.Next() {
		var folder string
		var n int
		if err := rows.Scan(&folder, &n); err != nil {
			return nil, err
		}
		out[folder] = n
	}
	return out, rows.Err()
}

// SuggestRecipients returns up to 10 distinct addresses (as originally cached — a
// display name like "Name <addr@example.com>" is kept as-is, not parsed apart, since
// that's exactly what a To/Cc/Bcc field already accepts) this mailbox has previously
// exchanged mail with — its own Sent "To" list plus INBOX "From" senders, plus its
// saved address book (esrv_mailbox_contacts, formatted the same "Name <addr>" way so
// a contact's name is searchable and shown, not just its address) — whose value
// contains prefix. Backs the compose recipient autocomplete.
func (d *DB) SuggestRecipients(mailboxID int64, prefix string) ([]string, error) {
	like := "%" + escapeLike(prefix) + "%"
	rows, err := d.Query(`
		SELECT addr FROM (
			SELECT cached_to AS addr FROM esrv_mailbox_messages WHERE mailbox_id = ? AND folder = 'Sent' AND cached_to != ''
			UNION
			SELECT cached_from AS addr FROM esrv_mailbox_messages WHERE mailbox_id = ? AND folder = 'INBOX' AND cached_from != ''
			UNION
			SELECT name || ' <' || email || '>' AS addr FROM esrv_mailbox_contacts WHERE mailbox_id = ?
		)
		WHERE addr LIKE ? ESCAPE '\'
		ORDER BY addr LIMIT 10`, mailboxID, mailboxID, mailboxID, like)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []string
	for rows.Next() {
		var addr string
		if err := rows.Scan(&addr); err != nil {
			return nil, err
		}
		out = append(out, addr)
	}
	return out, rows.Err()
}

// DistinctFoldersForMailbox returns every folder name that has at least one stored
// message, plus "INBOX" always (even if empty) — the folder list internal/imapserver's
// LIST command reports.
func (d *DB) DistinctFoldersForMailbox(mailboxID int64) ([]string, error) {
	rows, err := d.Query(`SELECT DISTINCT folder FROM esrv_mailbox_messages WHERE mailbox_id = ?`, mailboxID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	seen := map[string]bool{"INBOX": true}
	out := []string{"INBOX"}
	for rows.Next() {
		var folder string
		if err := rows.Scan(&folder); err != nil {
			return nil, err
		}
		if !seen[folder] {
			seen[folder] = true
			out = append(out, folder)
		}
	}
	return out, rows.Err()
}
