import GRDB

public final class MailStore: Sendable {
	let dbWriter: any DatabaseWriter

	public init(dbWriter: any DatabaseWriter) {
		self.dbWriter = dbWriter
	}

	// MARK: - Accounts

	public func insertAccount(_ account: AccountRecord) throws {
		try dbWriter.write { db in
			try account.insert(db)
		}
	}

	public func accounts() throws -> [AccountRecord] {
		try dbWriter.read { db in
			try AccountRecord.fetchAll(db)
		}
	}

	// MARK: - Mailboxes

	public func upsertMailbox(_ mailbox: MailboxRecord) throws {
		try dbWriter.write { db in
			try mailbox.save(db)
		}
	}

	public func mailboxes(accountId: String) throws -> [MailboxRecord] {
		try dbWriter.read { db in
			try MailboxRecord
				.filter(Column("accountId") == accountId)
				.order(Column("name"))
				.fetchAll(db)
		}
	}

	public func mailbox(id: String) throws -> MailboxRecord? {
		try dbWriter.read { db in
			try MailboxRecord.fetchOne(db, key: id)
		}
	}

	public func updateMailboxSync(id: String, uidValidity: Int, uidNext: Int) throws {
		try dbWriter.write { db in
			try db.execute(
				sql: "UPDATE mailbox SET uidValidity = ?, uidNext = ? WHERE id = ?",
				arguments: [uidValidity, uidNext, id]
			)
		}
	}

	// MARK: - Messages

	public func insertMessages(_ messages: [MessageRecord]) throws {
		try dbWriter.write { db in
			for message in messages {
				try message.save(db)
			}
		}
	}

	public func messages(mailboxId: String) throws -> [MessageRecord] {
		try dbWriter.read { db in
			try MessageRecord
				.filter(Column("mailboxId") == mailboxId)
				.order(Column("date").desc)
				.fetchAll(db)
		}
	}

	public func message(id: String) throws -> MessageRecord? {
		try dbWriter.read { db in
			try MessageRecord.fetchOne(db, key: id)
		}
	}

	public func messagesForThread(threadId: String) throws -> [MessageRecord] {
		try dbWriter.read { db in
			try MessageRecord.fetchAll(db, sql: """
				SELECT m.* FROM message m
				JOIN threadMessage tm ON tm.messageId = m.id
				WHERE tm.threadId = ?
				ORDER BY m.date ASC
				""", arguments: [threadId])
		}
	}

	public func updateFlags(messageId: String, isRead: Bool, isFlagged: Bool) throws {
		try dbWriter.write { db in
			try db.execute(
				sql: "UPDATE message SET isRead = ?, isFlagged = ? WHERE id = ?",
				arguments: [isRead, isFlagged, messageId]
			)
		}
	}

	public func storeBody(messageId: String, text: String?, html: String?) throws {
		try dbWriter.write { db in
			try db.execute(
				sql: "UPDATE message SET bodyText = ?, bodyHtml = ? WHERE id = ?",
				arguments: [text, html, messageId]
			)
		}
	}

	// MARK: - Threads

	public func threads(accountId: String) throws -> [ThreadRecord] {
		try dbWriter.read { db in
			try ThreadRecord
				.filter(Column("accountId") == accountId)
				.order(Column("lastDate").desc)
				.fetchAll(db)
		}
	}

	public func insertThread(_ thread: ThreadRecord) throws {
		try dbWriter.write { db in
			try thread.save(db)
		}
	}

	public func linkMessageToThread(threadId: String, messageId: String) throws {
		try dbWriter.write { db in
			try ThreadMessageRecord(threadId: threadId, messageId: messageId).save(db)
		}
	}

	public func updateThread(id: String, lastDate: String, messageCount: Int, subject: String?) throws {
		try dbWriter.write { db in
			try db.execute(
				sql: "UPDATE thread SET lastDate = ?, messageCount = ?, subject = COALESCE(?, subject) WHERE id = ?",
				arguments: [lastDate, messageCount, subject, id]
			)
		}
	}

	/// Returns all message IDs linked to a thread
	public func threadMessageIds(threadId: String) throws -> [String] {
		try dbWriter.read { db in
			try String.fetchAll(
				db,
				sql: "SELECT messageId FROM threadMessage WHERE threadId = ?",
				arguments: [threadId]
			)
		}
	}

