// mail-search.swift — Fast email search via Mail's Envelope Index (SQLite)
// Requires Full Disk Access to read ~/Library/Mail/V*/MailData/Envelope Index.
// Falls back gracefully with exit code 2 if FDA is not available.
//
// Usage: swift mail-search.swift --query "search term" [--sender "name"] [--days-back 30] [--limit 20] [--content]
// Output: JSON { count, messages: [...] }

import Foundation
import SQLite3

// --- Parse CLI args ---
var query = ""
var sender = ""
var daysBack = 30
var limit = 20
var includeContent = false
let args = CommandLine.arguments
for i in 0..<args.count {
    if args[i] == "--query", i + 1 < args.count { query = args[i + 1] }
    if args[i] == "--sender", i + 1 < args.count { sender = args[i + 1] }
    if args[i] == "--days-back", i + 1 < args.count, let n = Int(args[i + 1]) { daysBack = n }
    if args[i] == "--limit", i + 1 < args.count, let n = Int(args[i + 1]) { limit = n }
    if args[i] == "--content" { includeContent = true }
}
if query.isEmpty && sender.isEmpty {
    let err: [String: Any] = ["error": "Provide --query or --sender", "count": 0, "messages": [Any]()]
    print(jsonString(err))
    exit(1)
}

// --- Find Envelope Index ---
let home = FileManager.default.homeDirectoryForCurrentUser.path
var envelopePath: String?

// Try both containerized (modern macOS) and legacy paths
let searchDirs = [
    "\(home)/Library/Mail",
    "\(home)/Library/Containers/com.apple.mail/Data/Library/Mail",
]
for mailDataDir in searchDirs {
    for v in stride(from: 10, through: 1, by: -1) {
        let candidate = "\(mailDataDir)/V\(v)/MailData/Envelope Index"
        if FileManager.default.fileExists(atPath: candidate) {
            envelopePath = candidate
            break
        }
    }
    if envelopePath != nil { break }
}

guard let dbPath = envelopePath else {
    let err: [String: Any] = ["error": "Mail Envelope Index not found", "count": 0, "messages": [Any]()]
    print(jsonString(err))
    exit(2)
}

// --- Open database ---
var db: OpaquePointer?
let rc = sqlite3_open_v2(dbPath, &db, SQLITE_OPEN_READONLY, nil)
guard rc == SQLITE_OK else {
    let errMsg = db != nil ? String(cString: sqlite3_errmsg(db)) : "unknown"
    sqlite3_close(db)
    if errMsg.contains("authorization") {
        // TCC restriction — no Full Disk Access
        let err: [String: Any] = ["error": "Full Disk Access required. Grant it in System Settings > Privacy & Security > Full Disk Access.", "count": 0, "messages": [Any]()]
        print(jsonString(err))
        exit(2)
    }
    let err: [String: Any] = ["error": "DB open failed: \(errMsg)", "count": 0, "messages": [Any]()]
    print(jsonString(err))
    exit(1)
}

// --- Discover schema (first run) ---
// The Envelope Index schema varies by macOS version. Discover the columns we need.
let tableNames = queryColumn(db!, "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")

// Messages table might be named 'messages' or something similar
let msgTable: String
if tableNames.contains("messages") {
    msgTable = "messages"
} else {
    let err: [String: Any] = ["error": "Unknown schema. Tables: \(tableNames.joined(separator: ", "))", "count": 0, "messages": [Any]()]
    print(jsonString(err))
    sqlite3_close(db)
    exit(1)
}

// Discover columns
let columns = queryColumn(db!, "PRAGMA table_info(\(msgTable))").enumerated().map { _ in "" }
let colInfo = queryRows(db!, "PRAGMA table_info(\(msgTable))")
let colNames = Set(colInfo.map { $0["name"] as? String ?? "" })

// Build the query — adapt to available columns
let subjectCol = colNames.contains("subject") ? "subject" : (colNames.contains("_subject") ? "_subject" : nil)
let senderCol = colNames.contains("sender") ? "sender" : (colNames.contains("_sender") ? "_sender" : nil)
let dateCol = colNames.contains("date_received") ? "date_received"
    : colNames.contains("date_sent") ? "date_sent"
    : colNames.contains("date_created") ? "date_created"
    : nil
let messageIdCol = colNames.contains("message_id") ? "message_id" : (colNames.contains("_message_id") ? "_message_id" : nil)

guard let sc = subjectCol else {
    let err: [String: Any] = ["error": "Cannot find subject column. Columns: \(colNames.sorted().joined(separator: ", "))", "count": 0, "messages": [Any]()]
    print(jsonString(err))
    sqlite3_close(db)
    exit(1)
}

