import Foundation
import LilithLogging
import MacSyncShared

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

// MARK: - Apple Notes via AppleScript — known limitations
//
// - **Attachments** (images, scans, drawings) do NOT round-trip through
//   AppleScript. We sync only title, body (HTML), folder, and
//   modification date. Any attachment in a note authored on the Mac
//   side will be silently dropped on the server-side copy and will not
//   re-appear after a server-side edit.
// - **Rich formatting** (bulleted/numbered lists, tables, font styling)
//   may degrade through the HTML round-trip. Apple Notes' AppleScript
//   `body` accessor emits a flavor of HTML that does not exactly match
//   what the app stores internally.
// - **Note IDs** returned by AppleScript (`id of note`) are stable
//   within a given macOS version but the URI format is not publicly
//   documented. Apple may change this in future releases; if that
//   happens we will need to re-key against name+modificationDate.
// - **Permission**: requires "Automation" entitlement granting the Mac
//   client the right to control Notes.app. Granted via System Settings
//   → Privacy & Security → Automation. Without it every AppleScript
//   call fails immediately.

// MARK: - Wire types

public struct NoteSendPayload: Sendable, Decodable, Equatable {
    public let noteIdentifier: String?
    public let folder: String?
    public let name: String?
    public let body: String?

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

public struct PendingNoteSend: Sendable, Decodable, Equatable {
    public let id: String
    public let action: String
    public let payload: NoteSendPayload
    public let createdAt: String

    public init(id: String, action: String, payload: NoteSendPayload, createdAt: String) {
        self.id = id
        self.action = action
        self.payload = payload
        self.createdAt = createdAt
    }
}

// MARK: - Applier protocol (allows hermetic tests without AppleScript)

/// Narrow protocol the dispatcher calls into so tests can substitute a
/// fake that does not require Notes.app automation authorization.
public protocol NoteApplying: Sendable {
    func create(_ payload: NoteSendPayload) async -> SendQueueApplyResult
    func update(_ payload: NoteSendPayload) async -> SendQueueApplyResult
    func delete(_ payload: NoteSendPayload) async -> SendQueueApplyResult
}

// MARK: - Sender

/// Applies pending note send-queue items against Notes.app via AppleScript.
@MainActor
public final class NoteSender: NoteApplying {
    private let applier: any NoteApplying

    public init(applier: (any NoteApplying)? = nil) {
        self.applier = applier ?? AppleScriptNoteApplier()
    }

    public func apply(_ item: PendingNoteSend) async -> SendQueueApplyResult {
        log.info("apply id=\(item.id) action=\(item.action)")
        switch item.action {
        case "create_note": return await applier.create(item.payload)
        case "update_note": return await applier.update(item.payload)
        case "delete_note": return await applier.delete(item.payload)
        default:
            return .failed(reason: "unknown action: \(item.action)")
        }
    }

    // Pass-throughs so the sender itself conforms to NoteApplying and can
    // be used as a `NoteApplying` value in tests.
    public func create(_ payload: NoteSendPayload) async -> SendQueueApplyResult {
        await applier.create(payload)
    }
    public func update(_ payload: NoteSendPayload) async -> SendQueueApplyResult {
        await applier.update(payload)
    }
    public func delete(_ payload: NoteSendPayload) async -> SendQueueApplyResult {
        await applier.delete(payload)
    }
}

// MARK: - AppleScript-backed applier

/// Default `NoteApplying` that drives Notes.app via osascript.
public final class AppleScriptNoteApplier: NoteApplying, @unchecked Sendable {
    public init() {}

    public func create(_ payload: NoteSendPayload) async -> SendQueueApplyResult {
        guard let name = payload.name, !name.isEmpty else {
            return .failed(reason: "missing name")
        }
        let body = payload.body ?? ""
        let folder = payload.folder ?? "Notes"
        let script = Self.scriptForCreate(name: name, body: body, folder: folder)
        let (ok, err) = await Self.runAppleScript(script)
        return ok ? .sent : .failed(reason: err)
    }

    public func update(_ payload: NoteSendPayload) async -> SendQueueApplyResult {
        guard let id = payload.noteIdentifier, !id.isEmpty else {
            return .failed(reason: "missing noteIdentifier")
        }
        guard payload.name != nil || payload.body != nil else {
            return .failed(reason: "no fields to update")
        }
        let script = Self.scriptForUpdate(id: id, name: payload.name, body: payload.body)
        let (ok, err) = await Self.runAppleScript(script)
        return ok ? .sent : .failed(reason: err)
    }

    public func delete(_ payload: NoteSendPayload) async -> SendQueueApplyResult {
        guard let id = payload.noteIdentifier, !id.isEmpty else {
            return .failed(reason: "missing noteIdentifier")
        }
        let script = Self.scriptForDelete(id: id)
        let (ok, err) = await Self.runAppleScript(script)
        return ok ? .sent : .failed(reason: err)
    }

    // MARK: - Script generation (pure, testable)

    /// Build the AppleScript source for a `create_note` action. All
    /// interpolated values are escaped via `AppleScriptEscape.quote`.
    public static func scriptForCreate(name: String, body: String, folder: String) -> String {
        let qName = AppleScriptEscape.quote(name)
        let qBody = AppleScriptEscape.quote(body)
        let qFolder = AppleScriptEscape.quote(folder)
        return """
        tell application "Notes"
            tell folder "\(qFolder)" of default account
                make new note with properties {name:"\(qName)", body:"\(qBody)"}
            end tell
        end tell
        """
    }

    /// Build the AppleScript source for an `update_note` action.
    public static func scriptForUpdate(id: String, name: String?, body: String?) -> String {
        let qId = AppleScriptEscape.quote(id)
        var setters: [String] = []
        if let name {
            setters.append("set name of note id \"\(qId)\" to \"\(AppleScriptEscape.quote(name))\"")
        }
        if let body {
            setters.append("set body of note id \"\(qId)\" to \"\(AppleScriptEscape.quote(body))\"")
        }
        let inner = setters.joined(separator: "\n    ")
        return """
        tell application "Notes"
            \(inner)
        end tell
        """
    }

    /// Build the AppleScript source for a `delete_note` action.
    public static func scriptForDelete(id: String) -> String {
        let qId = AppleScriptEscape.quote(id)
        return """
        tell application "Notes"
            delete note id "\(qId)"
        end tell
        """
    }

    // MARK: - osascript runner

    /// Returns `(success, errorMessage)`. Uses /usr/bin/osascript so we
    /// never block a Swift main-actor task on a long AppleScript event.
    static 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 errData = errPipe.fileHandleForReading.readDataToEndOfFile()
                let errStr = String(data: errData, encoding: .utf8) ?? ""
                if process.terminationStatus == 0 {
                    return (true, "")
                } else {
                    return (false, errStr.isEmpty ? "osascript exit \(process.terminationStatus)" : errStr)
                }
            } catch {
                return (false, error.localizedDescription)
            }
        }.value
    }
}

// MARK: - SendQueueTransport adapter

public struct NoteSendTransport: SendQueueTransport {
    public typealias PendingItem = PendingNoteSend

    private let apiClient: any INoteAPIClientProtocol

    public init(apiClient: any INoteAPIClientProtocol) {
        self.apiClient = apiClient
    }

    public func id(of item: PendingNoteSend) -> String { item.id }

    public func fetchPending() async throws -> [PendingNoteSend] {
        try await apiClient.getPendingSends()
    }

    public func reportResult(id: String, status: String, error: String?) async throws {
        try await apiClient.reportSendResult(id: id, status: status, error: error)
    }
}