	/// Finds thread IDs that contain any of the given message IDs (by RFC 5322 Message-ID)
	public func findThreadsByMessageIds(_ messageIds: Set<String>) throws -> [String] {
		guard !messageIds.isEmpty else { return [] }
		return try dbWriter.read { db in
			let placeholders = databaseQuestionMarks(count: messageIds.count)
			let sql = """
				SELECT DISTINCT tm.threadId
				FROM threadMessage tm
				JOIN message m ON m.id = tm.messageId
				WHERE m.messageId IN (\(placeholders))
				"""
			return try String.fetchAll(db, sql: sql, arguments: StatementArguments(Array(messageIds)))
		}
	}

	/// Merges multiple threads into one, keeping the first thread ID
	public func mergeThreads(_ threadIds: [String]) throws {
		guard threadIds.count > 1 else { return }
		let keepId = threadIds[0]
		let mergeIds = Array(threadIds.dropFirst())
		try dbWriter.write { db in
			for mergeId in mergeIds {
				try db.execute(
					sql: "UPDATE threadMessage SET threadId = ? WHERE threadId = ?",
					arguments: [keepId, mergeId]
				)
				try db.execute(
					sql: "DELETE FROM thread WHERE id = ?",
					arguments: [mergeId]
				)
			}
		}
	}

	// MARK: - Drafts

	public func insertDraft(_ draft: DraftRecord) throws {
		try dbWriter.write { db in
			try draft.insert(db)
		}
	}

	public func updateDraft(_ draft: DraftRecord) throws {
		try dbWriter.write { db in
			try draft.update(db)
		}
	}

	public func deleteDraft(id: String) throws {
		try dbWriter.write { db in
			_ = try DraftRecord.deleteOne(db, key: id)
		}
	}

	public func draft(id: String) throws -> DraftRecord? {
		try dbWriter.read { db in
			try DraftRecord.fetchOne(db, key: id)
		}
	}

	public func drafts(accountId: String) throws -> [DraftRecord] {
		try dbWriter.read { db in
			try DraftRecord
				.filter(Column("accountId") == accountId)
				.order(Column("updatedAt").desc)
				.fetchAll(db)
		}
	}

	// MARK: - Pending Actions

	public func insertPendingAction(_ action: PendingActionRecord) throws {
		try dbWriter.write { db in
			try action.insert(db)
		}
	}

	public func insertPendingActions(_ actions: [PendingActionRecord]) throws {
		try dbWriter.write { db in
			for action in actions {
				try action.insert(db)
			}
		}
	}

	public func pendingActions(accountId: String) throws -> [PendingActionRecord] {
		try dbWriter.read { db in
			try PendingActionRecord
				.filter(Column("accountId") == accountId)
				.order(Column("createdAt").asc)
				.fetchAll(db)
		}
	}

	public func deletePendingAction(id: String) throws {
		try dbWriter.write { db in
			_ = try PendingActionRecord.deleteOne(db, key: id)
		}
	}

	public func updatePendingAction(_ action: PendingActionRecord) throws {
		try dbWriter.write { db in
			try action.update(db)
		}
	}

	public func pendingActionCount(accountId: String) throws -> Int {
		try dbWriter.read { db in
			try PendingActionRecord
				.filter(Column("accountId") == accountId)
				.fetchCount(db)
		}
	}

	// MARK: - Mailbox Roles

	public func mailboxWithRole(_ role: String, accountId: String) throws -> MailboxRecord? {
		try dbWriter.read { db in
			try MailboxRecord
				.filter(Column("accountId") == accountId)
				.filter(Column("role") == role)
				.fetchOne(db)
		}
	}

	public func updateMailboxRole(id: String, role: String?) throws {
		try dbWriter.write { db in
			try db.execute(
				sql: "UPDATE mailbox SET role = ? WHERE id = ?",
				arguments: [role, id]
			)
		}
	}

	// MARK: - Message Mutations

	public func updateMessageMailbox(messageId: String, newMailboxId: String) throws {
		try dbWriter.write { db in
			try db.execute(
				sql: "UPDATE message SET mailboxId = ? WHERE id = ?",
				arguments: [newMailboxId, messageId]
			)
		}
	}

	public func deleteMessage(id: String) throws {
		try dbWriter.write { db in
			_ = try MessageRecord.deleteOne(db, key: id)
		}
	}

