import SwiftUI
import GRDB
import Models
import MailStore
import SyncEngine
import IMAPClient
import SMTPClient

@Observable
@MainActor
final class MailViewModel {
	private(set) var store: MailStore?
	private var coordinator: SyncCoordinator?
	var actionQueue: ActionQueue?
	var accountConfig: AccountConfig?

	var threads: [ThreadSummary] = []
	var selectedThread: ThreadSummary?
	var messages: [MessageSummary] = []
	var mailboxes: [MailboxInfo] = []
	var selectedMailbox: MailboxInfo?
	var syncState: SyncState = .idle
	var errorMessage: String?

	private var imapClientProvider: (() -> IMAPClient)?
	private var threadObservation: Task<Void, Never>?
	private var messageObservation: Task<Void, Never>?

	var hasAccount: Bool {
		store != nil && coordinator != nil
	}

	func setup(config: AccountConfig, credentials: Credentials) throws {
		let dbPath = Self.databasePath(for: config.id)
		let dbPool = try DatabaseSetup.openDatabase(atPath: dbPath)
		let mailStore = MailStore(dbWriter: dbPool)
		let imapClient = IMAPClient(
			host: config.imapHost,
			port: config.imapPort,
			credentials: credentials
		)
		store = mailStore
		accountConfig = config

		let capturedImapHost = config.imapHost
		let capturedImapPort = config.imapPort
		let capturedCredentials = credentials

		// Build smtpClientProvider when SMTP fields are present
		var smtpProvider: (@Sendable () -> SMTPClient)?
		if let smtpHost = config.smtpHost,
		   let smtpPort = config.smtpPort {
			let security = config.smtpSecurity ?? .starttls
			smtpProvider = {
				SMTPClient(
					host: smtpHost,
					port: smtpPort,
					security: security,
					credentials: capturedCredentials
				)
			}
		}

		let queue = ActionQueue(
			store: mailStore,
			accountId: config.id,
			imapClientProvider: {
				IMAPClient(
					host: capturedImapHost,
					port: capturedImapPort,
					credentials: capturedCredentials
				)
			},
			smtpClientProvider: smtpProvider
		)
		actionQueue = queue
		imapClientProvider = {
			IMAPClient(
				host: capturedImapHost,
				port: capturedImapPort,
				credentials: capturedCredentials
			)
		}

		coordinator = SyncCoordinator(
			accountConfig: config,
			imapClient: imapClient,
			store: mailStore,
			actionQueue: queue
		)
	}

	func loadMailboxes(accountId: String) async {
		guard let store else { return }
		do {
			let records = try store.mailboxes(accountId: accountId)
			mailboxes = records.map { record in
				MailboxInfo(
					id: record.id, accountId: record.accountId,
					name: record.name, unreadCount: 0, totalCount: 0
				)
			}
			if selectedMailbox == nil, let inbox = mailboxes.first(where: { $0.name.lowercased() == "inbox" }) {
				selectedMailbox = inbox
			}
		} catch {
			errorMessage = error.localizedDescription
		}
	}

	func startObservingThreads(accountId: String) {
		guard let store else { return }
		threadObservation?.cancel()
		threadObservation = Task {
			do {
				for try await summaries in store.observeThreadSummaries(accountId: accountId) {
					self.threads = summaries
				}
			} catch {
				if !Task.isCancelled {
					self.errorMessage = error.localizedDescription
				}
			}
		}
	}

	func selectThread(_ thread: ThreadSummary) {
		selectedThread = thread
		messageObservation?.cancel()
		guard let store else { return }
		messageObservation = Task {
			do {
				for try await msgs in store.observeMessages(threadId: thread.id) {
					self.messages = msgs
				}
			} catch {
				if !Task.isCancelled {
					self.errorMessage = error.localizedDescription
				}
			}
		}
	}

	func syncNow() async {
		guard let coordinator else { return }
		do {
			try await coordinator.syncNow()
			syncState = coordinator.syncState
		} catch {
			errorMessage = error.localizedDescription
			syncState = .error(error.localizedDescription)
		}
	}

	func startPeriodicSync() {
		coordinator?.startPeriodicSync()
	}

	func stopSync() {
		coordinator?.stopSync()
		threadObservation?.cancel()
		messageObservation?.cancel()
	}

	// MARK: - Triage Actions

	func archiveSelectedThread() {
		guard let store, let actionQueue, let selectedThread, let selectedMailbox,
			  let accountConfig else { return }
		do {
			guard let archiveMailbox = try store.mailboxWithRole("archive", accountId: accountConfig.id) else {
				errorMessage = "No archive mailbox found"
				return
			}
			let messages = try store.messagesInThread(threadId: selectedThread.id, mailboxId: selectedMailbox.id)
			for message in messages {
				try store.updateMessageMailbox(messageId: message.id, newMailboxId: archiveMailbox.id)
				let action = PendingAction(
					accountId: accountConfig.id,
					actionType: .move,
					payload: .move(uid: message.uid, from: selectedMailbox.name, to: archiveMailbox.name)
				)
				Task { try await actionQueue.enqueue(action) }
			}
			autoAdvance()
		} catch {
			errorMessage = error.localizedDescription
		}
	}

