import Foundation
import Models
import IMAPClient
import MailStore

@Observable
@MainActor
public final class SyncCoordinator {
	private let accountConfig: AccountConfig
	private let imapClient: any IMAPClientProtocol
	private let store: MailStore
	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) {
		self.accountConfig = accountConfig
		self.imapClient = imapClient
		self.store = store
	}

	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 performSync() async throws {
		// 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
			))
		}

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

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

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