// BTT-Plugin-Name: BetterZip
// BTT-Plugin-Identifier: com.macitbetter.betterzip.btt.launcher
// BTT-Plugin-Type: Launcher
// BTT-Plugin-Icon: archivebox

import AppKit

/// BetterTouchTool launcher plugin for BetterZip.
///
/// This plugin lists the user's compress and extract presets and runs the 
/// chosen preset on the current Finder selection.
/// Presets are read straight from BetterZip's preferences; the operation is
/// driven through BetterZip's AppleScript scripting interface.
///
/// The plugin auto-detects which build of BetterZip is present (the regular/direct
/// build or the Setapp build) and reads its presets and sends its commands to that
/// one, so a single plugin works no matter which build is installed.
final class BetterZipLauncherPlugin: NSObject, BTTLauncherPluginInterface {
    weak var delegate: (any BTTLauncherPluginDelegate)?

    /// The bundle identifiers of the known BetterZip builds, in preference order:
    /// the regular/direct build first, then the Setapp build. A build's bundle
    /// identifier doubles as its preferences domain (where its presets live).
    private static let candidateBundleIDs = [
        "com.macitbetter.betterzip",
        "com.macitbetter.betterzip-setapp",
    ]

    /// The bundle identifier of the BetterZip build to talk to. Resolved once:
    /// a running build wins; otherwise the first installed build; otherwise the
    /// direct build's identifier as a last resort.
    private lazy var betterZipBundleID: String = Self.resolveBetterZipBundleID()

    private static func resolveBetterZipBundleID() -> String {
        // Prefer a build that is currently running.
        for bundleID in candidateBundleIDs
        where !NSRunningApplication.runningApplications(withBundleIdentifier: bundleID).isEmpty {
            return bundleID
        }
        // Otherwise the first build that is installed.
        let workspace = NSWorkspace.shared
        for bundleID in candidateBundleIDs
        where workspace.urlForApplication(withBundleIdentifier: bundleID) != nil {
            return bundleID
        }
        return candidateBundleIDs[0]
    }

    private enum Op {
        static let compress = "compress"
        static let extract = "extract"
    }

    static func launcherPluginName() -> String { "BetterZip" }
    static func launcherPluginDescription() -> String {
        "Compress or extract the Finder selection with a BetterZip preset."
    }
    static func launcherPluginIcon() -> String { "archivebox" }

    // MARK: - Results

    func launcherResults(for context: BTTLauncherPluginContext) -> [BTTLauncherPluginResult]? {
        let compress = makeOperationResult(
            operation: Op.compress,
            title: "Compress with BetterZip",
            subtitle: "Create an archive from the Finder selection using a save preset.",
            icon: "archivebox",
            actionHint: "Compress",
            presetKey: "MIBSavePresets")

        let extract = makeOperationResult(
            operation: Op.extract,
            title: "Extract with BetterZip",
            subtitle: "Extract the selected archives using an extraction preset.",
            icon: "arrow.up.bin",
            actionHint: "Extract",
            presetKey: "MIBExtractPresets")

        return [compress, extract]
    }

    /// Builds a top-level row whose children are the matching presets.
    private func makeOperationResult(
        operation: String,
        title: String,
        subtitle: String,
        icon: String,
        actionHint: String,
        presetKey: String
    ) -> BTTLauncherPluginResult {
        let result = BTTLauncherPluginResult()
        result.itemIdentifier = operation
        result.title = title
        result.systemImageName = icon
        result.keywords = ["betterzip", "archive", "zip", operation]

        let presets = loadPresetNames(forKey: presetKey)
        if presets.isEmpty {
            result.subtitle = "No presets found. Add presets in BetterZip ▸ Settings ▸ Presets."
        } else {
            result.subtitle = subtitle
            result.trailingHint = "\(presets.count) preset\(presets.count == 1 ? "" : "s")"
            result.children = presets.map { name in
                let child = BTTLauncherPluginResult()
                child.itemIdentifier = "\(operation):\(name)"
                child.title = name
                child.subtitle = title
                child.systemImageName = icon
                child.primaryActionIdentifier = "run"
                child.trailingHint = actionHint
                child.keywords = [name, operation]
                return child
            }
        }
        return result
    }

    // MARK: - Actions

