//
// OpenRocky — Voice-first AI Agent
// https://github.com/openrocky
//
// Developed by everettjf with the assistance of Claude Code and Codex.
// Date: 2026-04-06
// Copyright (c) 2026 everettjf. All rights reserved.
//

import Foundation
import Network

// MARK: - Email Provider

enum OpenRockyEmailProvider: String, Codable, Sendable, CaseIterable {
    case smtp
    case resend

    var displayName: String {
        switch self {
        case .smtp: return "SMTP"
        case .resend: return "Resend"
        }
    }
}

// MARK: - Email Configuration

struct OpenRockyEmailConfig: Codable, Sendable {
    var provider: OpenRockyEmailProvider

    // SMTP fields
    var smtpHost: String
    var smtpPort: Int
    var username: String  // also serves as From for SMTP
    var useTLS: Bool

    // Resend fields
    var resendFromAddress: String  // e.g. "you@yourdomain.com" or "onboarding@resend.dev"

    /// Default recipient used when a send is requested without an explicit `to`
    /// (e.g. "email this article to me"). Configured in Settings → Features → Send Email.
    var defaultRecipient: String

    static let configKey = "rocky.email.config"
    static let smtpKeychainAccount = "rocky.email.smtp.password"
    static let resendKeychainAccount = "rocky.email.resend.apikey"

    /// Well-known SMTP presets
    static let gmailPreset = OpenRockyEmailConfig(provider: .smtp, smtpHost: "smtp.gmail.com", smtpPort: 465, username: "", useTLS: true, resendFromAddress: "")
    static let outlookPreset = OpenRockyEmailConfig(provider: .smtp, smtpHost: "smtp.office365.com", smtpPort: 587, username: "", useTLS: true, resendFromAddress: "")
    static let qqPreset = OpenRockyEmailConfig(provider: .smtp, smtpHost: "smtp.qq.com", smtpPort: 465, username: "", useTLS: true, resendFromAddress: "")
    /// Resend sandbox sender — works without domain verification, but can only deliver
    /// to the email address registered on the user's Resend account.
    static let resendSandboxFrom = "onboarding@resend.dev"

    init(provider: OpenRockyEmailProvider = .smtp, smtpHost: String = "", smtpPort: Int = 465, username: String = "", useTLS: Bool = true, resendFromAddress: String = "", defaultRecipient: String = "") {
        self.provider = provider
        self.smtpHost = smtpHost
        self.smtpPort = smtpPort
        self.username = username
        self.useTLS = useTLS
        self.resendFromAddress = resendFromAddress
        self.defaultRecipient = defaultRecipient
    }

    /// Custom decoder for forward-compat: pre-Resend configs lack `provider` and
    /// `resendFromAddress` keys. Treat the absence of `provider` as SMTP so existing
    /// users don't lose their settings on upgrade.
    init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        self.provider = (try? c.decode(OpenRockyEmailProvider.self, forKey: .provider)) ?? .smtp
        self.smtpHost = (try? c.decode(String.self, forKey: .smtpHost)) ?? ""
        self.smtpPort = (try? c.decode(Int.self, forKey: .smtpPort)) ?? 465
        self.username = (try? c.decode(String.self, forKey: .username)) ?? ""
        self.useTLS = (try? c.decode(Bool.self, forKey: .useTLS)) ?? true
        self.resendFromAddress = (try? c.decode(String.self, forKey: .resendFromAddress)) ?? ""
        self.defaultRecipient = (try? c.decode(String.self, forKey: .defaultRecipient)) ?? ""
    }

    func save() {
        if let data = try? JSONEncoder().encode(self) {
            UserDefaults.standard.set(data, forKey: Self.configKey)
        }
    }

    static func load() -> OpenRockyEmailConfig? {
        guard let data = UserDefaults.standard.data(forKey: Self.configKey) else { return nil }
        return try? JSONDecoder().decode(OpenRockyEmailConfig.self, from: data)
    }

    /// Destructive removal — clears the config and credentials for BOTH providers, since
    /// the user explicitly chose to wipe email setup.
    static func remove() {
        UserDefaults.standard.removeObject(forKey: Self.configKey)
        OpenRockyKeychain.live.removeValue(for: smtpKeychainAccount)
        OpenRockyKeychain.live.removeValue(for: resendKeychainAccount)
    }

    var keychainAccount: String {
        switch provider {
        case .smtp: return Self.smtpKeychainAccount
        case .resend: return Self.resendKeychainAccount
        }
    }

    var isConfigured: Bool {
        switch provider {
        case .smtp:
            return !smtpHost.isEmpty && !username.isEmpty && smtpPort > 0
        case .resend:
            return !resendFromAddress.isEmpty
        }
    }

    var hasCredential: Bool {
        OpenRockyKeychain.live.value(for: keychainAccount) != nil
    }

    /// Human-readable identity for status rows: SMTP shows the account, Resend shows the From.
    var displayIdentity: String {
        switch provider {
        case .smtp: return username
        case .resend: return resendFromAddress
        }
    }
}

