import Foundation
import Models

/// Orchestrates SMTP command sequences on top of SMTPConnection.
struct SMTPCommandRunner {
	private let connection: SMTPConnection

	init(connection: SMTPConnection) {
		self.connection = connection
	}

	/// Send EHLO and parse capability lines from response.
	func ehlo(hostname: String) async throws -> [String] {
		let response = try await connection.sendCommand("EHLO \(hostname)")
		guard response.isSuccess else {
			throw SMTPError.unexpectedResponse(code: response.code, message: response.message)
		}
		return response.lines
	}

	/// Initiate STARTTLS upgrade.
	func startTLS() async throws {
		let response = try await connection.sendCommand("STARTTLS")
		guard response.code == 220 else {
			throw SMTPError.tlsUpgradeFailed
		}
		try await connection.upgradeToTLS()
	}

	/// Authenticate using AUTH PLAIN, falling back to AUTH LOGIN.
	func authenticate(credentials: Credentials) async throws {
		// Try AUTH PLAIN first
		let plainToken = authPlainToken(
			username: credentials.username,
			password: credentials.password
		)
		let response = try await connection.sendCommand("AUTH PLAIN \(plainToken)")

		if response.code == 235 {
			return
		}

		// Fallback to AUTH LOGIN
		let loginResponse = try await connection.sendCommand("AUTH LOGIN")
		guard loginResponse.code == 334 else {
			throw SMTPError.authenticationFailed(response.message)
		}

		let userResponse = try await connection.sendCommand(
			Data(credentials.username.utf8).base64EncodedString()
		)
		guard userResponse.code == 334 else {
			throw SMTPError.authenticationFailed(userResponse.message)
		}

		let passResponse = try await connection.sendCommand(
			Data(credentials.password.utf8).base64EncodedString()
		)
		guard passResponse.code == 235 else {
			throw SMTPError.authenticationFailed(passResponse.message)
		}
	}

	func mailFrom(_ address: String) async throws {
		let response = try await connection.sendCommand("MAIL FROM:<\(address)>")
		guard response.isSuccess else {
			throw SMTPError.sendFailed("MAIL FROM rejected: \(response.message)")
		}
	}

	func rcptTo(_ address: String) async throws {
		let response = try await connection.sendCommand("RCPT TO:<\(address)>")
		guard response.isSuccess else {
			throw SMTPError.recipientRejected(address)
		}
	}

	/// Send the DATA command, then the message content, then the terminating dot.
	func data(_ content: String) async throws {
		let response = try await connection.sendCommand("DATA")
		guard response.code == 354 else {
			throw SMTPError.sendFailed("DATA rejected: \(response.message)")
		}

		// Dot-stuff: lines starting with "." must be prefixed with an extra "."
		// to prevent premature DATA termination (RFC 5321 §4.5.2)
		let stuffed = content
			.replacingOccurrences(of: "\r\n.", with: "\r\n..")

		// sendCommand appends \r\n, so sending "content\r\n." produces "content\r\n.\r\n"
		// which is the correct DATA termination sequence
		let payload = stuffed + "\r\n."
		let dataResponse = try await connection.sendCommand(payload)

		guard dataResponse.isSuccess else {
			throw SMTPError.sendFailed("DATA content rejected: \(dataResponse.message)")
		}
	}

	func quit() async throws {
		let response = try await connection.sendCommand("QUIT")
		// 221 is the expected quit response, but don't throw on failure
		_ = response
	}

	// MARK: - Private

	private func authPlainToken(username: String, password: String) -> String {
		// AUTH PLAIN format: \0username\0password (base64 encoded)
		var token = Data()
		token.append(0)
		token.append(Data(username.utf8))
		token.append(0)
		token.append(Data(password.utf8))
		return token.base64EncodedString()
	}
}
