import Foundation
import ScriptingBridge

// MARK: - ScriptingBridge Protocol Declarations
//
// Mail.app exposes its object model via AppleScript/ScriptingBridge.  We declare
// lightweight @objc protocols here that mirror the Mail.sdef so we can call them
// from Swift without a bridging-header dependency on a generated Mail.h.
//
// To regenerate the full ObjC header on plum (optional, for IDE completion):
//   sdef /System/Applications/Mail.app | sdp -fh --basename Mail -o /tmp/
//
// The protocols below cover exactly what IMailSync needs; they are NOT an exhaustive
// mapping of the Mail dictionary.
//
// NOTE: Protocol names use the "SB" prefix to avoid collision with the MailMessage
// struct defined below.

@objc protocol SBMailMessage {
    @objc optional var messageId: String { get }
    @objc optional var subject: String { get }
    @objc optional var sender: String { get }
    @objc optional var dateSent: Date { get }
    @objc optional var dateReceived: Date { get }
    @objc optional var isRead: Bool { get }
    @objc optional var content: SBObject { get }
    @objc optional var source: String { get }
    @objc optional var toRecipients: SBElementArray { get }
    @objc optional var ccRecipients: SBElementArray { get }
    @objc optional var numberOfAttachments: Int { get }
}

@objc protocol SBMailRecipient {
    @objc optional var address: String { get }
    @objc optional var name: String { get }
}

@objc protocol SBMailMailbox {
    @objc optional var name: String { get }
    @objc optional func messages() -> SBElementArray
}

@objc protocol SBMailAccount {
    @objc optional var name: String { get }
    @objc optional func mailboxes() -> SBElementArray
}

@objc protocol SBMailApplication {
    @objc optional func accounts() -> SBElementArray
}

// MARK: - Models

/// An email message extracted from Mail.app, ready to push to the server.
public struct MailMessage: Sendable {
    public let messageId: String
    public let threadId: String?
    public let subject: String?
    public let fromAddress: String
    public let fromName: String?
    public let to: [EmailAddress]
    public let cc: [EmailAddress]
    public let folder: String
    public let direction: String           // "incoming" | "outgoing"
    public let textBody: String?
    public let htmlBody: String?
    public let hasAttachments: Bool
    public let attachmentCount: Int
    public let sentAt: String             // ISO 8601
    public let receivedAt: String?
    public let isRead: Bool
}

public struct EmailAddress: Sendable {
    public let address: String
    public let name: String?
}

// MARK: - Reader

/// Reads mail from Mail.app via ScriptingBridge.
///
/// Incremental sync: the caller provides a `since` watermark (last seen send date).
/// We enumerate all known mailboxes on all accounts and return messages whose
/// `dateSent` is after `since`.
///
/// Mail.app must be running and accessible for ScriptingBridge calls to succeed.
/// The automation entitlement (`com.apple.security.automation.apple-events`) must be
/// present and the user must have granted automation access to Mail.app.
public final class Reader: @unchecked Sendable {
    public static let shared = Reader()

    /// Set of known own email addresses (used to classify direction).
    /// Populated from UserDefaults key `imailOwnAddresses`.
    private var ownAddresses: Set<String> {
        let stored = UserDefaults.standard.stringArray(forKey: "imailOwnAddresses") ?? []
        return Set(stored.map { $0.lowercased() })
    }

    private init() {}

    // MARK: - Public API