// MARK: - Email Service

@MainActor
final class OpenRockyEmailService {
    static let shared = OpenRockyEmailService()

    enum EmailError: Error, LocalizedError {
        case notConfigured
        case noCredential
        case connectionFailed(String)
        case smtpError(String)
        case resendError(String)
        case encodingError

        var errorDescription: String? {
            switch self {
            case .notConfigured: return "Email not configured. Please set up an email provider in Settings → Features → Send Email."
            case .noCredential: return "Email credential not found in Keychain."
            case .connectionFailed(let msg): return "Connection failed: \(msg)"
            case .smtpError(let msg): return "SMTP error: \(msg)"
            case .resendError(let msg): return "Resend error: \(msg)"
            case .encodingError: return "Failed to encode email message."
            }
        }
    }

    func send(to: [String], subject: String, body: String, cc: [String] = [], bcc: [String] = []) async throws -> String {
        guard let config = OpenRockyEmailConfig.load(), config.isConfigured else {
            throw EmailError.notConfigured
        }
        guard let credential = OpenRockyKeychain.live.value(for: config.keychainAccount) else {
            throw EmailError.noCredential
        }

        switch config.provider {
        case .smtp:
            return try await sendViaSMTPRoute(config: config, password: credential, to: to, subject: subject, body: body, cc: cc, bcc: bcc)
        case .resend:
            return try await sendViaResend(config: config, apiKey: credential, to: to, subject: subject, body: body, cc: cc, bcc: bcc)
        }
    }

    // MARK: - Resend HTTP API

    private struct ResendResponse: Decodable { let id: String }
    private struct ResendErrorBody: Decodable { let message: String? }

    private func sendViaResend(config: OpenRockyEmailConfig, apiKey: String, to: [String], subject: String, body: String, cc: [String], bcc: [String]) async throws -> String {
        guard let url = URL(string: "https://api.resend.com/emails") else {
            throw EmailError.resendError("Invalid endpoint URL")
        }
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")

        var payload: [String: Any] = [
            "from": config.resendFromAddress,
            "to": to,
            "subject": subject,
            "text": body
        ]
        if !cc.isEmpty { payload["cc"] = cc }
        if !bcc.isEmpty { payload["bcc"] = bcc }

        do {
            request.httpBody = try JSONSerialization.data(withJSONObject: payload)
        } catch {
            throw EmailError.encodingError
        }

        let (data, response) = try await URLSession.shared.data(for: request)
        guard let http = response as? HTTPURLResponse else {
            throw EmailError.resendError("No HTTP response")
        }
        guard (200..<300).contains(http.statusCode) else {
            let message = (try? JSONDecoder().decode(ResendErrorBody.self, from: data))?.message
                ?? String(data: data, encoding: .utf8)
                ?? "HTTP \(http.statusCode)"
            throw EmailError.resendError("HTTP \(http.statusCode): \(message)")
        }
        do {
            return try JSONDecoder().decode(ResendResponse.self, from: data).id
        } catch {
            throw EmailError.resendError("Unexpected response body")
        }
    }

    // MARK: - SMTP Route