    func performAction(
        forItemIdentifier itemIdentifier: String,
        actionIdentifier: String?,
        context: BTTLauncherPluginContext
    ) -> BTTLauncherPluginActionResult? {
        // Child identifiers look like "compress:<preset name>".
        let parts = itemIdentifier.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false)
        guard parts.count == 2 else { return nil }
        let operation = String(parts[0])
        let presetName = String(parts[1])
        guard operation == Op.compress || operation == Op.extract else { return nil }

        let files: [String]
        do {
            files = try selectedFinderPaths()
        } catch {
            return failure("Could not read the Finder selection. \(error.localizedDescription)")
        }
        guard !files.isEmpty else {
            let noun = operation == Op.extract ? "archive" : "item"
            return failure("Select one or more \(noun)s in Finder first.")
        }

        do {
            try runBetterZip(operation: operation, presetName: presetName, files: files)
        } catch {
            return failure(error.localizedDescription)
        }

        let verb = operation == Op.compress ? "Compressing" : "Extracting"
        let result = BTTLauncherPluginActionResult()
        result.success = true
        result.message = "\(verb) with “\(presetName)”…"
        result.closeLauncher = true
        return result
    }

    // MARK: - Helpers

    private func failure(_ message: String) -> BTTLauncherPluginActionResult {
        let result = BTTLauncherPluginActionResult()
        result.success = false
        result.message = message
        result.closeLauncher = false
        return result
    }

    /// Reads the preset names (the "name" field of each preset dictionary) from
    /// the detected BetterZip build's preferences for the given key.
    private func loadPresetNames(forKey key: String) -> [String] {
        guard let defaults = UserDefaults(suiteName: betterZipBundleID),
              let raw = defaults.array(forKey: key) as? [[String: Any]] else {
            return []
        }
        return raw.compactMap { $0["name"] as? String }
    }

    /// Returns the POSIX paths of the items currently selected in Finder.
    private func selectedFinderPaths() throws -> [String] {
        let script = """
        tell application "Finder"
            set selectedItems to selection
            if (count of selectedItems) is 0 then return ""
            set selectedPaths to {}
            repeat with currentItem in selectedItems
                set end of selectedPaths to POSIX path of (currentItem as alias)
            end repeat
            set previousDelimiters to AppleScript's text item delimiters
            set AppleScript's text item delimiters to linefeed
            set joinedPaths to selectedPaths as text
            set AppleScript's text item delimiters to previousDelimiters
            return joinedPaths
        end tell
        """
        let output = try runAppleScript(script)
        return output
            .split(whereSeparator: { $0.isNewline })
            .map(String.init)
            .filter { !$0.isEmpty }
    }

    /// Hands the files to BetterZip via its scripting interface, exactly like the
    /// Alfred workflow does (`queued archive … with preset … show ui yes`).
    private func runBetterZip(operation: String, presetName: String, files: [String]) throws {
        let command = operation == Op.compress ? "queued archive" : "queued unarchive"
        let fileList = files
            .map { "POSIX file \"\(escapeForAppleScript($0))\"" }
            .joined(separator: ", ")
        let script = """
        tell application id "\(escapeForAppleScript(betterZipBundleID))"
            \(command) {\(fileList)} with preset "\(escapeForAppleScript(presetName))" show ui yes
        end tell
        """
        _ = try runAppleScript(script)
    }

    private func escapeForAppleScript(_ value: String) -> String {
        value
            .replacingOccurrences(of: "\\", with: "\\\\")
            .replacingOccurrences(of: "\"", with: "\\\"")
    }

    @discardableResult
    private func runAppleScript(_ source: String) throws -> String {
        var errorInfo: NSDictionary?
        guard let script = NSAppleScript(source: source) else {
            throw PluginError.appleScript("The AppleScript could not be created.")
        }
        let descriptor = script.executeAndReturnError(&errorInfo)
        if let errorInfo {
            let message = errorInfo[NSAppleScript.errorMessage] as? String
                ?? String(describing: errorInfo)
            throw PluginError.appleScript(message)
        }
        return descriptor.stringValue ?? ""
    }
}

private enum PluginError: LocalizedError {
    case appleScript(String)

    var errorDescription: String? {
        switch self {
        case .appleScript(let message):
            return message
        }
    }
}