// Build WHERE clause
var conditions = [String]()
var params = [String]()
if !query.isEmpty {
    conditions.append("\(sc) LIKE ?")
    params.append("%\(query)%")
}
if !sender.isEmpty, let sndr = senderCol {
    conditions.append("\(sndr) LIKE ?")
    params.append("%\(sender)%")
}
if let dc = dateCol {
    // Mail stores dates as Mac epoch (seconds since 2001-01-01)
    let macEpochOffset = 978307200.0 // Unix timestamp of 2001-01-01
    let cutoff = Date().timeIntervalSince1970 - Double(daysBack * 86400) - macEpochOffset
    conditions.append("\(dc) >= ?")
    params.append(String(cutoff))
}

let whereClause = conditions.isEmpty ? "1=1" : conditions.joined(separator: " AND ")

// Select columns
var selectCols = [sc]
if let s = senderCol { selectCols.append(s) }
if let d = dateCol { selectCols.append(d) }
if let m = messageIdCol { selectCols.append(m) }
// Check for mailbox/read status columns
if colNames.contains("read") { selectCols.append("read") }
if colNames.contains("mailbox") { selectCols.append("mailbox") }

let sql = "SELECT \(selectCols.joined(separator: ", ")) FROM \(msgTable) WHERE \(whereClause) ORDER BY \(dateCol ?? "rowid") DESC LIMIT \(limit)"

var stmt: OpaquePointer?
guard sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK else {
    let errMsg = String(cString: sqlite3_errmsg(db))
    let err: [String: Any] = ["error": "Query failed: \(errMsg)", "sql": sql, "count": 0, "messages": [Any]()]
    print(jsonString(err))
    sqlite3_close(db)
    exit(1)
}

// Bind parameters
for (i, param) in params.enumerated() {
    sqlite3_bind_text(stmt, Int32(i + 1), (param as NSString).utf8String, -1, nil)
}

// Execute and collect results
var messages = [[String: Any]]()
let macEpochOffset = 978307200.0

while sqlite3_step(stmt) == SQLITE_ROW {
    var msg = [String: Any]()
    for i in 0..<sqlite3_column_count(stmt) {
        let name = String(cString: sqlite3_column_name(stmt, i))
        let type = sqlite3_column_type(stmt, i)
        switch type {
        case SQLITE_TEXT:
            msg[name] = String(cString: sqlite3_column_text(stmt, i))
        case SQLITE_INTEGER:
            msg[name] = sqlite3_column_int64(stmt, i)
        case SQLITE_FLOAT:
            let val = sqlite3_column_double(stmt, i)
            if name.contains("date") {
                // Convert Mac epoch to ISO 8601
                let unix = val + macEpochOffset
                let date = Date(timeIntervalSince1970: unix)
                let fmt = ISO8601DateFormatter()
                msg[name] = fmt.string(from: date)
            } else {
                msg[name] = val
            }
        case SQLITE_NULL:
            msg[name] = NSNull()
        default:
            break
        }
    }
    messages.append(msg)
}

sqlite3_finalize(stmt)
sqlite3_close(db)

let result: [String: Any] = ["count": messages.count, "messages": messages]
print(jsonString(result))

// --- Helpers ---
func jsonString(_ dict: [String: Any]) -> String {
    if let data = try? JSONSerialization.data(withJSONObject: dict, options: [.sortedKeys]),
       let str = String(data: data, encoding: .utf8) {
        return str
    }
    return "{\"error\":\"JSON serialization failed\"}"
}

func queryColumn(_ db: OpaquePointer, _ sql: String) -> [String] {
    var stmt: OpaquePointer?
    var results = [String]()
    if sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK {
        while sqlite3_step(stmt) == SQLITE_ROW {
            if let text = sqlite3_column_text(stmt, 0) {
                results.append(String(cString: text))
            }
        }
    }
    sqlite3_finalize(stmt)
    return results
}

func queryRows(_ db: OpaquePointer, _ sql: String) -> [[String: Any]] {
    var stmt: OpaquePointer?
    var results = [[String: Any]]()
    if sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK {
        let colCount = sqlite3_column_count(stmt)
        while sqlite3_step(stmt) == SQLITE_ROW {
            var row = [String: Any]()
            for i in 0..<colCount {
                let name = String(cString: sqlite3_column_name(stmt, i))
                if let text = sqlite3_column_text(stmt, i) {
                    row[name] = String(cString: text)
                }
            }
            results.append(row)
        }
    }
    sqlite3_finalize(stmt)
    return results
}
