package db

import (
	"database/sql"
	"errors"
	"sort"
)

// StandardMailboxFolders are the folders every mailbox always has, regardless of
// whether they currently hold any mail — the single source of truth shared by the
// webmail UI (which always shows them in the sidebar) and the IMAP LIST handler
// (which must report them too, or a desktop client never learns Trash/Junk exist
// until a message happens to land in one). These 5 are always top-level and never
// renamed or reordered relative to each other — only custom folders nested under
// INBOX (or, after being deleted, under Trash — see webmailDeleteFolder) form a real
// tree; see the parent_id/parent_root doc comment on esrv_mailbox_folders.
//
// "Junk" (not "Spam") specifically because several desktop IMAP clients look for a
// folder literally named "Junk" to auto-recognize it, even though the SPECIAL-USE
// \Junk attribute (see specialUseAttrs, internal/imapserver) is supposed to make the
// exact name irrelevant — in practice not every client honors SPECIAL-USE reliably.
// See migrateSpamRenamedToJunk (schema.go) for existing mailboxes that already had a
// "Spam" folder before this rename.
var StandardMailboxFolders = []string{"INBOX", "Junk", "Sent", "Drafts", "Trash"}

func isStandardFolderName(name string) bool {
	for _, f := range StandardMailboxFolders {
		if f == name {
			return true
		}
	}
	return false
}

// AllFoldersForMailbox is mailboxID's full folder list: the standard folders above,
// plus every folder that either holds at least one message (DistinctFoldersForMailbox)
// or was explicitly created and is still empty (ListMailboxFolders) — a folder can
// exist via either path, sometimes both. This is a flat list (IMAP LIST, the "Move
// to…" dropdown, and any other non-tree consumer) — for the sidebar's actual tree
// structure, see FolderTree.
func (d *DB) AllFoldersForMailbox(mailboxID int64) ([]string, error) {
	fromMessages, err := d.DistinctFoldersForMailbox(mailboxID)
	if err != nil {
		return nil, err
	}
	explicit, err := d.ListMailboxFolders(mailboxID)
	if err != nil {
		return nil, err
	}
	seen := make(map[string]bool, len(StandardMailboxFolders)+len(fromMessages)+len(explicit))
	out := make([]string, 0, len(StandardMailboxFolders)+len(fromMessages)+len(explicit))
	add := func(f string) {
		if !seen[f] {
			seen[f] = true
			out = append(out, f)
		}
	}
	for _, f := range StandardMailboxFolders {
		add(f)
	}
	for _, f := range fromMessages {
		add(f)
	}
	for _, f := range explicit {
		add(f)
	}
	return out, nil
}

// unpositionedFolder is the position value a folder row gets when it exists for some
// other reason (CreateMailboxFolder, rename, becoming a parent) but has never
// actually been dragged — distinct from 0, which is a legitimate "dragged to the very
// top" position, so FolderPositions can tell "never ordered" apart from "explicitly
// ordered first".
const unpositionedFolder = -1

