import AppKit
import Foundation
import LilithLogging

private let log = AppLogger.logger(for: "INote.Reader")

// MARK: - NoteEntry

public struct NoteEntry: Sendable, Equatable {
    public let noteIdentifier: String
    public let folder: String?
    public let name: String
    public let body: String      // HTML
    public let modificationDate: Date?

    public init(
        noteIdentifier: String,
        folder: String?,
        name: String,
        body: String,
        modificationDate: Date?
    ) {
        self.noteIdentifier = noteIdentifier
        self.folder = folder
        self.name = name
        self.body = body
        self.modificationDate = modificationDate
    }
}

// MARK: - NotesReader

/// Reads notes from Apple Notes via AppleScript.
///
/// Apple Notes has no public Cocoa framework — AppleScript is the only
/// supported automation surface. We run scripts via the `osascript`
/// subprocess (rather than `NSAppleScript`) so we never block the main
/// thread on long-running automation events.
///
/// # Output parsing
/// The fetch script joins each note's fields with the ASCII Unit Separator
/// (`\u{001F}`) and notes themselves with the ASCII Record Separator
/// (`\u{001E}`). These control characters are extremely unlikely to appear
/// inside note bodies (Apple Notes bodies are sanitized HTML), so splitting
/// on them is safe in practice. If we see field corruption in the field, a
/// follow-up could migrate this to JXA (JavaScript for Automation) which
/// emits clean JSON.
@MainActor
public final class NotesReader {
    public static let shared = NotesReader()

    public private(set) var isAuthorized: Bool = false

    private init() {}

    /// AppleScript automation permission for Notes.app must be granted in
    /// System Settings → Privacy & Security → Automation. We prime the
    /// prompt with a trivial script; if it fails we mark unauthorized.
    public func requestAuthorization() async -> Bool {
        let probe = "tell application \"Notes\" to get name"
        let (ok, _) = await runAppleScript(probe)
        isAuthorized = ok
        if !ok {
            log.warning("Notes automation authorization probe failed")
        }
        return ok
    }

    /// Fetch every note from Notes.app. Empty array on failure.
    public func fetchAllNotes() async -> [NoteEntry] {
        let script = """
        tell application "Notes"
            set output to ""
            repeat with n in every note
                try
                    set noteId to id of n as string
                    set noteName to name of n as string
                    set noteBody to body of n as string
                    set noteFolder to ""
                    try
                        set noteFolder to name of (container of n) as string
                    end try
                    set noteMod to ""
                    try
                        set noteMod to ((modification date of n) as «class isot» as string)
                    end try
                    set output to output & noteId & "\\u001F" & noteName & "\\u001F" & noteBody & "\\u001F" & noteFolder & "\\u001F" & noteMod & "\\u001E"
                end try
            end repeat
            return output
        end tell
        """
        let (ok, out) = await runAppleScript(script)
        guard ok else {
            log.warning("fetchAllNotes script failed")
            return []
        }
        return Self.parse(out)
    }

    /// Parse the joined AppleScript output. `internal` so tests can hit it.
    /// `nonisolated` because parsing is a pure function and the test suite
    /// calls it from a non-actor context.
    nonisolated static func parse(_ raw: String) -> [NoteEntry] {
        let fieldSep = Character("\u{001F}")
        let recordSep = Character("\u{001E}")
        let iso = ISO8601DateFormatter()
        iso.formatOptions = [.withInternetDateTime]
        let isoFractional = ISO8601DateFormatter()
        isoFractional.formatOptions = [.withInternetDateTime, .withFractionalSeconds]

        var out: [NoteEntry] = []
        for record in raw.split(separator: recordSep, omittingEmptySubsequences: true) {
            let fields = record.split(separator: fieldSep, omittingEmptySubsequences: false).map(String.init)
            // Need at least id, name, body, folder, mod — defensively skip malformed
            guard fields.count >= 5 else { continue }
            let id = fields[0]
            guard !id.isEmpty else { continue }
            let name = fields[1]
            let body = fields[2]
            let folder = fields[3].isEmpty ? nil : fields[3]
            let modStr = fields[4]
            let mod: Date? = {
                guard !modStr.isEmpty else { return nil }
                return isoFractional.date(from: modStr) ?? iso.date(from: modStr)
            }()
            out.append(NoteEntry(
                noteIdentifier: id,
                folder: folder,
                name: name,
                body: body,
                modificationDate: mod
            ))
        }
        return out
    }

    /// Run AppleScript via /usr/bin/osascript subprocess. Returns
    /// `(success, stdout-or-stderr)`. We deliberately do NOT use
    /// `NSAppleScript` because it blocks the calling thread; `osascript`
    /// runs out-of-process and is awaitable.
    private func runAppleScript(_ source: String) async -> (Bool, String) {
        await Task.detached {
            let process = Process()
            process.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
            process.arguments = ["-e", source]
            let outPipe = Pipe()
            let errPipe = Pipe()
            process.standardOutput = outPipe
            process.standardError = errPipe
            do {
                try process.run()
                process.waitUntilExit()
                let outData = outPipe.fileHandleForReading.readDataToEndOfFile()
                let errData = errPipe.fileHandleForReading.readDataToEndOfFile()
                let outStr = String(data: outData, encoding: .utf8) ?? ""
                let errStr = String(data: errData, encoding: .utf8) ?? ""
                if process.terminationStatus == 0 {
                    return (true, outStr)
                } else {
                    return (false, errStr.isEmpty ? outStr : errStr)
                }
            } catch {
                return (false, error.localizedDescription)
            }
        }.value
    }
}
