//
//  IMAPClient.swift
//  oAI
//
//  Swift-native IMAP client for email monitoring
//
// 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 IMAPClient {
    private let log = Logger(subsystem: "com.oai.oAI", category: "imap")

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

    private var commandTag = 1
    private var receiveBuffer = Data()

    init(host: String, port: UInt16 = 993, username: String, password: String, useTLS: Bool = true) {
        self.host = host
        self.port = port
        self.username = username
        self.password = password
        self.useTLS = useTLS
    }

    // MARK: - Connection

    func connect() async throws {
        let tlsOptions = useTLS ? NWProtocolTLS.Options() : nil
        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
        return try await withCheckedThrowingContinuation { continuation 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("IMAP connected to \(hostName)")
                    continuation.resume()
                case .failed(let error):
                    resumeOnce.resumed = true
                    self?.log.error("IMAP connection failed: \(error.localizedDescription)")
                    continuation.resume(throwing: EmailServiceError.connectionFailed(error.localizedDescription))
                default:
                    break
                }
            }

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

    func disconnect() {
        connection?.cancel()
        connection = nil
        log.info("IMAP disconnected")
    }

    // MARK: - Authentication

    func login() async throws {
        // Wait for server greeting
        _ = try await readResponse()

        // Send LOGIN command
        let loginCmd = "LOGIN \"\(username)\" \"\(password)\""
        let response = try await sendCommand(loginCmd)

        if !response.contains("OK") {
            throw EmailServiceError.authenticationFailed
        }

        log.info("IMAP login successful")
    }

    // MARK: - Mailbox Operations

    func selectMailbox(_ mailbox: String = "INBOX") async throws {
        let response = try await sendCommand("SELECT \(mailbox)")

        if !response.contains("OK") {
            throw EmailServiceError.connectionFailed("Failed to select mailbox")
        }
    }

    func searchUnseenWithSubject(_ subject: String) async throws -> [UInt32] {
        let response = try await sendCommand("SEARCH UNSEEN SUBJECT \"\(subject)\"")

        // Parse UIDs from response like "* SEARCH 123 124 125"
        var uids: [UInt32] = []

        for line in response.split(separator: "\r\n") {
            if line.hasPrefix("* SEARCH") {
                let parts = line.split(separator: " ")
                for part in parts.dropFirst(2) {
                    if let uid = UInt32(part) {
                        uids.append(uid)
                    }
                }
            }
        }

        return uids
    }

    func searchUnseen() async throws -> [UInt32] {
        let response = try await sendCommand("SEARCH UNSEEN")

        // Parse UIDs from response like "* SEARCH 123 124 125"
        var uids: [UInt32] = []

        for line in response.split(separator: "\r\n") {
            if line.hasPrefix("* SEARCH") {
                let parts = line.split(separator: " ")
                for part in parts.dropFirst(2) {
                    if let uid = UInt32(part) {
                        uids.append(uid)
                    }
                }
            }
        }

        return uids
    }

    func fetchEmail(uid: UInt32) async throws -> IncomingEmail {
        let response = try await sendCommand("FETCH \(uid) (BODY.PEEK[] FLAGS)")

        // Parse email from response
        return try parseEmailResponse(response, uid: uid)
    }

    func markAsRead(uid: UInt32) async throws {
        _ = try await sendCommand("STORE \(uid) +FLAGS (\\Seen)")
    }

    func deleteEmail(uid: UInt32) async throws {
        _ = try await sendCommand("STORE \(uid) +FLAGS (\\Deleted)")
    }

    func expunge() async throws {
        _ = try await sendCommand("EXPUNGE")
    }

    // MARK: - Low-level Protocol

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

        let tag = "A\(commandTag)"
        commandTag += 1

        let fullCommand = "\(tag) \(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
        return try await readResponse(until: tag)
    }

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

        var fullResponse = ""

        while true {
            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"))
                    }
                }
            }

            receiveBuffer.append(data)

            if let response = String(data: receiveBuffer, encoding: .utf8) {
                fullResponse = response

                // Check if we have a complete response
                if let tag = tag {
                    if response.contains("\(tag) OK") || response.contains("\(tag) NO") || response.contains("\(tag) BAD") {
                        receiveBuffer.removeAll()
                        break
                    }
                } else {
                    // Just return what we got
                    receiveBuffer.removeAll()
                    break
                }
            }
        }

        return fullResponse
    }

    // MARK: - Email Parsing

    private func parseEmailResponse(_ response: String, uid: UInt32) throws -> IncomingEmail {
        // Basic email parsing - extract headers and body
        let lines = response.split(separator: "\r\n", omittingEmptySubsequences: false)

        var from = ""
        var subject = ""
        var messageId = ""
        var inReplyTo: String?
        var body = ""
        var inBody = false
        var receivedDate = Date()

        for line in lines {
            let lineStr = String(line)

            if lineStr.hasPrefix("From: ") {
                from = String(lineStr.dropFirst(6))
                // Extract email from "Name <email@domain.com>"
                if let start = from.firstIndex(of: "<"), let end = from.firstIndex(of: ">") {
                    from = String(from[start...end].dropFirst().dropLast())
                }
            } else if lineStr.hasPrefix("Subject: ") {
                subject = String(lineStr.dropFirst(9))
            } else if lineStr.hasPrefix("Message-ID: ") || lineStr.hasPrefix("Message-Id: ") {
                messageId = String(lineStr.split(separator: ": ", maxSplits: 1).last ?? "")
            } else if lineStr.hasPrefix("In-Reply-To: ") {
                inReplyTo = String(lineStr.split(separator: ": ", maxSplits: 1).last ?? "")
            } else if lineStr.hasPrefix("Date: ") {
                let dateStr = String(lineStr.dropFirst(6))
                // Parse RFC 2822 date format
                let formatter = DateFormatter()
                formatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss Z"
                formatter.locale = Locale(identifier: "en_US_POSIX")
                if let date = formatter.date(from: dateStr) {
                    receivedDate = date
                }
            } else if lineStr.isEmpty && !inBody {
                // Empty line marks end of headers
                inBody = true
            } else if inBody {
                body += lineStr + "\n"
            }
        }

        return IncomingEmail(
            id: UUID(),
            uid: uid,
            messageId: messageId.isEmpty ? "msg-\(uid)" : messageId,
            from: from,
            to: [username],
            subject: subject,
            body: body.trimmingCharacters(in: .whitespacesAndNewlines),
            receivedDate: receivedDate,
            inReplyTo: inReplyTo
        )
    }
}