// FolderPositions returns mailboxID's saved sibling order as {folder name: position}
// — for folders that have ever been dragged to a specific spot among their siblings
// (see SetFolderOrder). A folder absent from the map has never been reordered.
// Positions are only ever compared between actual siblings (see FolderTree); the raw
// integer means nothing across different parents.
func (d *DB) FolderPositions(mailboxID int64) (map[string]int, error) {
	rows, err := d.Query(`SELECT name, position FROM esrv_mailbox_folders WHERE mailbox_id = ? AND position >= 0`, mailboxID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	out := map[string]int{}
	for rows.Next() {
		var name string
		var pos int
		if err := rows.Scan(&name, &pos); err != nil {
			return nil, err
		}
		out[name] = pos
	}
	return out, rows.Err()
}

// SetFolderOrder persists the drag-and-drop order of one set of siblings in one shot
// — order is that parent's full children list top to bottom (never the whole tree;
// positions only mean something relative to siblings, see FolderPositions).
func (d *DB) SetFolderOrder(mailboxID int64, order []string) error {
	tx, err := d.Begin()
	if err != nil {
		return err
	}
	defer tx.Rollback()
	for i, name := range order {
		if _, err := tx.Exec(`INSERT INTO esrv_mailbox_folders (mailbox_id, name, position) VALUES (?, ?, ?)
			ON CONFLICT(mailbox_id, name) DO UPDATE SET position = excluded.position`, mailboxID, name, i); err != nil {
			return err
		}
	}
	return tx.Commit()
}

// folderRow is one esrv_mailbox_folders row, as needed to resolve/rebuild the tree.
type folderRow struct {
	id         int64
	name       string
	parentID   sql.NullInt64
	parentRoot string
	position   int
}

func (d *DB) allFolderRows(mailboxID int64) ([]folderRow, error) {
	rows, err := d.Query(`SELECT id, name, parent_id, parent_root, position FROM esrv_mailbox_folders WHERE mailbox_id = ?`, mailboxID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []folderRow
	for rows.Next() {
		var r folderRow
		if err := rows.Scan(&r.id, &r.name, &r.parentID, &r.parentRoot, &r.position); err != nil {
			return nil, err
		}
		out = append(out, r)
	}
	return out, rows.Err()
}

// FolderParentMap returns, for every custom folder that has a row, its immediate
// parent's current display name (one of the 5 standard names, or another custom
// folder's current name) — a row with neither parent_id nor parent_root set (legacy
// data, or a dangling parent_id somehow) defaults to "INBOX", never crashes.
func (d *DB) FolderParentMap(mailboxID int64) (map[string]string, error) {
	all, err := d.allFolderRows(mailboxID)
	if err != nil {
		return nil, err
	}
	byID := make(map[int64]folderRow, len(all))
	for _, r := range all {
		byID[r.id] = r
	}
	out := make(map[string]string, len(all))
	for _, r := range all {
		switch {
		case r.parentRoot != "":
			out[r.name] = r.parentRoot
		case r.parentID.Valid:
			if p, ok := byID[r.parentID.Int64]; ok {
				out[r.name] = p.name
			} else {
				out[r.name] = "INBOX"
			}
		default:
			out[r.name] = "INBOX"
		}
	}
	return out, nil
}

// FolderRoot resolves which of the 5 standard folders name ultimately lives under —
// name itself if it's already standard, otherwise walking up the parent chain. Used
// to decide "is this folder (still) under INBOX" (renameable, can hold new
// subfolders) vs "under Trash" (a deleted folder's new home — see
// webmailDeleteFolder). A cycle or dangling reference (shouldn't happen) falls back
// to "INBOX" rather than looping forever.
func (d *DB) FolderRoot(mailboxID int64, name string) (string, error) {
	if isStandardFolderName(name) {
		return name, nil
	}
	parents, err := d.FolderParentMap(mailboxID)
	if err != nil {
		return "", err
	}
	seen := map[string]bool{}
	cur := name
	for {
		if isStandardFolderName(cur) {
			return cur, nil
		}
		if seen[cur] {
			return "INBOX", nil
		}
		seen[cur] = true
		next, ok := parents[cur]
		if !ok {
			return "INBOX", nil
		}
		cur = next
	}
}

// FolderSubtreeNames returns startName plus every descendant folder (any depth) —
// backs "Empty Trash" (Trash's whole subtree, since deleting a folder re-parents it
// there) and "Clean up Junk" (Junk never has children, so this is just ["Junk"]) when
// startName is one of the 5 standard folders, and permanently deleting or restoring
// one specific custom folder (plus whatever's nested under it) when it isn't.
func (d *DB) FolderSubtreeNames(mailboxID int64, startName string) ([]string, error) {
	all, err := d.allFolderRows(mailboxID)
	if err != nil {
		return nil, err
	}
	inSubtree := map[int64]bool{}
	for _, r := range all {
		if r.name == startName {
			inSubtree[r.id] = true
			break
		}
	}
	out := []string{startName}
	// Direct children referencing startName as their standard-root parent — only
	// matches when startName is itself one of the 5 standard folders.
	for _, r := range all {
		if r.parentRoot == startName {
			out = append(out, r.name)
			inSubtree[r.id] = true
		}
	}
	// Everything deeper nests via parent_id chains — repeat until a pass finds
	// nothing new, which correctly handles any depth without a recursive query.
	for {
		added := false
		for _, r := range all {
			if inSubtree[r.id] {
				continue
			}
			if r.parentID.Valid && inSubtree[r.parentID.Int64] {
				out = append(out, r.name)
				inSubtree[r.id] = true
				added = true
			}
		}
		if !added {
			break
		}
	}
	return out, nil
}

// DeleteFolderRows removes esrv_mailbox_folders records for the given names — used
// after permanently deleting a folder's messages (Empty Trash, Clean up Junk, or
// permanently deleting one Trash-nested folder), so a permanently-deleted folder
// doesn't linger as an empty shell in the sidebar the way a merely-emptied one does.
func (d *DB) DeleteFolderRows(mailboxID int64, names []string) error {
	if len(names) == 0 {
		return nil
	}
	tx, err := d.Begin()
	if err != nil {
		return err
	}
	defer tx.Rollback()
	for _, name := range names {
		if _, err := tx.Exec(`DELETE FROM esrv_mailbox_folders WHERE mailbox_id = ? AND name = ?`, mailboxID, name); err != nil {
			return err
		}
	}
	return tx.Commit()
}

// FolderNode is one node of the sidebar's actual folder tree — see FolderTree.
type FolderNode struct {
	Name       string
	Children   []*FolderNode
	Renameable bool // a custom folder still under INBOX (not INBOX itself, not under Trash)
	CanAddKid  bool // INBOX itself, or a custom folder still under INBOX
	UnderTrash bool // a custom folder that's been deleted into Trash — can be permanently deleted or restored
}

// FolderTree builds the mailbox's sidebar tree: the 5 standard folders as fixed-order
// roots (never reordered relative to each other — see StandardMailboxFolders), each
// with its custom-folder descendants nested underneath, siblings ordered by any saved
// drag position (FolderPositions), unpositioned ones keeping insertion order.
func (d *DB) FolderTree(mailboxID int64) ([]*FolderNode, error) {
	flat, err := d.AllFoldersForMailbox(mailboxID)
	if err != nil {
		return nil, err
	}
	parents, err := d.FolderParentMap(mailboxID)
	if err != nil {
		return nil, err
	}
	positions, err := d.FolderPositions(mailboxID)
	if err != nil {
		return nil, err
	}
	childrenOf := map[string][]string{}
	for _, f := range flat {
		if isStandardFolderName(f) {
			continue
		}
		p := parents[f]
		childrenOf[p] = append(childrenOf[p], f)
	}
	for parent := range childrenOf {
		kids := childrenOf[parent]
		sort.SliceStable(kids, func(i, j int) bool {
			pi, oki := positions[kids[i]]
			pj, okj := positions[kids[j]]
			if oki && okj {
				return pi < pj
			}
			return oki && !okj
		})
		childrenOf[parent] = kids
	}
	var build func(name string) *FolderNode
	build = func(name string) *FolderNode {
		root, _ := d.FolderRoot(mailboxID, name)
		n := &FolderNode{
			Name:       name,
			Renameable: !isStandardFolderName(name) && root == "INBOX",
			CanAddKid:  root == "INBOX",
			UnderTrash: !isStandardFolderName(name) && root == "Trash",
		}
		for _, childName := range childrenOf[name] {
			n.Children = append(n.Children, build(childName))
		}
		return n
	}
	roots := make([]*FolderNode, 0, len(StandardMailboxFolders))
	for _, r := range StandardMailboxFolders {
		roots = append(roots, build(r))
	}
	return roots, nil
}

// CreateMailboxFolder ensures a folder row exists, purely to hold position/rename
// metadata — idempotent (INSERT OR IGNORE), so calling it on an already-existing
// folder is a safe no-op that never clobbers that folder's real parent. Only used
// when the caller doesn't care about (or already knows) the parent — see
// CreateMailboxFolderUnder for actually creating a new folder under a chosen parent.
func (d *DB) CreateMailboxFolder(mailboxID int64, name string) error {
	_, err := d.Exec(`INSERT OR IGNORE INTO esrv_mailbox_folders (mailbox_id, name, position) VALUES (?, ?, ?)`, mailboxID, name, unpositionedFolder)
	return err
}

// CreateMailboxFolderUnder creates a brand-new custom folder as a child of parent —
// parent must already be valid (a standard name, or an existing custom folder with
// its own row) by the time this is called; see webmailAddFolder for the validation
// this relies on.
func (d *DB) CreateMailboxFolderUnder(mailboxID int64, name, parent string) error {
	if isStandardFolderName(parent) {
		_, err := d.Exec(`INSERT INTO esrv_mailbox_folders (mailbox_id, name, parent_id, parent_root, position) VALUES (?, ?, NULL, ?, ?)`,
			mailboxID, name, parent, unpositionedFolder)
		return err
	}
	var parentID int64
	if err := d.QueryRow(`SELECT id FROM esrv_mailbox_folders WHERE mailbox_id = ? AND name = ?`, mailboxID, parent).Scan(&parentID); err != nil {
		return err
	}
	_, err := d.Exec(`INSERT INTO esrv_mailbox_folders (mailbox_id, name, parent_id, parent_root, position) VALUES (?, ?, ?, '', ?)`,
		mailboxID, name, parentID, unpositionedFolder)
	return err
}

// MoveFolderToTrash re-parents a folder (and, since its descendants reference it by
// id rather than a materialized path, its whole subtree along with it) under Trash —
// this IS "delete a folder": see webmailDeleteFolder for why messages are never
// separately relocated. Captures the folder's current parent into
// restore_parent_id/restore_parent_root first, so RestoreFolder can put it back where
// it came from later.
func (d *DB) MoveFolderToTrash(mailboxID int64, name string) error {
	tx, err := d.Begin()
	if err != nil {
		return err
	}
	defer tx.Rollback()

	var id int64
	var parentID sql.NullInt64
	var parentRoot string
	err = tx.QueryRow(`SELECT id, parent_id, parent_root FROM esrv_mailbox_folders WHERE mailbox_id = ? AND name = ?`, mailboxID, name).
		Scan(&id, &parentID, &parentRoot)
	switch {
	case errors.Is(err, sql.ErrNoRows):
		// No row yet (a message-derived-only folder, never explicitly parented) — its
		// restore target defaults to INBOX, the same default FolderParentMap already
		// uses for a row-less folder.
		if _, err := tx.Exec(`INSERT INTO esrv_mailbox_folders (mailbox_id, name, parent_id, parent_root, restore_parent_id, restore_parent_root, position) VALUES (?, ?, NULL, 'Trash', NULL, 'INBOX', ?)`,
			mailboxID, name, unpositionedFolder); err != nil {
			return err
		}
	case err != nil:
		return err
	default:
		restoreRoot := parentRoot
		if !parentID.Valid && parentRoot == "" {
			restoreRoot = "INBOX"
		}
		if _, err := tx.Exec(`UPDATE esrv_mailbox_folders SET restore_parent_id = ?, restore_parent_root = ?, parent_id = NULL, parent_root = 'Trash' WHERE id = ?`,
			parentID, restoreRoot, id); err != nil {
			return err
		}
	}
	return tx.Commit()
}

// RestoreFolder re-parents a folder back to where it was the moment it was deleted
// (see MoveFolderToTrash's restore_parent_id/restore_parent_root capture), falling
// back to INBOX if that's no longer meaningful — its former parent has itself since
// been deleted into Trash too, so restoring underneath it would leave this folder
// looking un-restored (still inside Trash's subtree).
func (d *DB) RestoreFolder(mailboxID int64, name string) error {
	var restoreParentID sql.NullInt64
	var restoreParentRoot string
	if err := d.QueryRow(`SELECT restore_parent_id, restore_parent_root FROM esrv_mailbox_folders WHERE mailbox_id = ? AND name = ?`, mailboxID, name).
		Scan(&restoreParentID, &restoreParentRoot); err != nil {
		return err
	}

	parentID := restoreParentID
	parentRoot := restoreParentRoot
	if parentID.Valid {
		valid := false
		var parentName string
		if err := d.QueryRow(`SELECT name FROM esrv_mailbox_folders WHERE id = ?`, parentID.Int64).Scan(&parentName); err == nil {
			if root, err := d.FolderRoot(mailboxID, parentName); err == nil && root != "Trash" {
				valid = true
			}
		}
		if valid {
			parentRoot = ""
		} else {
			parentID = sql.NullInt64{}
			parentRoot = "INBOX"
		}
	} else if parentRoot == "" {
		parentRoot = "INBOX"
	}

	_, err := d.Exec(`UPDATE esrv_mailbox_folders SET parent_id = ?, parent_root = ?, restore_parent_id = NULL, restore_parent_root = '' WHERE mailbox_id = ? AND name = ?`,
		parentID, parentRoot, mailboxID, name)
	return err
}

// ListMailboxFolders returns a mailbox's explicitly-created custom folders — combine
// with DistinctFoldersForMailbox (message-derived) for the full folder list, since a
// folder can exist via either path (or both).
func (d *DB) ListMailboxFolders(mailboxID int64) ([]string, error) {
	rows, err := d.Query(`SELECT name FROM esrv_mailbox_folders WHERE mailbox_id = ? ORDER BY name`, mailboxID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []string
	for rows.Next() {
		var name string
		if err := rows.Scan(&name); err != nil {
			return nil, err
		}
		out = append(out, name)
	}
	return out, rows.Err()
}

// RenameMailboxFolder changes a folder's display name only — a single-row UPDATE is
// enough (no cascade needed): children reference their parent by row id, not by name,
// so they stay correctly nested with zero further writes.
func (d *DB) RenameMailboxFolder(mailboxID int64, oldName, newName string) error {
	tx, err := d.Begin()
	if err != nil {
		return err
	}
	defer tx.Rollback()
	if _, err := tx.Exec(`UPDATE esrv_mailbox_folders SET name = ? WHERE mailbox_id = ? AND name = ?`, newName, mailboxID, oldName); err != nil {
		return err
	}
	if _, err := tx.Exec(`UPDATE esrv_mailbox_messages SET folder = ? WHERE mailbox_id = ? AND folder = ?`, newName, mailboxID, oldName); err != nil {
		return err
	}
	return tx.Commit()
}