	func deleteSelectedThread() {
		guard let store, let actionQueue, let selectedThread, let selectedMailbox,
			  let accountConfig else { return }
		do {
			guard let trashMailbox = try store.mailboxWithRole("trash", accountId: accountConfig.id) else {
				errorMessage = "No trash mailbox found"
				return
			}
			let messages = try store.messagesInThread(threadId: selectedThread.id, mailboxId: selectedMailbox.id)
			let isAlreadyInTrash = selectedMailbox.id == trashMailbox.id
			for message in messages {
				if isAlreadyInTrash {
					try store.deleteMessage(id: message.id)
					let action = PendingAction(
						accountId: accountConfig.id,
						actionType: .delete,
						payload: .delete(uid: message.uid, mailbox: trashMailbox.name, trashMailbox: trashMailbox.name)
					)
					Task { try await actionQueue.enqueue(action) }
				} else {
					try store.updateMessageMailbox(messageId: message.id, newMailboxId: trashMailbox.id)
					let action = PendingAction(
						accountId: accountConfig.id,
						actionType: .move,
						payload: .move(uid: message.uid, from: selectedMailbox.name, to: trashMailbox.name)
					)
					Task { try await actionQueue.enqueue(action) }
				}
			}
			autoAdvance()
		} catch {
			errorMessage = error.localizedDescription
		}
	}

	func toggleFlagSelectedThread() {
		guard let store, let actionQueue, let selectedThread, let accountConfig else { return }
		do {
			let messages = try store.messagesForThread(threadId: selectedThread.id)
			guard let first = messages.first else { return }
			let newFlagged = !first.isFlagged
			for message in messages {
				try store.updateFlags(messageId: message.id, isRead: message.isRead, isFlagged: newFlagged)
				let add = newFlagged ? ["\\Flagged"] : [String]()
				let remove = newFlagged ? [String]() : ["\\Flagged"]
				let action = PendingAction(
					accountId: accountConfig.id,
					actionType: .setFlags,
					payload: .setFlags(uid: message.uid, mailbox: mailboxName(for: message.mailboxId) ?? "INBOX", add: add, remove: remove)
				)
				Task { try await actionQueue.enqueue(action) }
			}
		} catch {
			errorMessage = error.localizedDescription
		}
	}

	func toggleReadSelectedThread() {
		guard let store, let actionQueue, let selectedThread, let accountConfig else { return }
		do {
			let messages = try store.messagesForThread(threadId: selectedThread.id)
			guard let first = messages.first else { return }
			let newRead = !first.isRead
			for message in messages {
				try store.updateFlags(messageId: message.id, isRead: newRead, isFlagged: message.isFlagged)
				let add = newRead ? ["\\Seen"] : [String]()
				let remove = newRead ? [String]() : ["\\Seen"]
				let action = PendingAction(
					accountId: accountConfig.id,
					actionType: .setFlags,
					payload: .setFlags(uid: message.uid, mailbox: mailboxName(for: message.mailboxId) ?? "INBOX", add: add, remove: remove)
				)
				Task { try await actionQueue.enqueue(action) }
			}
		} catch {
			errorMessage = error.localizedDescription
		}
	}

	func moveSelectedThread(to mailbox: MailboxInfo) {
		guard let store, let actionQueue, let selectedThread, let selectedMailbox,
			  let accountConfig else { return }
		do {
			let messages = try store.messagesInThread(threadId: selectedThread.id, mailboxId: selectedMailbox.id)
			for message in messages {
				try store.updateMessageMailbox(messageId: message.id, newMailboxId: mailbox.id)
				let action = PendingAction(
					accountId: accountConfig.id,
					actionType: .move,
					payload: .move(uid: message.uid, from: selectedMailbox.name, to: mailbox.name)
				)
				Task { try await actionQueue.enqueue(action) }
			}
			autoAdvance()
		} catch {
			errorMessage = error.localizedDescription
		}
	}

	// MARK: - Auto Advance

	private func autoAdvance() {
		guard let current = selectedThread else { return }
		guard let currentIndex = threads.firstIndex(where: { $0.id == current.id }) else {
			selectedThread = nil
			messages = []
			return
		}
		let nextIndex = threads.index(after: currentIndex)
		if nextIndex < threads.endIndex {
			selectThread(threads[nextIndex])
		} else if currentIndex > threads.startIndex {
			let prevIndex = threads.index(before: currentIndex)
			selectThread(threads[prevIndex])
		} else {
			selectedThread = nil
			messages = []
			messageObservation?.cancel()
		}
	}

	// MARK: - Body Fetch for Reply/Forward

	/// Ensures the message body is available for quoting in compose.
	/// If bodyText is nil, attempts to fetch via IMAP. Returns the message
	/// with body populated, or the original if fetch fails (offline).
	func ensureBodyLoaded(for message: MessageSummary) async -> MessageSummary {
		if message.bodyText != nil { return message }
		guard let store, let provider = imapClientProvider else { return message }

		// Look up the MessageRecord to get the IMAP uid
		guard let record = try? store.message(id: message.id) else { return message }

		let client = provider()
		do {
			try await client.connect()
			let (text, html) = try await client.fetchBody(uid: record.uid)
			try? await client.disconnect()
			if text != nil || html != nil {
				try store.storeBody(messageId: message.id, text: text, html: html)
				var updated = message
				updated.bodyText = text
				updated.bodyHtml = html
				return updated
			}
		} catch {
			try? await client.disconnect()
			// Offline or fetch failed — compose without quoted text
		}
		return message
	}

	// MARK: - Helpers

	private func mailboxName(for mailboxId: String) -> String? {
		mailboxes.first(where: { $0.id == mailboxId })?.name
	}

	static func databasePath(for accountId: String) -> String {
		let dir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
			.appendingPathComponent("MagnumOpus", isDirectory: true)
		try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
		return dir.appendingPathComponent("\(accountId).sqlite").path
	}
}
