import Foundation
import Models
import NIO
import NIOIMAPCore

public actor IMAPClient: IMAPClientProtocol {
	private let host: String
	private let port: Int
	private let credentials: Credentials
	private var connection: IMAPConnection?
	private var runner: IMAPCommandRunner?
	private var cachedCapabilities: Set<String>?

	public init(host: String, port: Int, credentials: Credentials) {
		self.host = host
		self.port = port
		self.credentials = credentials
	}

	// MARK: - Connection lifecycle

	public func connect() async throws {
		let conn = IMAPConnection(host: host, port: port)
		try await conn.connect()
		connection = conn
		var newRunner = IMAPCommandRunner(connection: conn)
		let responses = try await newRunner.run(.login(username: credentials.username, password: credentials.password))
		guard responses.contains(where: { isOKTagged($0) }) else {
			throw IMAPError.authenticationFailed
		}
		runner = newRunner
	}

	public func disconnect() async throws {
		if var r = runner {
			_ = try? await r.run(.logout)
			runner = r
		}
		try await connection?.disconnect()
		try await connection?.shutdown()
		connection = nil
		runner = nil
	}

	// MARK: - Mailbox operations

	public func listMailboxes() async throws -> [IMAPMailboxInfo] {
		guard var runner else { throw IMAPError.notConnected }
		let responses = try await runner.run(
			.list(nil, reference: MailboxName(ByteBuffer(string: "")), .mailbox(ByteBuffer(string: "*")))
		)
		self.runner = runner
		return parseListResponses(responses)
	}

	public func selectMailbox(_ name: String) async throws -> IMAPMailboxStatus {
		guard var runner else { throw IMAPError.notConnected }
		let responses = try await runner.run(.select(MailboxName(ByteBuffer(string: name))))
		self.runner = runner
		return parseSelectResponses(responses, name: name)
	}

	// MARK: - Fetch operations

	public func fetchEnvelopes(uidsGreaterThan uid: Int) async throws -> [FetchedEnvelope] {
		guard var runner else { throw IMAPError.notConnected }
		let lower = UID(rawValue: UInt32(uid + 1))
		let range = MessageIdentifierRange<UID>(lower...UID.max)
		let set = MessageIdentifierSetNonEmpty<UID>(range: range)
		let responses = try await runner.run(.uidFetch(
			.set(set),
			[
				.envelope,
				.flags,
				.uid,
				.rfc822Size,
				.bodySection(peek: true, SectionSpecifier(kind: .text), nil),
			],
			[]
		))
		self.runner = runner
		return parseFetchResponses(responses)
	}

	public func fetchFlags(uids: ClosedRange<Int>) async throws -> [UIDFlagsPair] {
		guard var runner else { throw IMAPError.notConnected }
		let lower = UID(rawValue: UInt32(uids.lowerBound))
		let upper = UID(rawValue: UInt32(uids.upperBound))
		let range = MessageIdentifierRange<UID>(lower...upper)
		let set = MessageIdentifierSetNonEmpty<UID>(range: range)
		let responses = try await runner.run(.uidFetch(.set(set), [.uid, .flags], []))
		self.runner = runner
		return parseFlagResponses(responses)
	}

	public func fetchBody(uid: Int) async throws -> (text: String?, html: String?) {
		guard var runner else { throw IMAPError.notConnected }
		let uidValue = UID(rawValue: UInt32(uid))
		let range = MessageIdentifierRange<UID>(uidValue...uidValue)
		let set = MessageIdentifierSetNonEmpty<UID>(range: range)
		let responses = try await runner.run(.uidFetch(
			.set(set),
			[.bodySection(peek: true, SectionSpecifier(kind: .text), nil)],
			[]
		))
		self.runner = runner
		return parseBodyResponse(responses)
	}

	// MARK: - Write operations

	public func storeFlags(uid: Int, mailbox: String, add: [String], remove: [String]) async throws {
		guard var runner else { throw IMAPError.notConnected }
		let uidValue = UID(rawValue: UInt32(uid))
		let range = MessageIdentifierRange<UID>(uidValue...uidValue)
		let set = MessageIdentifierSetNonEmpty<UID>(range: range)

		if !add.isEmpty {
			let flags = add.map { Flag($0) }
			let storeData = StoreData.flags(.add(silent: true, list: flags))
			let responses = try await runner.run(.uidStore(.set(set), [], storeData))
			self.runner = runner
			guard responses.contains(where: { isOKTagged($0) }) else {
				throw IMAPError.unexpectedResponse("UID STORE +FLAGS failed")
			}
		}

		if !remove.isEmpty {
			let flags = remove.map { Flag($0) }
			let storeData = StoreData.flags(.remove(silent: true, list: flags))
			let responses = try await runner.run(.uidStore(.set(set), [], storeData))
			self.runner = runner
			guard responses.contains(where: { isOKTagged($0) }) else {
				throw IMAPError.unexpectedResponse("UID STORE -FLAGS failed")
			}
		}

		self.runner = runner
	}

	public func moveMessage(uid: Int, from: String, to: String) async throws {
		// Check capabilities first (updates self.runner internally)
		let caps = try await capabilities()

		guard var runner else { throw IMAPError.notConnected }
		let uidValue = UID(rawValue: UInt32(uid))
		let range = MessageIdentifierRange<UID>(uidValue...uidValue)
		let set = MessageIdentifierSetNonEmpty<UID>(range: range)
		let toMailbox = MailboxName(ByteBuffer(string: to))

		// Select the source mailbox
		let selectResponses = try await runner.run(.select(MailboxName(ByteBuffer(string: from))))
		self.runner = runner
		guard selectResponses.contains(where: { isOKTagged($0) }) else {
			throw IMAPError.unexpectedResponse("SELECT \(from) failed")
		}

		if caps.contains("MOVE") {
			let responses = try await runner.run(.uidMove(.set(set), toMailbox))
			self.runner = runner
			guard responses.contains(where: { isOKTagged($0) }) else {
				throw IMAPError.unexpectedResponse("UID MOVE failed")
			}
		} else {
			// Fallback: COPY + STORE \Deleted + EXPUNGE
			let copyResponses = try await runner.run(.uidCopy(.set(set), toMailbox))
			self.runner = runner
			guard copyResponses.contains(where: { isOKTagged($0) }) else {
				throw IMAPError.unexpectedResponse("UID COPY failed")
			}

			let storeData = StoreData.flags(.add(silent: true, list: [.deleted]))
			let storeResponses = try await runner.run(.uidStore(.set(set), [], storeData))
			self.runner = runner
			guard storeResponses.contains(where: { isOKTagged($0) }) else {
				throw IMAPError.unexpectedResponse("UID STORE +FLAGS \\Deleted failed")
			}

			let expungeResponses = try await runner.run(.expunge)
			self.runner = runner
			guard expungeResponses.contains(where: { isOKTagged($0) }) else {
				throw IMAPError.unexpectedResponse("EXPUNGE failed")
			}
		}

		self.runner = runner
	}

	public func copyMessage(uid: Int, from: String, to: String) async throws {
		guard var runner else { throw IMAPError.notConnected }
		let uidValue = UID(rawValue: UInt32(uid))
		let range = MessageIdentifierRange<UID>(uidValue...uidValue)
		let set = MessageIdentifierSetNonEmpty<UID>(range: range)
		let toMailbox = MailboxName(ByteBuffer(string: to))

		// Select the source mailbox first
		let selectResponses = try await runner.run(.select(MailboxName(ByteBuffer(string: from))))
		self.runner = runner
		guard selectResponses.contains(where: { isOKTagged($0) }) else {
			throw IMAPError.unexpectedResponse("SELECT \(from) failed")
		}

		let responses = try await runner.run(.uidCopy(.set(set), toMailbox))
		self.runner = runner
		guard responses.contains(where: { isOKTagged($0) }) else {
			throw IMAPError.unexpectedResponse("UID COPY failed")
		}
	}

	public func expunge(mailbox: String) async throws {
		guard var runner else { throw IMAPError.notConnected }

		let selectResponses = try await runner.run(.select(MailboxName(ByteBuffer(string: mailbox))))
		self.runner = runner
		guard selectResponses.contains(where: { isOKTagged($0) }) else {
			throw IMAPError.unexpectedResponse("SELECT \(mailbox) failed")
		}

		let responses = try await runner.run(.expunge)
		self.runner = runner
		guard responses.contains(where: { isOKTagged($0) }) else {
			throw IMAPError.unexpectedResponse("EXPUNGE failed")
		}
	}

	public func appendMessage(to mailbox: String, message: Data, flags: [String]) async throws {
		guard var runner else { throw IMAPError.notConnected }
		let mailboxName = MailboxName(ByteBuffer(string: mailbox))
		let flagList = flags.map { Flag($0) }
		let options = AppendOptions(flagList: flagList)
		let data = AppendData(byteCount: message.count)
		let appendMessage = AppendMessage(options: options, data: data)

		let parts: [AppendCommand] = [
			.start(tag: "", appendingTo: mailboxName),
			.beginMessage(message: appendMessage),
			.messageBytes(ByteBuffer(bytes: message)),
			.endMessage,
			.finish,
		]

		let responses = try await runner.runAppend(parts)
		self.runner = runner
		guard responses.contains(where: { isOKTagged($0) }) else {
			throw IMAPError.unexpectedResponse("APPEND failed")
		}
	}

	public func capabilities() async throws -> Set<String> {
		if let cached = cachedCapabilities {
			return cached
		}
		guard var runner else { throw IMAPError.notConnected }
		let responses = try await runner.run(.capability)
		self.runner = runner
		let caps = parseCapabilityResponses(responses)
		cachedCapabilities = caps
		return caps
	}

	// MARK: - Response parsing helpers

	private func isOKTagged(_ response: Response) -> Bool {
		if case .tagged(let tagged) = response {
			if case .ok = tagged.state {
				return true
			}
		}
		return false
	}

	private func parseCapabilityResponses(_ responses: [Response]) -> Set<String> {
		var caps: Set<String> = []
		for response in responses {
			guard case .untagged(let payload) = response,
				  case .capabilityData(let capabilities) = payload
			else { continue }
			for cap in capabilities {
				if let value = cap.value {
					caps.insert("\(cap.name)=\(value)")
				} else {
					caps.insert(cap.name)
				}
			}
		}
		return caps
	}

	private func parseListResponses(_ responses: [Response]) -> [IMAPMailboxInfo] {
		var results: [IMAPMailboxInfo] = []
		for response in responses {
			guard case .untagged(let payload) = response,
				  case .mailboxData(let mailboxData) = payload,
				  case .list(let info) = mailboxData
			else { continue }
			let name = String(decoding: info.path.name.bytes, as: UTF8.self)
			let attributes = Set(info.attributes.map { String($0) })
			results.append(IMAPMailboxInfo(name: name, attributes: attributes))
		}
		return results
	}

	private func parseSelectResponses(_ responses: [Response], name: String) -> IMAPMailboxStatus {
		var uidValidity = 0
		var uidNext = 0
		var messageCount = 0
		var recentCount = 0

		for response in responses {
			switch response {
			case .untagged(let payload):
				switch payload {
				case .mailboxData(let data):
					switch data {
					case .exists(let count):
						messageCount = count
					case .recent(let count):
						recentCount = count
					default:
						break
					}
				case .conditionalState(let status):
					switch status {
					case .ok(let text):
						if let code = text.code {
							switch code {
							case .uidValidity(let val):
								uidValidity = Int(val)
							case .uidNext(let val):
								uidNext = Int(val.rawValue)
							default:
								break
							}
						}
					default:
						break
					}
				default:
					break
				}
			default:
				break
			}
		}

		return IMAPMailboxStatus(
			name: name,
			uidValidity: uidValidity,
			uidNext: uidNext,
			messageCount: messageCount,
			recentCount: recentCount
		)
	}

	private func parseFetchResponses(_ responses: [Response]) -> [FetchedEnvelope] {
		var envelopes: [FetchedEnvelope] = []
		var currentUID: Int?
		var currentEnvelope: Envelope?
		var currentFlags: [Flag] = []
		var currentSize: Int = 0
		var bodyBuffer = ByteBuffer()

		for response in responses {
			switch response {
			case .fetch(let fetchResponse):
				switch fetchResponse {
				case .start:
					currentUID = nil
					currentEnvelope = nil
					currentFlags = []
					currentSize = 0
					bodyBuffer = ByteBuffer()
				case .startUID:
					currentUID = nil
					currentEnvelope = nil
					currentFlags = []
					currentSize = 0
					bodyBuffer = ByteBuffer()
				case .simpleAttribute(let attr):
					switch attr {
					case .uid(let uid):
						currentUID = Int(uid.rawValue)
					case .envelope(let env):
						currentEnvelope = env
					case .flags(let flags):
						currentFlags = flags
					case .rfc822Size(let size):
						currentSize = size
					default:
						break
					}
				case .streamingBegin:
					break
				case .streamingBytes(let bytes):
					var mutableBytes = bytes
					bodyBuffer.writeBuffer(&mutableBytes)
				case .streamingEnd:
					break
				case .finish:
					if let uid = currentUID {
						let envelope = buildFetchedEnvelope(
							uid: uid,
							envelope: currentEnvelope,
							flags: currentFlags,
							size: currentSize,
							bodyBuffer: bodyBuffer
						)
						envelopes.append(envelope)
					}
				}
			default:
				break
			}
		}

		return envelopes
	}

	private func parseFlagResponses(_ responses: [Response]) -> [UIDFlagsPair] {
		var results: [UIDFlagsPair] = []
		var currentUID: Int?
		var currentFlags: [Flag] = []

		for response in responses {
			switch response {
			case .fetch(let fetchResponse):
				switch fetchResponse {
				case .start, .startUID:
					currentUID = nil
					currentFlags = []
				case .simpleAttribute(let attr):
					switch attr {
					case .uid(let uid):
						currentUID = Int(uid.rawValue)
					case .flags(let flags):
						currentFlags = flags
					default:
						break
					}
				case .finish:
					if let uid = currentUID {
						results.append(UIDFlagsPair(
							uid: uid,
							isRead: currentFlags.contains(.seen),
							isFlagged: currentFlags.contains(.flagged)
						))
					}
				default:
					break
				}
			default:
				break
			}
		}

		return results
	}

	private func parseBodyResponse(_ responses: [Response]) -> (text: String?, html: String?) {
		var bodyBuffer = ByteBuffer()

		for response in responses {
			if case .fetch(let fetchResponse) = response {
				switch fetchResponse {
				case .streamingBytes(let bytes):
					var mutableBytes = bytes
					bodyBuffer.writeBuffer(&mutableBytes)
				default:
					break
				}
			}
		}

		guard bodyBuffer.readableBytes > 0 else {
			return (text: nil, html: nil)
		}

		let bodyString = String(buffer: bodyBuffer)

		// Simple heuristic: if the body contains HTML tags, treat as HTML
		let lowered = bodyString.lowercased()
		if lowered.contains("<html") || lowered.contains("<body") || lowered.contains("<div") {
			return (text: nil, html: bodyString)
		}
		return (text: bodyString, html: nil)
	}

	// MARK: - Envelope conversion

	private func buildFetchedEnvelope(
		uid: Int,
		envelope: Envelope?,
		flags: [Flag],
		size: Int,
		bodyBuffer: ByteBuffer
	) -> FetchedEnvelope {
		let subject = envelope?.subject.flatMap { String(buffer: $0) }
		let date = envelope?.date.map { String($0) } ?? ""
		let messageId = envelope?.messageID.map { String($0) }
		let inReplyTo = envelope?.inReplyTo.map { String($0) }
		let from = envelope?.from.compactMap { extractEmailAddress($0) }.first
		let to = envelope?.to.compactMap { extractEmailAddress($0) } ?? []
		let cc = envelope?.cc.compactMap { extractEmailAddress($0) } ?? []

		let bodyText: String?
		let bodyHtml: String?
		if bodyBuffer.readableBytes > 0 {
			let bodyString = String(buffer: bodyBuffer)
			let lowered = bodyString.lowercased()
			if lowered.contains("<html") || lowered.contains("<body") || lowered.contains("<div") {
				bodyText = nil
				bodyHtml = bodyString
			} else {
				bodyText = bodyString
				bodyHtml = nil
			}
		} else {
			bodyText = nil
			bodyHtml = nil
		}

		let snippet: String? = bodyText.map { text in
			let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
			return String(trimmed.prefix(200))
		}

		return FetchedEnvelope(
			uid: uid,
			messageId: messageId,
			inReplyTo: inReplyTo,
			references: nil,
			subject: subject,
			from: from,
			to: to,
			cc: cc,
			date: date,
			snippet: snippet,
			bodyText: bodyText,
			bodyHtml: bodyHtml,
			isRead: flags.contains(.seen),
			isFlagged: flags.contains(.flagged),
			size: size
		)
	}

	private func extractEmailAddress(_ element: EmailAddressListElement) -> Models.EmailAddress? {
		switch element {
		case .singleAddress(let addr):
			let mailbox = addr.mailbox.flatMap { String(buffer: $0) }
			let host = addr.host.flatMap { String(buffer: $0) }
			let personName = addr.personName.flatMap { String(buffer: $0) }
			guard let mailbox, let host else { return nil }
			return Models.EmailAddress(name: personName, address: "\(mailbox)@\(host)")
		case .group(let group):
			// Return the first child address from the group
			return group.children.compactMap { extractEmailAddress($0) }.first
		}
	}
}
