import Foundation

struct DiscoveredServer: Sendable {
	var hostname: String
	var port: Int
	var socketType: String
}

struct DiscoveredConfig: Sendable {
	var imap: DiscoveredServer?
	var smtp: DiscoveredServer?
}

enum AutoDiscovery {
	static func discover(for email: String) async -> DiscoveredConfig? {
		guard let domain = email.split(separator: "@").last.map(String.init) else { return nil }

		if let config = await queryISPDB(domain: domain) {
			return config
		}

		// Fall back to probing common hostnames
		let imap = await probeIMAP(domain: domain)
		let smtp = await probeSMTP(domain: domain)

		guard imap != nil || smtp != nil else { return nil }
		return DiscoveredConfig(imap: imap, smtp: smtp)
	}

	/// Convenience wrapper for IMAP-only discovery (backwards compatibility).
	static func discoverIMAP(for email: String) async -> DiscoveredServer? {
		await discover(for: email)?.imap
	}

	// MARK: - ISPDB

	private static func queryISPDB(domain: String) async -> DiscoveredConfig? {
		let url = URL(string: "https://autoconfig.thunderbird.net/v1.1/\(domain)")!
		guard let (data, response) = try? await URLSession.shared.data(from: url),
			  let httpResponse = response as? HTTPURLResponse,
			  httpResponse.statusCode == 200,
			  let xml = String(data: data, encoding: .utf8)
		else { return nil }

		let imap = parseISPDBXML(xml, serverType: "imap", tag: "incomingServer")
		let smtp = parseISPDBXML(xml, serverType: "smtp", tag: "outgoingServer")

		guard imap != nil || smtp != nil else { return nil }
		return DiscoveredConfig(imap: imap, smtp: smtp)
	}

	private static func parseISPDBXML(_ xml: String, serverType: String, tag: String) -> DiscoveredServer? {
		guard let startRange = xml.range(of: "<\(tag) type=\"\(serverType)\">"),
			  let endRange = xml.range(of: "</\(tag)>", range: startRange.upperBound..<xml.endIndex)
		else { return nil }

		let section = String(xml[startRange.upperBound..<endRange.lowerBound])

		func extractTag(_ tagName: String) -> String? {
			guard let start = section.range(of: "<\(tagName)>"),
				  let end = section.range(of: "</\(tagName)>", range: start.upperBound..<section.endIndex)
			else { return nil }
			return String(section[start.upperBound..<end.lowerBound])
		}

		guard let hostname = extractTag("hostname"),
			  let portStr = extractTag("port"),
			  let port = Int(portStr)
		else { return nil }

		let socketType = extractTag("socketType") ?? "SSL"
		return DiscoveredServer(hostname: hostname, port: port, socketType: socketType)
	}

	// MARK: - Probe fallbacks

	private static func probeIMAP(domain: String) async -> DiscoveredServer? {
		let candidates = [
			"imap.\(domain)",
			"mail.\(domain)",
		]
		for candidate in candidates {
			if await testConnection(host: candidate, port: 993) {
				return DiscoveredServer(hostname: candidate, port: 993, socketType: "SSL")
			}
		}
		return nil
	}

	private static func probeSMTP(domain: String) async -> DiscoveredServer? {
		let candidates: [(String, Int, String)] = [
			("smtp.\(domain)", 587, "STARTTLS"),
			("mail.\(domain)", 587, "STARTTLS"),
			("smtp.\(domain)", 465, "SSL"),
			("mail.\(domain)", 465, "SSL"),
		]
		for (host, port, socketType) in candidates {
			if await testConnection(host: host, port: port) {
				return DiscoveredServer(hostname: host, port: port, socketType: socketType)
			}
		}
		return nil
	}

	// MARK: - Connection test

	private static func testConnection(host: String, port: Int) async -> Bool {
		do {
			return try await withThrowingTaskGroup(of: Bool.self) { group in
				group.addTask {
					let task = URLSession.shared.streamTask(withHostName: host, port: port)
					task.resume()
					let (data, _) = try await task.readData(ofMinLength: 1, maxLength: 1024, timeout: 3)
					task.cancel()
					return data.map { !$0.isEmpty } ?? false
				}
				group.addTask {
					try await Task.sleep(for: .seconds(3))
					return false
				}
				let result = try await group.next() ?? false
				group.cancelAll()
				return result
			}
		} catch {
			return false
		}
	}
}
