import Foundation
import Models
import IMAPClient
import MailStore
import TaskStore

@Observable
@MainActor
public final class SyncCoordinator {
	private let accountConfig: AccountConfig
	private let imapClient: any IMAPClientProtocol
	private let store: MailStore
	private let actionQueue: ActionQueue?
	private let taskStore: TaskStore?
	private var syncTask: Task<Void, Never>?

	public private(set) var syncState: SyncState = .idle
	private var eventHandlers: [(SyncEvent) -> Void] = []

	public init(
		accountConfig: AccountConfig,
		imapClient: any IMAPClientProtocol,
		store: MailStore,
		actionQueue: ActionQueue? = nil,
		taskStore: TaskStore? = nil
	) {
		self.accountConfig = accountConfig
		self.imapClient = imapClient
		self.store = store
		self.actionQueue = actionQueue
		self.taskStore = taskStore
	}

	public func onEvent(_ handler: @escaping (SyncEvent) -> Void) {
		eventHandlers.append(handler)
	}

	private func emit(_ event: SyncEvent) {
		for handler in eventHandlers {
			handler(event)
		}
	}

	// MARK: - Sync

	public func syncNow() async throws {
		syncState = .syncing(mailbox: nil)
		emit(.syncStarted)

		do {
			try await performSync()
			syncState = .idle
			emit(.syncCompleted)
		} catch {
			syncState = .error(error.localizedDescription)
			emit(.syncFailed(error.localizedDescription))
			throw error
		}
	}

	private func resurfaceDeferrals() {
		let now = ISO8601DateFormatter().string(from: Date())
		do {
			// Resurface deferred emails: delete deferral records so messages reappear in Inbox
			let expiredDeferrals = try store.expiredDeferrals(beforeDate: now)
			for deferral in expiredDeferrals {
				try store.deleteDeferral(id: deferral.id)
			}

			// Resurface deferred tasks: clear deferUntil in cache and rewrite .ics files
			let expiredTasks = try store.expiredDeferredTasks(beforeDate: now)
			for task in expiredTasks {
				try store.clearTaskDeferral(id: task.id)
				try taskStore?.updateDeferral(id: task.id, deferUntil: nil, isSomeday: false)
			}
		} catch {
			// Resurfacing is non-fatal; log and continue
			print("[SyncCoordinator] resurfaceDeferrals error: \(error)")
		}
	}

	private func performSync() async throws {
		// Resurface any expired deferrals before processing new state
		resurfaceDeferrals()

		// Ensure account exists in DB
		let existingAccounts = try store.accounts()
		if !existingAccounts.contains(where: { $0.id == accountConfig.id }) {
			try store.insertAccount(AccountRecord(
				id: accountConfig.id,
				name: accountConfig.name,
				email: accountConfig.email,
				imapHost: accountConfig.imapHost,
				imapPort: accountConfig.imapPort
			))
		}

		// Flush pending actions before fetching new state
		await actionQueue?.flush()

		try await imapClient.connect()
		do {
			try await syncAllMailboxes()
		} catch {
			try? await imapClient.disconnect()
			throw error
		}
		try? await imapClient.disconnect()
	}

	private func syncAllMailboxes() async throws {
		let remoteMailboxes = try await imapClient.listMailboxes()
		for remoteMailbox in remoteMailboxes {
			syncState = .syncing(mailbox: remoteMailbox.name)
			try await syncMailbox(remoteMailbox)
		}
	}

	private func syncMailbox(_ remoteMailbox: IMAPMailboxInfo) async throws {
		let status = try await imapClient.selectMailbox(remoteMailbox.name)

		let localMailboxes = try store.mailboxes(accountId: accountConfig.id)
		let localMailbox = localMailboxes.first { $0.name == remoteMailbox.name }

		let mailboxId: String
		let lastUid: Int

		if let local = localMailbox {
			mailboxId = local.id
			if local.uidValidity != status.uidValidity {
				// UID validity changed — re-fetch everything
				lastUid = 0
			} else {
				lastUid = local.uidNext - 1
			}
		} else {
			mailboxId = UUID().uuidString
			try store.upsertMailbox(MailboxRecord(
				id: mailboxId,
				accountId: accountConfig.id,
				name: remoteMailbox.name,
				uidValidity: status.uidValidity,
				uidNext: status.uidNext
			))
			lastUid = 0
		}

		let envelopes = try await imapClient.fetchEnvelopes(uidsGreaterThan: lastUid)

		if !envelopes.isEmpty {
			let records = envelopes.map { envelope -> MessageRecord in
				envelopeToRecord(envelope, accountId: accountConfig.id, mailboxId: mailboxId)
			}
			try store.insertMessages(records)

			let reconstructor = ThreadReconstructor(store: store)
			try reconstructor.processMessages(records)

			emit(.newMessages(count: envelopes.count, mailbox: remoteMailbox.name))
		}

		await prefetchBodies(mailboxId: mailboxId)

		// Detect and store mailbox role from LIST attributes
		let role = Self.detectMailboxRole(attributes: remoteMailbox.attributes, name: remoteMailbox.name)
		if role != nil {
			try store.updateMailboxRole(id: mailboxId, role: role)
		}

		try store.updateMailboxSync(
			id: mailboxId,
			uidValidity: status.uidValidity,
			uidNext: status.uidNext
		)
	}