    private func sendViaSMTPRoute(config: OpenRockyEmailConfig, password: String, to: [String], subject: String, body: String, cc: [String], bcc: [String]) async throws -> String {
        let messageID = "<\(UUID().uuidString)@rocky.local>"
        let mime = buildMIME(from: config.username, to: to, cc: cc, subject: subject, body: body, messageID: messageID)
        try await sendViaSMTP(config: config, password: password, from: config.username, recipients: to + cc + bcc, data: mime)
        return messageID
    }

    // MARK: - MIME Builder

    private func buildMIME(from: String, to: [String], cc: [String], subject: String, body: String, messageID: String) -> String {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss Z"
        dateFormatter.locale = Locale(identifier: "en_US_POSIX")
        let dateString = dateFormatter.string(from: Date())

        var headers = [
            "From: \(from)",
            "To: \(to.joined(separator: ", "))",
            "Date: \(dateString)",
            "Subject: =?UTF-8?B?\(Data(subject.utf8).base64EncodedString())?=",
            "Message-ID: \(messageID)",
            "MIME-Version: 1.0",
            "Content-Type: text/plain; charset=UTF-8",
            "Content-Transfer-Encoding: base64",
            "X-Mailer: Rocky/1.0"
        ]
        if !cc.isEmpty {
            headers.insert("Cc: \(cc.joined(separator: ", "))", at: 2)
        }

        let encodedBody = Data(body.utf8).base64EncodedString(options: .lineLength76Characters)

        return headers.joined(separator: "\r\n") + "\r\n\r\n" + encodedBody + "\r\n"
    }

    // MARK: - SMTP Protocol

    private func sendViaSMTP(config: OpenRockyEmailConfig, password: String, from: String, recipients: [String], data: String) async throws {
        let (inputStream, outputStream) = try await connectToSMTP(host: config.smtpHost, port: config.smtpPort, useTLS: config.useTLS)

        defer {
            inputStream.close()
            outputStream.close()
        }

        // Read greeting
        let greeting = try readResponse(from: inputStream)
        guard greeting.hasPrefix("220") else {
            throw EmailError.smtpError("Bad greeting: \(greeting)")
        }

        // EHLO
        try sendCommand("EHLO rocky.local", to: outputStream)
        let ehloResponse = try readResponse(from: inputStream)
        guard ehloResponse.contains("250") else {
            throw EmailError.smtpError("EHLO failed: \(ehloResponse)")
        }

        // STARTTLS for port 587
        if config.smtpPort == 587 {
            try sendCommand("STARTTLS", to: outputStream)
            let starttlsResponse = try readResponse(from: inputStream)
            guard starttlsResponse.hasPrefix("220") else {
                throw EmailError.smtpError("STARTTLS failed: \(starttlsResponse)")
            }
            // After STARTTLS, TLS is negotiated by the stream; for simplicity we rely on the
            // initial TLS connection for port 465 and skip explicit STARTTLS upgrade here.
            // A full implementation would upgrade the socket. For port 587 users, we note
            // this limitation.
        }

        // AUTH LOGIN
        try sendCommand("AUTH LOGIN", to: outputStream)
        let authResponse = try readResponse(from: inputStream)
        guard authResponse.hasPrefix("334") else {
            throw EmailError.smtpError("AUTH LOGIN failed: \(authResponse)")
        }

        // Username (base64)
        try sendCommand(Data(from.utf8).base64EncodedString(), to: outputStream)
        let userResponse = try readResponse(from: inputStream)
        guard userResponse.hasPrefix("334") else {
            throw EmailError.smtpError("Username rejected: \(userResponse)")
        }

        // Password (base64)
        try sendCommand(Data(password.utf8).base64EncodedString(), to: outputStream)
        let passResponse = try readResponse(from: inputStream)
        guard passResponse.hasPrefix("235") else {
            throw EmailError.smtpError("Authentication failed: \(passResponse)")
        }

        // MAIL FROM
        try sendCommand("MAIL FROM:<\(from)>", to: outputStream)
        let mailFromResponse = try readResponse(from: inputStream)
        guard mailFromResponse.hasPrefix("250") else {
            throw EmailError.smtpError("MAIL FROM failed: \(mailFromResponse)")
        }

        // RCPT TO
        for recipient in recipients {
            try sendCommand("RCPT TO:<\(recipient)>", to: outputStream)
            let rcptResponse = try readResponse(from: inputStream)
            guard rcptResponse.hasPrefix("250") else {
                throw EmailError.smtpError("RCPT TO <\(recipient)> failed: \(rcptResponse)")
            }
        }

        // DATA
        try sendCommand("DATA", to: outputStream)
        let dataResponse = try readResponse(from: inputStream)
        guard dataResponse.hasPrefix("354") else {
            throw EmailError.smtpError("DATA failed: \(dataResponse)")
        }

        // Send message body + terminator
        try sendCommand(data + "\r\n.", to: outputStream)
        let sendResponse = try readResponse(from: inputStream)
        guard sendResponse.hasPrefix("250") else {
            throw EmailError.smtpError("Message rejected: \(sendResponse)")
        }

        // QUIT
        try sendCommand("QUIT", to: outputStream)
        _ = try? readResponse(from: inputStream)
    }