    /// Enumerate all messages in all account mailboxes with `dateSent` after `since`.
    ///
    /// - Parameter since: Only return messages with `dateSent` strictly after this date.
    ///                    Pass `nil` on first run to back-fill everything.
    /// - Returns: All matching messages across all folders.
    public func fetchMessages(since: Date?) -> [MailMessage] {
        guard let app = SBApplication(bundleIdentifier: "com.apple.mail"),
              let mailApp = app as? SBMailApplication else {
            NSLog("IMailReader: Mail.app not available via ScriptingBridge")
            return []
        }

        guard let accountsArray = mailApp.accounts?() as? [AnyObject] else {
            NSLog("IMailReader: no accounts from Mail.app")
            return []
        }

        var results: [MailMessage] = []
        let own = ownAddresses

        for accountObj in accountsArray {
            guard let acc = accountObj as? SBMailAccount,
                  let mailboxesArray = acc.mailboxes?() as? [AnyObject] else { continue }

            for mailboxObj in mailboxesArray {
                guard let mb = mailboxObj as? SBMailMailbox,
                      let mbName = mb.name,
                      let messagesArray = mb.messages?() as? [AnyObject] else { continue }

                for messageObj in messagesArray {
                    guard let msg = messageObj as? SBMailMessage else { continue }
                    guard let sent = msg.dateSent else { continue }
                    if let since, sent <= since { continue }

                    guard let parsed = parseMessage(msg, folder: mbName, ownAddresses: own) else { continue }
                    results.append(parsed)
                }
            }
        }

        NSLog("IMailReader: fetched \(results.count) messages since \(String(describing: since))")
        return results
    }

    // MARK: - Private

    private func parseMessage(_ msg: SBMailMessage, folder: String, ownAddresses: Set<String>) -> MailMessage? {
        let msgId = msg.messageId ?? "mail-app-\(UUID().uuidString)"
        let senderStr = msg.sender ?? ""
        let (fromAddr, fromName) = parseSenderString(senderStr)
        guard !fromAddr.isEmpty else { return nil }

        let direction = ownAddresses.contains(fromAddr.lowercased()) ? "outgoing" : "incoming"
        let df = ISO8601DateFormatter()

        let toRecipients: [EmailAddress] = (msg.toRecipients as? [AnyObject])?.compactMap { obj in
            guard let r = obj as? SBMailRecipient, let addr = r.address, !addr.isEmpty else { return nil }
            return EmailAddress(address: addr, name: r.name)
        } ?? []

        let ccRecipients: [EmailAddress] = (msg.ccRecipients as? [AnyObject])?.compactMap { obj in
            guard let r = obj as? SBMailRecipient, let addr = r.address, !addr.isEmpty else { return nil }
            return EmailAddress(address: addr, name: r.name)
        } ?? []

        // Use raw MIME source as textBody if available; fall back to content.
        let textBody: String?
        if let src = msg.source, !src.isEmpty {
            textBody = src
        } else if let contentObj = msg.content {
            textBody = contentObj.get() as? String
        } else {
            textBody = nil
        }

        let attachmentCount = msg.numberOfAttachments ?? 0

        return MailMessage(
            messageId: msgId,
            threadId: nil,   // Thread grouping is server-side (same as IMAP path)
            subject: msg.subject.flatMap { $0.isEmpty ? nil : $0 },
            fromAddress: fromAddr,
            fromName: fromName,
            to: toRecipients,
            cc: ccRecipients,
            folder: folder,
            direction: direction,
            textBody: textBody,
            htmlBody: nil,   // HTML extraction from raw source is done in MIMEParser
            hasAttachments: attachmentCount > 0,
            attachmentCount: attachmentCount,
            sentAt: df.string(from: msg.dateSent ?? Date()),
            receivedAt: msg.dateReceived.map { df.string(from: $0) },
            isRead: msg.isRead ?? false
        )
    }

    /// Parse Mail.app's `sender` string ("Name <addr>" or "addr") into (address, name?).
    private func parseSenderString(_ sender: String) -> (address: String, name: String?) {
        let trimmed = sender.trimmingCharacters(in: .whitespaces)
        if let ltRange = trimmed.range(of: "<"),
           let gtRange = trimmed.range(of: ">"),
           ltRange.lowerBound < gtRange.lowerBound {
            let addr = String(trimmed[ltRange.upperBound..<gtRange.lowerBound]).trimmingCharacters(in: .whitespaces)
            let name = String(trimmed[trimmed.startIndex..<ltRange.lowerBound]).trimmingCharacters(in: .whitespaces)
            return (addr, name.isEmpty ? nil : name)
        }
        return (trimmed, nil)
    }
}
