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

enum Perspective: String, CaseIterable, Identifiable {
	case inbox
	case today
	case upcoming
	case projects
	case someday
	case archive

	var id: String { rawValue }

	var label: String {
		switch self {
		case .inbox: "Inbox"
		case .today: "Today"
		case .upcoming: "Upcoming"
		case .projects: "Projects"
		case .someday: "Someday"
		case .archive: "Archive"
		}
	}

	var systemImage: String {
		switch self {
		case .inbox: "tray"
		case .today: "sun.max"
		case .upcoming: "calendar"
		case .projects: "folder"
		case .someday: "moon.zzz"
		case .archive: "archivebox"
		}
	}
}

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

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

	var selectedPerspective: Perspective = .inbox
	var items: [ItemSummary] = []
	var perspectiveCounts: [Perspective: Int] = [:]

	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
			)
		}

		// Set up TaskStore sharing the same database
		let taskDir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
			.appendingPathComponent("MagnumOpus", isDirectory: true)
			.appendingPathComponent(config.id, isDirectory: true)
			.appendingPathComponent("tasks", isDirectory: true)
		let ts = TaskStore(taskDirectory: taskDir, dbWriter: mailStore.databaseWriter)
		taskStore = ts

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

	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: - Perspective Loading

	func loadPerspective(_ perspective: Perspective) {
		selectedPerspective = perspective
		guard let store, let accountConfig else { return }
		let accountId = accountConfig.id
		do {
			var result: [ItemSummary] = []
			let todayString = VTODOParser.formatDateOnly(Date())

			switch perspective {
			case .inbox:
				// Emails in INBOX with no deferral
				if let inboxMailbox = mailboxes.first(where: { $0.name.lowercased() == "inbox" }) {
					let msgs = try store.messages(mailboxId: inboxMailbox.id)
					for msg in msgs {
						// Skip deferred emails
						if let _ = try? store.deferralForMessage(messageId: msg.id) {
							continue
						}
						result.append(.email(MailStore.toMessageSummary(msg)))
					}
				}
				// Tasks: NEEDS-ACTION, no deferUntil, not someday
				let tasks = try store.inboxTasks(accountId: accountId)
				for task in tasks {
					result.append(.task(taskRecordToSummary(task)))
				}

			case .today:
				// Tasks due today or overdue
				let tasks = try store.todayTasks(accountId: accountId, todayDate: todayString)
				for task in tasks {
					result.append(.task(taskRecordToSummary(task)))
				}

			case .upcoming:
				// Tasks with due date after today
				let tasks = try store.upcomingTasks(accountId: accountId, afterDate: todayString)
				for task in tasks {
					result.append(.task(taskRecordToSummary(task)))
				}

			case .projects:
				// Items with project labels
				let projectLabels = try store.projectLabels(accountId: accountId)
				for label in projectLabels {
					let itemLabels = try store.itemsWithLabel(labelId: label.id)
					for il in itemLabels {
						if il.itemType == "task" {
							if let task = try store.task(id: il.itemId) {
								result.append(.task(taskRecordToSummary(task)))
							}
						}
					}
				}

			case .someday:
				// Emails with deferral where deferUntil IS NULL (someday)
				let somedayDeferrals = try store.somedayDeferrals()
				for deferral in somedayDeferrals {
					if let msg = try store.message(id: deferral.messageId) {
						result.append(.email(MailStore.toMessageSummary(msg)))
					}
				}
				// Tasks with isSomeday=true
				let tasks = try store.somedayTasks(accountId: accountId)
				for task in tasks {
					result.append(.task(taskRecordToSummary(task)))
				}

			case .archive:
				// Tasks with COMPLETED/CANCELLED status
				let tasks = try store.archivedTasks(accountId: accountId)
				for task in tasks {
					result.append(.task(taskRecordToSummary(task)))
				}
			}

			items = result.sorted { $0.date > $1.date }
		} catch {
			errorMessage = error.localizedDescription
		}
	}

	func loadAllPerspectiveCounts() {
		guard let store, let accountConfig else { return }
		let accountId = accountConfig.id
		let todayString = VTODOParser.formatDateOnly(Date())

		var counts: [Perspective: Int] = [:]

		do {
			// Inbox count
			var inboxCount = 0
			if let inboxMailbox = mailboxes.first(where: { $0.name.lowercased() == "inbox" }) {
				let msgs = try store.messages(mailboxId: inboxMailbox.id)
				for msg in msgs {
					if (try? store.deferralForMessage(messageId: msg.id)) == nil {
						inboxCount += 1
					}
				}
			}
			inboxCount += try store.inboxTasks(accountId: accountId).count
			counts[.inbox] = inboxCount

			// Today count
			counts[.today] = try store.todayTasks(accountId: accountId, todayDate: todayString).count

			// Upcoming count
			counts[.upcoming] = try store.upcomingTasks(accountId: accountId, afterDate: todayString).count

			// Projects count
			var projectCount = 0
			let projectLabels = try store.projectLabels(accountId: accountId)
			for label in projectLabels {
				projectCount += try store.itemsWithLabel(labelId: label.id).count
			}
			counts[.projects] = projectCount

			// Someday count
			let somedayCount = try store.somedayDeferrals().count + store.somedayTasks(accountId: accountId).count
			counts[.someday] = somedayCount

			// Archive count
			counts[.archive] = try store.archivedTasks(accountId: accountId).count
		} catch {
			// Counts are best-effort
		}

		perspectiveCounts = counts
	}

	// 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: - GTD Triage Actions (unified items)

	func deferItem(_ item: ItemSummary, until date: Date?) {
		guard let store, let accountConfig else { return }
		do {
			switch item {
			case .email(let msg):
				// Create a DeferralRecord
				let deferUntilStr = date.map { VTODOParser.formatDateOnly($0) }
				let deferral = DeferralRecord(
					id: UUID().uuidString,
					messageId: msg.id,
					deferUntil: deferUntilStr,
					originalMailbox: nil,
					createdAt: ISO8601DateFormatter().string(from: Date())
				)
				try store.insertDeferral(deferral)

			case .task(let task):
				let isSomeday = date == nil
				try taskStore?.updateDeferral(id: task.id, deferUntil: date, isSomeday: isSomeday)
			}
			loadPerspective(selectedPerspective)
			loadAllPerspectiveCounts()
		} catch {
			errorMessage = error.localizedDescription
		}
	}

	func fileItem(_ item: ItemSummary, label: LabelInfo) {
		guard let store, let accountConfig else { return }
		do {
			switch item {
			case .email(let msg):
				try store.attachLabel(labelId: label.id, itemType: "email", itemId: msg.id)
			case .task(let task):
				try store.attachLabel(labelId: label.id, itemType: "task", itemId: task.id)
				// Also update VTODO CATEGORIES
				var categories = task.categories
				if !categories.contains(label.name) {
					categories.append(label.name)
				}
				try taskStore?.writeTask(
					id: task.id,
					accountId: task.accountId,
					summary: task.summary,
					description: task.description,
					status: task.status.rawValue,
					priority: task.priority,
					dueDate: task.dueDate,
					deferUntil: task.deferUntil,
					linkedMessageId: task.linkedMessageId,
					categories: categories,
					isSomeday: task.isSomeday
				)
			}
			loadPerspective(selectedPerspective)
			loadAllPerspectiveCounts()
		} catch {
			errorMessage = error.localizedDescription
		}
	}

	func discardItem(_ item: ItemSummary) {
		guard let store, let actionQueue, let accountConfig else { return }
		do {
			switch item {
			case .email(let msg):
				// Archive the email
				guard let archiveMailbox = try store.mailboxWithRole("archive", accountId: accountConfig.id) else {
					errorMessage = "No archive mailbox found"
					return
				}
				guard let record = try store.message(id: msg.id) else { return }
				try store.updateMessageMailbox(messageId: msg.id, newMailboxId: archiveMailbox.id)
				let mailboxName = self.mailboxName(for: record.mailboxId) ?? "INBOX"
				let action = PendingAction(
					accountId: accountConfig.id,
					actionType: .move,
					payload: .move(uid: record.uid, from: mailboxName, to: archiveMailbox.name)
				)
				Task { try await actionQueue.enqueue(action) }

			case .task(let task):
				try taskStore?.updateStatus(id: task.id, status: TaskStatus.cancelled.rawValue)
			}
			loadPerspective(selectedPerspective)
			loadAllPerspectiveCounts()
		} catch {
			errorMessage = error.localizedDescription
		}
	}

	func completeItem(_ item: ItemSummary) {
		guard let store, let actionQueue, let accountConfig else { return }
		do {
			switch item {
			case .email(let msg):
				// Archive the email
				guard let archiveMailbox = try store.mailboxWithRole("archive", accountId: accountConfig.id) else {
					errorMessage = "No archive mailbox found"
					return
				}
				guard let record = try store.message(id: msg.id) else { return }
				try store.updateMessageMailbox(messageId: msg.id, newMailboxId: archiveMailbox.id)
				let mailboxName = self.mailboxName(for: record.mailboxId) ?? "INBOX"
				let action = PendingAction(
					accountId: accountConfig.id,
					actionType: .move,
					payload: .move(uid: record.uid, from: mailboxName, to: archiveMailbox.name)
				)
				Task { try await actionQueue.enqueue(action) }

			case .task(let task):
				try taskStore?.updateStatus(id: task.id, status: TaskStatus.completed.rawValue)
			}
			loadPerspective(selectedPerspective)
			loadAllPerspectiveCounts()
		} catch {
			errorMessage = error.localizedDescription
		}
	}

	// MARK: - Linked Tasks

	func tasksLinkedToThread(threadId: String) -> [TaskSummary] {
		guard let store else { return [] }
		do {
			let records = try store.tasksLinkedToThread(threadId: threadId)
			return records.map { taskRecordToSummary($0) }
		} catch {
			return []
		}
	}

	// 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
	}

	private func taskRecordToSummary(_ record: TaskRecord) -> TaskSummary {
		TaskSummary(
			id: record.id,
			accountId: record.accountId,
			summary: record.summary,
			description: record.description,
			status: TaskStatus(rawValue: record.status) ?? .needsAction,
			priority: record.priority,
			dueDate: record.dueDate.flatMap { VTODOParser.parseDate($0) },
			deferUntil: record.deferUntil.flatMap { VTODOParser.parseDate($0) },
			createdAt: VTODOParser.parseDate(record.createdAt) ?? Date.distantPast,
			linkedMessageId: record.linkedMessageId,
			categories: [],
			isSomeday: record.isSomeday
		)
	}

	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
	}
}
