//
//  SMTPClient.swift
//  oAI
//
//  Swift-native SMTP client for sending emails
//
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Rune Olsen
//
// This file is part of oAI.
//
// oAI is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// oAI is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
// Public License for more details.
//
// You should have received a copy of the GNU Affero General Public
// License along with oAI. If not, see <https://www.gnu.org/licenses/>.


import Foundation
import Network
import os

class SMTPClient {
    private let log = Logger(subsystem: "com.oai.oAI", category: "smtp")

    private var connection: NWConnection?
    private var host: String
    private var port: UInt16
    private var username: String
    private var password: String

    init(host: String, port: UInt16 = 587, username: String, password: String) {
        self.host = host
        self.port = port
        self.username = username
        self.password = password
    }

    // MARK: - Connection

    func connectWithTLS() async throws {
        let tlsOptions = NWProtocolTLS.Options()
        let tcpOptions = NWProtocolTCP.Options()
        let params = NWParameters(tls: tlsOptions, tcp: tcpOptions)

        connection = NWConnection(host: NWEndpoint.Host(host), port: NWEndpoint.Port(integerLiteral: port), using: params)

        let hostName = self.host
        try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
            final class ResumeOnce: @unchecked Sendable {
                var resumed = false
                let lock = NSLock()
            }
            let resumeOnce = ResumeOnce()

            connection?.stateUpdateHandler = { [weak self] state in
                resumeOnce.lock.lock()
                defer { resumeOnce.lock.unlock() }

                guard !resumeOnce.resumed else { return }

                switch state {
                case .ready:
                    resumeOnce.resumed = true
                    self?.log.info("SMTP connected to \(hostName) with TLS")
                    continuation.resume()
                case .failed(let error):
                    resumeOnce.resumed = true
                    self?.log.error("SMTP TLS connection failed: \(error.localizedDescription)")
                    continuation.resume(throwing: EmailServiceError.connectionFailed(error.localizedDescription))
                default:
                    break
                }
            }

            connection?.start(queue: .global())
        }
    }

    func connect() async throws {
        let tcpOptions = NWProtocolTCP.Options()
        let params = NWParameters(tls: nil, tcp: tcpOptions)

        connection = NWConnection(host: NWEndpoint.Host(host), port: NWEndpoint.Port(integerLiteral: port), using: params)

        let hostName = self.host
        try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
            final class ResumeOnce: @unchecked Sendable {
                var resumed = false
                let lock = NSLock()
            }
            let resumeOnce = ResumeOnce()

            connection?.stateUpdateHandler = { [weak self] state in
                resumeOnce.lock.lock()
                defer { resumeOnce.lock.unlock() }

                guard !resumeOnce.resumed else { return }

                switch state {
                case .ready:
                    resumeOnce.resumed = true
                    self?.log.info("SMTP connected to \(hostName)")
                    continuation.resume()
                case .failed(let error):
                    resumeOnce.resumed = true
                    self?.log.error("SMTP connection failed: \(error.localizedDescription)")
                    continuation.resume(throwing: EmailServiceError.connectionFailed(error.localizedDescription))
                default:
                    break
                }
            }

            connection?.start(queue: .global())
        }
    }

    func disconnect() {
        _ = try? sendCommandSync("QUIT")
        connection?.cancel()
        connection = nil
        log.info("SMTP disconnected")
    }

    // MARK: - Send Email

    func sendEmail(from: String, to: [String], subject: String, body: String, htmlBody: String? = nil, inReplyTo: String? = nil) async throws -> String {
        // Port 465 uses direct TLS (implicit), port 587 uses STARTTLS (explicit)
        let usesDirectTLS = port == 465

        if usesDirectTLS {
            // Direct TLS connection (port 465)
            try await connectWithTLS()
        } else {
            // Plain connection first (port 587)
            try await connect()
        }
        defer { disconnect() }

        // Read server greeting (220)
        _ = try await readResponse()

        // EHLO
        _ = try await sendCommand("EHLO \(host)")

        // STARTTLS only for port 587
        if !usesDirectTLS {
            // Note: Network framework doesn't support mid-connection TLS upgrade
            // For STARTTLS, we skip the upgrade and continue with plain connection
            // This is insecure - recommend using port 465 instead
            log.warning("Port 587 with STARTTLS not fully supported, consider using port 465 (direct TLS)")
        }

        // AUTH LOGIN
        _ = try await sendCommand("AUTH LOGIN", expectCode: "334")

        // Send base64 encoded username
        let usernameB64 = Data(username.utf8).base64EncodedString()
        _ = try await sendCommand(usernameB64, expectCode: "334")

        // Send base64 encoded password
        let passwordB64 = Data(password.utf8).base64EncodedString()
        _ = try await sendCommand(passwordB64, expectCode: "235")

        // MAIL FROM
        _ = try await sendCommand("MAIL FROM:<\(from)>")

        // RCPT TO
        for recipient in to {
            _ = try await sendCommand("RCPT TO:<\(recipient)>")
        }

        // DATA
        _ = try await sendCommand("DATA", expectCode: "354")

        // Build email
        let messageId = "<\(UUID().uuidString)@\(host)>"
        let date = formatEmailDate(Date())

        var email = ""
        email += "From: \(from)\r\n"
        email += "To: \(to.joined(separator: ", "))\r\n"
        email += "Subject: \(subject)\r\n"
        email += "Date: \(date)\r\n"
        email += "Message-ID: \(messageId)\r\n"

        if let inReplyTo = inReplyTo {
            email += "In-Reply-To: \(inReplyTo)\r\n"
            email += "References: \(inReplyTo)\r\n"
        }

        if let htmlBody = htmlBody {
            // Multipart alternative
            let boundary = "----=_Part_\(UUID().uuidString.prefix(16))"
            email += "MIME-Version: 1.0\r\n"
            email += "Content-Type: multipart/alternative; boundary=\"\(boundary)\"\r\n"
            email += "\r\n"
            email += "--\(boundary)\r\n"
            email += "Content-Type: text/plain; charset=utf-8\r\n"
            email += "\r\n"
            email += body
            email += "\r\n\r\n"
            email += "--\(boundary)\r\n"
            email += "Content-Type: text/html; charset=utf-8\r\n"
            email += "\r\n"
            email += htmlBody
            email += "\r\n\r\n"
            email += "--\(boundary)--\r\n"
        } else {
            // Plain text
            email += "Content-Type: text/plain; charset=utf-8\r\n"
            email += "\r\n"
            email += body
            email += "\r\n"
        }

        // End with CRLF.CRLF
        email += ".\r\n"

        // Send email data
        _ = try await sendCommand(email, expectCode: "250", raw: true)

        log.info("Email sent successfully: \(messageId)")
        return messageId
    }

    // MARK: - Low-level Protocol

    private func upgradToTLS() async throws {
        guard let connection = connection else {
            throw EmailServiceError.connectionFailed("Not connected")
        }

        // Create TLS options
        let tlsOptions = NWProtocolTLS.Options()

        // Create new parameters with TLS
        let params = NWParameters(tls: tlsOptions)

        // Cancel old connection
        connection.cancel()

        // Create new connection with TLS
        self.connection = NWConnection(host: NWEndpoint.Host(host), port: NWEndpoint.Port(integerLiteral: port), using: params)

        try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
            final class ResumeOnce: @unchecked Sendable {
                var resumed = false
                let lock = NSLock()
            }
            let resumeOnce = ResumeOnce()

            self.connection?.stateUpdateHandler = { [weak self] state in
                resumeOnce.lock.lock()
                defer { resumeOnce.lock.unlock() }

                guard !resumeOnce.resumed else { return }

                switch state {
                case .ready:
                    resumeOnce.resumed = true
                    self?.log.info("SMTP upgraded to TLS")
                    continuation.resume()
                case .failed(let error):
                    resumeOnce.resumed = true
                    continuation.resume(throwing: error)
                default:
                    break
                }
            }

            self.connection?.start(queue: .global())
        }
    }

    private func sendCommand(_ command: String, expectCode: String = "250", raw: Bool = false) async throws -> String {
        guard let connection = connection else {
            throw EmailServiceError.connectionFailed("Not connected")
        }

        let fullCommand = raw ? command : "\(command)\r\n"
        let data = fullCommand.data(using: .utf8)!

        // Send command
        try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
            connection.send(content: data, completion: .contentProcessed { error in
                if let error = error {
                    continuation.resume(throwing: error)
                } else {
                    continuation.resume()
                }
            })
        }

        // Read response
        let response = try await readResponse()

        if !response.hasPrefix(expectCode) {
            throw EmailServiceError.sendingFailed("Expected \(expectCode), got: \(response)")
        }

        return response
    }

    private func sendCommandSync(_ command: String) throws {
        guard let connection = connection else { return }

        let fullCommand = "\(command)\r\n"
        let data = fullCommand.data(using: .utf8)!

        connection.send(content: data, completion: .idempotent)
    }

    private func readResponse() async throws -> String {
        guard let connection = connection else {
            throw EmailServiceError.connectionFailed("Not connected")
        }

        var fullResponse = ""
        var isComplete = false

        // Keep reading until we get a complete SMTP response
        // Multi-line responses end with "XXX " (space), interim lines have "XXX-" (dash)
        while !isComplete {
            let data: Data = try await withCheckedThrowingContinuation { continuation in
                connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { data, _, _, error in
                    if let error = error {
                        continuation.resume(throwing: error)
                    } else if let data = data {
                        continuation.resume(returning: data)
                    } else {
                        continuation.resume(throwing: EmailServiceError.connectionFailed("No data received"))
                    }
                }
            }

            guard let chunk = String(data: data, encoding: .utf8) else {
                throw EmailServiceError.connectionFailed("Invalid response encoding")
            }

            fullResponse += chunk

            // Check if we have a complete response
            // SMTP responses end with a line like "250 OK\r\n" (code + space + message)
            let lines = fullResponse.split(separator: "\r\n", omittingEmptySubsequences: false)
            for line in lines {
                if line.count >= 4 {
                    let prefix = String(line.prefix(4))
                    // Check if it's a final line (code + space, not code + dash)
                    // Format: "XXX " where X is a digit
                    if prefix.count == 4,
                       prefix[prefix.index(prefix.startIndex, offsetBy: 0)].isNumber,
                       prefix[prefix.index(prefix.startIndex, offsetBy: 1)].isNumber,
                       prefix[prefix.index(prefix.startIndex, offsetBy: 2)].isNumber,
                       prefix[prefix.index(prefix.startIndex, offsetBy: 3)] == " " {
                        isComplete = true
                        break
                    }
                }
            }

            // Safety: don't loop forever
            if fullResponse.count > 100000 {
                throw EmailServiceError.connectionFailed("Response too large")
            }
        }

        return fullResponse
    }

    // MARK: - Helpers

    private func formatEmailDate(_ date: Date) -> String {
        let formatter = DateFormatter()
        formatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss Z"
        formatter.locale = Locale(identifier: "en_US_POSIX")
        return formatter.string(from: date)
    }
}