	public func messagesInThread(threadId: String, mailboxId: String) throws -> [MessageRecord] {
		try dbWriter.read { db in
			try MessageRecord.fetchAll(db, sql: """
				SELECT m.* FROM message m
				JOIN threadMessage tm ON tm.messageId = m.id
				WHERE tm.threadId = ? AND m.mailboxId = ?
				ORDER BY m.date ASC
				""", arguments: [threadId, mailboxId])
		}
	}

	/// Access the underlying database writer (for ValueObservation)
	public var databaseReader: any DatabaseReader {
		dbWriter
	}

	/// Access the underlying database writer (for TaskStore and other modules)
	public var databaseWriter: any DatabaseWriter {
		dbWriter
	}

	// MARK: - Labels

	public func insertLabel(_ label: LabelRecord) throws {
		try dbWriter.write { db in
			try label.insert(db)
		}
	}

	public func labels(accountId: String) throws -> [LabelRecord] {
		try dbWriter.read { db in
			try LabelRecord
				.filter(Column("accountId") == accountId)
				.order(Column("name"))
				.fetchAll(db)
		}
	}

	public func projectLabels(accountId: String) throws -> [LabelRecord] {
		try dbWriter.read { db in
			try LabelRecord
				.filter(Column("accountId") == accountId)
				.filter(Column("isProject") == true)
				.order(Column("name"))
				.fetchAll(db)
		}
	}

	public func deleteLabel(id: String) throws {
		try dbWriter.write { db in
			_ = try LabelRecord.deleteOne(db, key: id)
		}
	}

	public func labelByName(_ name: String, accountId: String) throws -> LabelRecord? {
		try dbWriter.read { db in
			try LabelRecord
				.filter(Column("accountId") == accountId)
				.filter(Column("name") == name)
				.fetchOne(db)
		}
	}

	// MARK: - Item Labels

	public func attachLabel(labelId: String, itemType: String, itemId: String) throws {
		try dbWriter.write { db in
			try ItemLabelRecord(labelId: labelId, itemType: itemType, itemId: itemId).save(db)
		}
	}

	public func detachLabel(labelId: String, itemType: String, itemId: String) throws {
		try dbWriter.write { db in
			try db.execute(
				sql: "DELETE FROM itemLabel WHERE labelId = ? AND itemType = ? AND itemId = ?",
				arguments: [labelId, itemType, itemId]
			)
		}
	}

	public func labelsForItem(itemType: String, itemId: String) throws -> [LabelRecord] {
		try dbWriter.read { db in
			try LabelRecord.fetchAll(db, sql: """
				SELECT l.* FROM label l
				JOIN itemLabel il ON il.labelId = l.id
				WHERE il.itemType = ? AND il.itemId = ?
				ORDER BY l.name
				""", arguments: [itemType, itemId])
		}
	}

	public func itemsWithLabel(labelId: String) throws -> [ItemLabelRecord] {
		try dbWriter.read { db in
			try ItemLabelRecord
				.filter(Column("labelId") == labelId)
				.fetchAll(db)
		}
	}

	// MARK: - Deferrals (email only)

	public func insertDeferral(_ deferral: DeferralRecord) throws {
		try dbWriter.write { db in
			try deferral.insert(db)
		}
	}

	public func deleteDeferral(id: String) throws {
		try dbWriter.write { db in
			_ = try DeferralRecord.deleteOne(db, key: id)
		}
	}

	public func deleteDeferralForMessage(messageId: String) throws {
		try dbWriter.write { db in
			try db.execute(
				sql: "DELETE FROM deferral WHERE messageId = ?",
				arguments: [messageId]
			)
		}
	}

	public func deferralForMessage(messageId: String) throws -> DeferralRecord? {
		try dbWriter.read { db in
			try DeferralRecord
				.filter(Column("messageId") == messageId)
				.fetchOne(db)
		}
	}

	public func expiredDeferrals(beforeDate: String) throws -> [DeferralRecord] {
		try dbWriter.read { db in
			try DeferralRecord
				.filter(Column("deferUntil") != nil)
				.filter(Column("deferUntil") <= beforeDate)
				.fetchAll(db)
		}
	}

	public func somedayDeferrals() throws -> [DeferralRecord] {
		try dbWriter.read { db in
			try DeferralRecord
				.filter(Column("deferUntil") == nil)
				.fetchAll(db)
		}
	}