	/// Fetch full bodies for recent messages that don't have bodyText yet
	private func prefetchBodies(mailboxId: String) async {
		let thirtyDaysAgo = ISO8601DateFormatter().string(
			from: Calendar.current.date(byAdding: .day, value: -30, to: Date())!
		)
		do {
			let messages = try store.messages(mailboxId: mailboxId)
			let recent = messages.filter { $0.bodyText == nil && $0.date >= thirtyDaysAgo }
			for message in recent.prefix(50) {
				guard !Task.isCancelled else { break }
				let (text, html) = try await imapClient.fetchBody(uid: message.uid)
				if text != nil || html != nil {
					try store.storeBody(messageId: message.id, text: text, html: html)
				}
			}
		} catch {
			// Background prefetch failure is non-fatal
		}
	}

	private func envelopeToRecord(
		_ envelope: FetchedEnvelope, accountId: String, mailboxId: String
	) -> MessageRecord {
		let toJson = encodeAddresses(envelope.to)
		let ccJson = encodeAddresses(envelope.cc)
		return MessageRecord(
			id: UUID().uuidString,
			accountId: accountId,
			mailboxId: mailboxId,
			uid: envelope.uid,
			messageId: envelope.messageId,
			inReplyTo: envelope.inReplyTo,
			refs: envelope.references,
			subject: envelope.subject,
			fromAddress: envelope.from?.address,
			fromName: envelope.from?.name,
			toAddresses: toJson,
			ccAddresses: ccJson,
			date: envelope.date,
			snippet: envelope.snippet,
			bodyText: envelope.bodyText,
			bodyHtml: envelope.bodyHtml,
			isRead: envelope.isRead,
			isFlagged: envelope.isFlagged,
			size: envelope.size
		)
	}

	private func encodeAddresses(_ addresses: [EmailAddress]) -> String? {
		guard !addresses.isEmpty else { return nil }
		struct Addr: Codable { var name: String?; var address: String }
		let addrs = addresses.map { Addr(name: $0.name, address: $0.address) }
		guard let data = try? JSONEncoder().encode(addrs) else { return nil }
		return String(data: data, encoding: .utf8)
	}

	// MARK: - Mailbox role detection

	/// Maps LIST attributes to a role string. Falls back to name-based detection
	/// when the server does not provide special-use attributes (RFC 6154).
	static func detectMailboxRole(attributes: Set<String>, name: String) -> String? {
		// Attribute-based detection (case-insensitive comparison)
		let lowered = Set(attributes.map { $0.lowercased() })
		if lowered.contains("\\trash") { return "trash" }
		if lowered.contains("\\archive") || lowered.contains("\\all") { return "archive" }
		if lowered.contains("\\sent") { return "sent" }
		if lowered.contains("\\drafts") { return "drafts" }
		if lowered.contains("\\junk") { return "junk" }

		// Name-based fallback for servers without special-use attributes
		let nameLower = name.lowercased()
		switch nameLower {
		case "trash", "deleted messages", "deleted items":
			return "trash"
		case "archive", "all mail", "[gmail]/all mail":
			return "archive"
		case "sent", "sent messages", "sent items", "[gmail]/sent mail":
			return "sent"
		case "drafts", "draft", "[gmail]/drafts":
			return "drafts"
		case "junk", "spam", "bulk mail", "[gmail]/spam":
			return "junk"
		case "inbox":
			return "inbox"
		default:
			return nil
		}
	}

	// MARK: - Periodic Sync

	public func startPeriodicSync(interval: Duration = .seconds(300)) {
		stopSync()
		syncTask = Task { [weak self] in
			while !Task.isCancelled {
				try? await self?.syncNow()
				do {
					try await Task.sleep(for: interval)
				} catch {
					break
				}
			}
		}
	}

	public func stopSync() {
		syncTask?.cancel()
		syncTask = nil
	}
}