    // MARK: - Stream Helpers

    private func connectToSMTP(host: String, port: Int, useTLS: Bool) async throws -> (InputStream, OutputStream) {
        var readStream: Unmanaged<CFReadStream>?
        var writeStream: Unmanaged<CFWriteStream>?

        CFStreamCreatePairWithSocketToHost(nil, host as CFString, UInt32(port), &readStream, &writeStream)

        guard let inputCF = readStream?.takeRetainedValue(),
              let outputCF = writeStream?.takeRetainedValue() else {
            throw EmailError.connectionFailed("Failed to create socket streams")
        }

        let inputStream = inputCF as InputStream
        let outputStream = outputCF as OutputStream

        // For port 465 (implicit TLS), enable SSL on the streams
        if useTLS && port == 465 {
            inputStream.setProperty(StreamSocketSecurityLevel.negotiatedSSL.rawValue, forKey: .socketSecurityLevelKey)
            outputStream.setProperty(StreamSocketSecurityLevel.negotiatedSSL.rawValue, forKey: .socketSecurityLevelKey)
        }

        inputStream.open()
        outputStream.open()

        // Wait for connection
        try await Task.sleep(for: .milliseconds(500))

        guard inputStream.streamStatus != .error, outputStream.streamStatus != .error else {
            let errorMsg = inputStream.streamError?.localizedDescription ?? outputStream.streamError?.localizedDescription ?? "Unknown"
            throw EmailError.connectionFailed(errorMsg)
        }

        return (inputStream, outputStream)
    }

    private func sendCommand(_ command: String, to stream: OutputStream) throws {
        let data = (command + "\r\n").data(using: .utf8)!
        let bytes = [UInt8](data)
        let written = stream.write(bytes, maxLength: bytes.count)
        guard written > 0 else {
            throw EmailError.smtpError("Failed to write to stream")
        }
    }

    private func readResponse(from stream: InputStream) throws -> String {
        // Wait for data to become available
        var attempts = 0
        while !stream.hasBytesAvailable && attempts < 100 {
            Thread.sleep(forTimeInterval: 0.05)
            attempts += 1
        }

        var buffer = [UInt8](repeating: 0, count: 4096)
        var result = ""

        // Read all available data
        while stream.hasBytesAvailable {
            let bytesRead = stream.read(&buffer, maxLength: buffer.count)
            if bytesRead > 0 {
                result += String(bytes: buffer[0..<bytesRead], encoding: .utf8) ?? ""
            } else if bytesRead < 0 {
                throw EmailError.smtpError("Stream read error: \(stream.streamError?.localizedDescription ?? "unknown")")
            }
            // Small delay to check for more data
            if stream.hasBytesAvailable {
                Thread.sleep(forTimeInterval: 0.01)
            }
        }

        return result.trimmingCharacters(in: .whitespacesAndNewlines)
    }
}