	// MARK: - Task cache queries

	public func insertTask(_ task: TaskRecord) throws {
		try dbWriter.write { db in
			try task.insert(db)
		}
	}

	public func updateTask(_ task: TaskRecord) throws {
		try dbWriter.write { db in
			try task.update(db)
		}
	}

	public func deleteTask(id: String) throws {
		try dbWriter.write { db in
			_ = try TaskRecord.deleteOne(db, key: id)
		}
	}

	public func task(id: String) throws -> TaskRecord? {
		try dbWriter.read { db in
			try TaskRecord.fetchOne(db, key: id)
		}
	}

	public func inboxTasks(accountId: String) throws -> [TaskRecord] {
		try dbWriter.read { db in
			try TaskRecord
				.filter(Column("accountId") == accountId)
				.filter(Column("status") == "NEEDS-ACTION")
				.filter(Column("deferUntil") == nil)
				.filter(Column("isSomeday") == false)
				.order(Column("createdAt").desc)
				.fetchAll(db)
		}
	}

	public func todayTasks(accountId: String, todayDate: String) throws -> [TaskRecord] {
		try dbWriter.read { db in
			try TaskRecord
				.filter(Column("accountId") == accountId)
				.filter(Column("status") == "NEEDS-ACTION" || Column("status") == "IN-PROCESS")
				.filter(Column("dueDate") != nil)
				.filter(Column("dueDate") <= todayDate)
				.filter(Column("isSomeday") == false)
				.order(Column("priority").desc, Column("dueDate").asc)
				.fetchAll(db)
		}
	}

	public func upcomingTasks(accountId: String, afterDate: String) throws -> [TaskRecord] {
		try dbWriter.read { db in
			try TaskRecord
				.filter(Column("accountId") == accountId)
				.filter(Column("status") == "NEEDS-ACTION" || Column("status") == "IN-PROCESS")
				.filter(Column("dueDate") != nil)
				.filter(Column("dueDate") > afterDate)
				.filter(Column("isSomeday") == false)
				.order(Column("dueDate").asc)
				.fetchAll(db)
		}
	}

	public func somedayTasks(accountId: String) throws -> [TaskRecord] {
		try dbWriter.read { db in
			try TaskRecord
				.filter(Column("accountId") == accountId)
				.filter(Column("isSomeday") == true)
				.filter(Column("status") == "NEEDS-ACTION")
				.order(Column("createdAt").desc)
				.fetchAll(db)
		}
	}

	public func archivedTasks(accountId: String) throws -> [TaskRecord] {
		try dbWriter.read { db in
			try TaskRecord
				.filter(Column("accountId") == accountId)
				.filter(Column("status") == "COMPLETED" || Column("status") == "CANCELLED")
				.order(Column("updatedAt").desc)
				.fetchAll(db)
		}
	}

	public func tasksLinkedToThread(threadId: String) throws -> [TaskRecord] {
		try dbWriter.read { db in
			try TaskRecord.fetchAll(db, sql: """
				SELECT t.* FROM task t
				WHERE t.linkedMessageId IN (
					SELECT m.messageId FROM message m
					JOIN threadMessage tm ON tm.messageId = m.id
					WHERE tm.threadId = ?
				)
				ORDER BY t.createdAt ASC
				""", arguments: [threadId])
		}
	}

	public func expiredDeferredTasks(beforeDate: String) throws -> [TaskRecord] {
		try dbWriter.read { db in
			try TaskRecord
				.filter(Column("deferUntil") != nil)
				.filter(Column("deferUntil") <= beforeDate)
				.filter(Column("status") == "NEEDS-ACTION")
				.fetchAll(db)
		}
	}

	public func clearTaskDeferral(id: String) throws {
		try dbWriter.write { db in
			try db.execute(
				sql: "UPDATE task SET deferUntil = NULL WHERE id = ?",
				arguments: [id]
			)
		}
	}

	public func deleteAllTasks() throws {
		try dbWriter.write { db in
			_ = try TaskRecord.deleteAll(db)
		}
	}

	public func deleteAllItemLabelsForTasks() throws {
		try dbWriter.write { db in
			try db.execute(sql: "DELETE FROM itemLabel WHERE itemType = 'task'")
		}
	}
}
