// CLIPathInstaller.swift
// Manages installation of the CLI tool to the user's PATH by creating
// symlinks in /usr/local/bin.
//
// - Debug builds install as: rpce-cli-debug
// - Release builds install as: rpce-cli
//
// This allows users to invoke the CLI directly from their terminal.

import Foundation
import OSLog
import RepoPromptShared

enum CLIPathInstaller {
    private static let logger = Logger(subsystem: "CLI.PathInstaller", category: "install")

    #if DEBUG
        nonisolated static let identity = MCPFilesystemIdentity.repoPromptCE(.debug)
    #else
        nonisolated static let identity = MCPFilesystemIdentity.repoPromptCE(.release)
    #endif

    nonisolated static var cliCommandName: String {
        identity.pathCLICommandName
    }

    nonisolated static var claudeRPCommandName: String {
        identity.claudeWrapperCommandName
    }

    /// The installation directory for the CLI symlink (nonisolated - constant)
    nonisolated static let installDirectory = "/usr/local/bin"

    /// Full path where the CLI symlink will be created (nonisolated - derived from constants)
    nonisolated static var installPath: String {
        "\(installDirectory)/\(cliCommandName)"
    }

    /// Full path where the claude-rp wrapper will be created
    nonisolated static var claudeRPInstallPath: String {
        "\(installDirectory)/\(claudeRPCommandName)"
    }

    /// Returns the path to the bundled CLI executable
    @MainActor
    static var bundledCLIPath: String? {
        Bundle.main.url(forAuxiliaryExecutable: "repoprompt-mcp")?.path
    }

    /// Built-in Claude Code tools to disable in the claude-rp wrapper.
    /// These overlap with RepoPrompt MCP tools and should use RP versions instead.
    private static let claudeRPDisallowedTools = ["Read", "Write", "Edit", "Glob", "Grep"]
    private static let claudeRPManagedMarker = ManagedCLIPathPolicy.currentClaudeWrapperMarker

    private static func isManagedClaudeRPScript(_ content: String) -> Bool {
        ManagedCLIPathPolicy.isManagedWrapper(content)
    }

    /// Path to the stable MCP config file used by claude-rp wrapper.
    private static var claudeRPConfigPath: String {
        MCPConfigExportService.stableWrapperConfigURL.path
    }

    /// Generates the shell script content for the claude-rp wrapper
    private static func claudeRPScriptContent() -> String {
        let disallowedToolsArg = claudeRPDisallowedTools.joined(separator: ",")
        let configPath = claudeRPConfigPath
        let environmentExports = MCPIntegrationHelper.claudeProcessEnvironmentOverridePairs
            .map { "export \($0.0)=$" + "{\($0.0):-\($0.1)}" }
            .joined(separator: "\n")
        return """
        #!/bin/bash
        \(claudeRPManagedMarker)
        # Generated by RepoPrompt CE
        #
        # This wrapper runs Claude Code with:
        #   - RepoPrompt MCP server enabled via config file
        #   - Claude MCP timeouts/output limits set in the process environment with user-overridable defaults
        #   - Strict MCP config mode (ignores other MCP configs)
        #   - Built-in file tools disabled (uses RepoPrompt's MCP tools instead)
        #
        # Prerequisites:
        #   1. Claude Code CLI installed (https://docs.anthropic.com/claude-code)
        #   2. RepoPrompt CE app running with MCP server enabled
        #
        # Config file: \(configPath)
        # Disabled tools: \(disallowedToolsArg)

        \(environmentExports)
        exec claude --mcp-config "\(configPath)" --strict-mcp-config --disallowedTools \(disallowedToolsArg) "$@"
        """
    }

    #if DEBUG
        static func test_claudeRPScriptContent() -> String {
            claudeRPScriptContent()
        }

        static func test_isManagedClaudeRPScript(_ content: String) -> Bool {
            isManagedClaudeRPScript(content)
        }

        static func test_atomicManagedSymlinkInstallCommand(
            installPath: String,
            desiredDestination: String,
            managedDestinations: [String]
        ) -> String {
            atomicManagedSymlinkInstallCommand(
                installPath: installPath,
                desiredDestination: desiredDestination,
                managedDestinations: managedDestinations
            )
        }

        static func test_managedSymlinkRemovalCommand(
            installPath: String,
            managedDestinations: [String]
        ) -> String {
            managedSymlinkRemovalCommand(
                installPath: installPath,
                managedDestinations: managedDestinations
            )
        }

        static func test_atomicManagedWrapperInstallCommand(sourcePath: String, installPath: String) -> String {
            atomicManagedWrapperInstallCommand(sourcePath: sourcePath, installPath: installPath)
        }
    #endif

    // MARK: - Installation Status

    enum InstallationStatus: Equatable {
        case notInstalled
        case installed
        case installedButStale // Points to wrong location
        case installedByOther // Not our symlink (file exists but not our symlink)
        case directoryMissing // /usr/local/bin doesn't exist
    }

    /// Check the current installation status using lstat/readlink ownership semantics.
    @MainActor
    static func checkStatus() -> InstallationStatus {
        let fileManager = FileManager.default
        var isDirectory: ObjCBool = false
        guard fileManager.fileExists(atPath: installDirectory, isDirectory: &isDirectory), isDirectory.boolValue else {
            return .directoryMissing
        }

        let desiredDestination = CLISymlinkManagerUserSpace.userSymlinkPath
        let classification = ManagedCLIPathPolicy.classifySymlink(
            at: installPath,
            desiredDestination: desiredDestination,
            managedDestinations: ManagedCLIPathPolicy.managedDestinations(
                currentBundledCLIPath: bundledCLIPath,
                fileManager: fileManager
            ),
            fileManager: fileManager
        )
        switch classification {
        case .missing:
            return .notInstalled
        case .managedCurrent:
            return fileManager.isExecutableFile(atPath: installPath) ? .installed : .installedButStale
        case .managedStale:
            return .installedButStale
        case .unmanaged:
            return .installedByOther
        }
    }

    /// Human-readable description of current status
    @MainActor
    static func statusDescription() -> String {
        switch checkStatus() {
        case .notInstalled:
            "Not installed"
        case .installed:
            "Installed at \(installPath)"
        case .installedButStale:
            "Installed but points to old location"
        case .installedByOther:
            "Path exists but not managed by RepoPrompt"
        case .directoryMissing:
            "\(installDirectory) does not exist"
        }
    }

    // MARK: - Installation

    enum InstallError: LocalizedError {
        case noBundledCLI
        case directoryMissing
        case permissionDenied
        case pathExistsNotOurs
        case scriptFailed(String)
        case unknown(Error)

        var errorDescription: String? {
            switch self {
            case .noBundledCLI:
                "Could not find the bundled CLI executable"
            case .directoryMissing:
                "\(installDirectory) does not exist. Create it first with: sudo mkdir -p \(installDirectory)"
            case .permissionDenied:
                "Permission denied. Administrator access is required."
            case .pathExistsNotOurs:
                "A file already exists at \(installPath) that wasn't created by RepoPrompt"
            case let .scriptFailed(message):
                "Installation failed: \(message)"
            case let .unknown(error):
                "Installation failed: \(error.localizedDescription)"
            }
        }
    }

    /// Install the CLI to PATH using AppleScript for admin privileges
    @MainActor
    static func install() async throws {
        guard let bundledPath = bundledCLIPath else {
            throw InstallError.noBundledCLI
        }

        let status = checkStatus()

        switch status {
        case .directoryMissing:
            throw InstallError.directoryMissing
        case .installedByOther:
            throw InstallError.pathExistsNotOurs
        case .installed:
            logger.info("CLI already installed at \(installPath)")
            return
        case .notInstalled, .installedButStale:
            break
        }

        let userLink = CLISymlinkManagerUserSpace.userSymlinkPath
        guard CLISymlinkManagerUserSpace.ensureLocalSymlink(
            userSymlinkURL: URL(fileURLWithPath: userLink),
            bundledCLIURL: URL(fileURLWithPath: bundledPath)
        ) else {
            throw InstallError.pathExistsNotOurs
        }

        let managedDestinations = ManagedCLIPathPolicy.managedDestinations(
            currentBundledCLIPath: bundledPath
        ).sorted()
        let shellCommand = atomicManagedSymlinkInstallCommand(
            installPath: installPath,
            desiredDestination: userLink,
            managedDestinations: managedDestinations
        )
        let script = """
        do shell script "\(appleScriptEscaped(shellCommand))" with administrator privileges
        """

        logger.info("Installing CLI: \(shellCommand)")

        do {
            let result = try await runAppleScript(script)
            if let output = result, !output.isEmpty {
                logger.info("Install output: \(output)")
            }
            logger.info("CLI installed successfully at \(installPath)")
        } catch {
            logger.error("Failed to install CLI: \(error.localizedDescription)")
            if (error as NSError).domain == "com.apple.osascript" {
                throw InstallError.permissionDenied
            }
            throw InstallError.unknown(error)
        }
    }

    /// Uninstall the CLI from PATH
    @MainActor
    static func uninstall() async throws {
        let status = checkStatus()

        switch status {
        case .notInstalled, .directoryMissing:
            logger.info("CLI not installed, nothing to uninstall")
            return
        case .installedByOther:
            throw InstallError.pathExistsNotOurs
        case .installed, .installedButStale:
            break
        }

        let shellCommand = managedSymlinkRemovalCommand(
            installPath: installPath,
            managedDestinations: ManagedCLIPathPolicy.managedDestinations(
                currentBundledCLIPath: bundledCLIPath
            ).sorted()
        )

        let script = """
        do shell script "\(appleScriptEscaped(shellCommand))" with administrator privileges
        """

        logger.info("Uninstalling CLI: \(shellCommand)")

        do {
            let result = try await runAppleScript(script)
            if let output = result, !output.isEmpty {
                logger.info("Uninstall output: \(output)")
            }
            logger.info("CLI uninstalled successfully")
        } catch {
            logger.error("Failed to uninstall CLI: \(error.localizedDescription)")
            if (error as NSError).domain == "com.apple.osascript" {
                throw InstallError.permissionDenied
            }
            throw InstallError.unknown(error)
        }
    }

    // MARK: - claude-rp Wrapper Installation

    /// Installation status specific to claude-rp wrapper
    enum ClaudeRPInstallationStatus: Equatable {
        case notInstalled
        case installed
        case installedButOutdated // Script exists but content differs
        case installedByOther // File exists but not our script
        case directoryMissing // /usr/local/bin doesn't exist
    }

    /// Check the claude-rp wrapper installation status
    @MainActor
    static func checkClaudeRPStatus() -> ClaudeRPInstallationStatus {
        let fm = FileManager.default
        let path = claudeRPInstallPath

        // Check if /usr/local/bin exists
        var isDir: ObjCBool = false
        guard fm.fileExists(atPath: installDirectory, isDirectory: &isDir), isDir.boolValue else {
            return .directoryMissing
        }

        switch ManagedCLIPathPolicy.classifyWrapper(
            at: path,
            expectedContent: claudeRPScriptContent(),
            fileManager: fm
        ) {
        case .missing:
            return .notInstalled
        case .managedCurrent:
            return .installed
        case .managedOutdated:
            return .installedButOutdated
        case .unmanaged:
            return .installedByOther
        }
    }

    /// Human-readable description of claude-rp status
    @MainActor
    static func claudeRPStatusDescription() -> String {
        switch checkClaudeRPStatus() {
        case .notInstalled:
            "Not installed"
        case .installed:
            "Installed at \(claudeRPInstallPath)"
        case .installedButOutdated:
            "Installed but needs update"
        case .installedByOther:
            "Path exists but not managed by RepoPrompt"
        case .directoryMissing:
            "\(installDirectory) does not exist"
        }
    }

    /// Install the claude-rp wrapper script using AppleScript for admin privileges
    @MainActor
    static func installClaudeRP() async throws {
        let status = checkClaudeRPStatus()

        switch status {
        case .directoryMissing:
            throw InstallError.directoryMissing
        case .installedByOther:
            throw InstallError.pathExistsNotOurs
        case .installed:
            // Wrapper is already installed, but still refresh the MCP config file
            logger.info("claude-rp already installed at \(claudeRPInstallPath)")
            try await MCPConfigExportService.shared.prepareStableWrapperConfigFile()
            logger.info("MCP config file refreshed at \(claudeRPConfigPath)")
            return
        case .notInstalled, .installedButOutdated:
            break
        }

        // Write the script content to a temp file, then move it with admin privileges
        let scriptContent = claudeRPScriptContent()
        let tempDir = FileManager.default.temporaryDirectory
        let tempFile = tempDir.appendingPathComponent("claude-rp-\(UUID().uuidString).sh")

        do {
            try scriptContent.write(to: tempFile, atomically: true, encoding: .utf8)
        } catch {
            throw InstallError.scriptFailed("Failed to write temporary script: \(error.localizedDescription)")
        }

        defer {
            try? FileManager.default.removeItem(at: tempFile)
        }

        let shellCommand = atomicManagedWrapperInstallCommand(
            sourcePath: tempFile.path,
            installPath: claudeRPInstallPath
        )

        let script = """
        do shell script "\(appleScriptEscaped(shellCommand))" with administrator privileges
        """

        logger.info("Installing claude-rp: \(shellCommand)")

        do {
            let result = try await runAppleScript(script)
            if let output = result, !output.isEmpty {
                logger.info("Install output: \(output)")
            }
            logger.info("claude-rp installed successfully at \(claudeRPInstallPath)")

            // Create the MCP config file that claude-rp references
            try await MCPConfigExportService.shared.prepareStableWrapperConfigFile()
            logger.info("MCP config file created at \(claudeRPConfigPath)")
        } catch {
            logger.error("Failed to install claude-rp: \(error.localizedDescription)")
            if (error as NSError).domain == "com.apple.osascript" {
                throw InstallError.permissionDenied
            }
            throw InstallError.unknown(error)
        }
    }

    /// Uninstall the claude-rp wrapper from PATH
    @MainActor
    static func uninstallClaudeRP() async throws {
        let status = checkClaudeRPStatus()

        switch status {
        case .notInstalled, .directoryMissing:
            logger.info("claude-rp not installed, nothing to uninstall")
            return
        case .installedByOther:
            throw InstallError.pathExistsNotOurs
        case .installed, .installedButOutdated:
            break
        }

        let shellCommand = managedWrapperRemovalCommand(installPath: claudeRPInstallPath)

        let script = """
        do shell script "\(appleScriptEscaped(shellCommand))" with administrator privileges
        """

        logger.info("Uninstalling claude-rp: \(shellCommand)")

        do {
            let result = try await runAppleScript(script)
            if let output = result, !output.isEmpty {
                logger.info("Uninstall output: \(output)")
            }
            logger.info("claude-rp uninstalled successfully")
        } catch {
            logger.error("Failed to uninstall claude-rp: \(error.localizedDescription)")
            if (error as NSError).domain == "com.apple.osascript" {
                throw InstallError.permissionDenied
            }
            throw InstallError.unknown(error)
        }
    }

    // MARK: - Helper

    private static func atomicManagedSymlinkInstallCommand(
        installPath: String,
        desiredDestination: String,
        managedDestinations: [String]
    ) -> String {
        let path = shellQuoted(installPath)
        let destination = shellQuoted(desiredDestination)
        let temporary = shellQuoted("\(installPath).\(UUID().uuidString).tmp")
        let backup = shellQuoted("\(installPath).\(UUID().uuidString).backup")
        let ownershipFunction = symlinkOwnershipFunction(managedDestinations: managedDestinations)
        return "set -e; \(ownershipFunction); path=\(path); tmp=\(temporary); backup=\(backup); trap 'rm -f \"$tmp\"' EXIT; managed_link \"$path\" || exit 73; ln -s \(destination) \"$tmp\"; if [ -e \"$path\" ] || [ -L \"$path\" ]; then mv \"$path\" \"$backup\"; managed_link \"$backup\" || { [ -e \"$path\" ] || [ -L \"$path\" ] || mv -n \"$backup\" \"$path\"; exit 73; }; fi; mv -n \"$tmp\" \"$path\"; if [ ! -L \"$path\" ] || [ \"$(readlink \"$path\")\" != \(destination) ]; then rm -f \"$backup\"; exit 73; fi; rm -f \"$backup\"; trap - EXIT"
    }

    private static func managedSymlinkRemovalCommand(
        installPath: String,
        managedDestinations: [String]
    ) -> String {
        let path = shellQuoted(installPath)
        let backup = shellQuoted("\(installPath).\(UUID().uuidString).removing")
        let ownershipFunction = symlinkOwnershipFunction(managedDestinations: managedDestinations)
        return "set -e; \(ownershipFunction); path=\(path); backup=\(backup); managed_link \"$path\" || exit 73; [ -e \"$path\" ] || [ -L \"$path\" ] || exit 0; mv \"$path\" \"$backup\"; managed_link \"$backup\" || { [ -e \"$path\" ] || [ -L \"$path\" ] || mv -n \"$backup\" \"$path\"; exit 73; }; rm -f \"$backup\""
    }

    private static func symlinkOwnershipFunction(managedDestinations: [String]) -> String {
        let comparisons = managedDestinations
            .map { "[ \"$current\" = \(shellQuoted($0)) ]" }
            .joined(separator: " || ")
        return "managed_link(){ candidate=\"$1\"; if [ -e \"$candidate\" ] || [ -L \"$candidate\" ]; then [ -L \"$candidate\" ] || return 1; current=$(readlink \"$candidate\"); { \(comparisons); } || return 1; fi; }"
    }

    private static func atomicManagedWrapperInstallCommand(sourcePath: String, installPath: String) -> String {
        let source = shellQuoted(sourcePath)
        let path = shellQuoted(installPath)
        let temporary = shellQuoted("\(installPath).\(UUID().uuidString).tmp")
        let backup = shellQuoted("\(installPath).\(UUID().uuidString).backup")
        let ownershipFunction = wrapperOwnershipFunction()
        return "set -e; \(ownershipFunction); source=\(source); path=\(path); tmp=\(temporary); backup=\(backup); trap 'rm -f \"$tmp\"' EXIT; managed_wrapper \"$path\" || exit 73; cp \"$source\" \"$tmp\"; chmod 755 \"$tmp\"; if [ -e \"$path\" ] || [ -L \"$path\" ]; then mv \"$path\" \"$backup\"; managed_wrapper \"$backup\" || { [ -e \"$path\" ] || [ -L \"$path\" ] || mv -n \"$backup\" \"$path\"; exit 73; }; fi; mv -n \"$tmp\" \"$path\"; if [ ! -f \"$path\" ] || [ -L \"$path\" ] || ! cmp -s \"$source\" \"$path\"; then rm -f \"$backup\"; exit 73; fi; rm -f \"$backup\"; trap - EXIT"
    }

    private static func managedWrapperRemovalCommand(installPath: String) -> String {
        let path = shellQuoted(installPath)
        let backup = shellQuoted("\(installPath).\(UUID().uuidString).removing")
        return "set -e; \(wrapperOwnershipFunction()); path=\(path); backup=\(backup); managed_wrapper \"$path\" || exit 73; [ -e \"$path\" ] || [ -L \"$path\" ] || exit 0; mv \"$path\" \"$backup\"; managed_wrapper \"$backup\" || { [ -e \"$path\" ] || [ -L \"$path\" ] || mv -n \"$backup\" \"$path\"; exit 73; }; rm -f \"$backup\""
    }

    private static func wrapperOwnershipFunction() -> String {
        let markers = ([ManagedCLIPathPolicy.currentClaudeWrapperMarker] + ManagedCLIPathPolicy.legacyClaudeWrapperMarkers)
            .map { "head -n 8 \"$candidate\" | grep -Fqx \(shellQuoted($0))" }
            .joined(separator: " || ")
        return "managed_wrapper(){ candidate=\"$1\"; if [ -e \"$candidate\" ] || [ -L \"$candidate\" ]; then [ -f \"$candidate\" ] && [ ! -L \"$candidate\" ] || return 1; { \(markers); } || return 1; fi; }"
    }

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

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

    private static func runAppleScript(_ script: String) async throws -> String? {
        try await withCheckedThrowingContinuation { continuation in
            DispatchQueue.global(qos: .userInitiated).async {
                var error: NSDictionary?
                let appleScript = NSAppleScript(source: script)
                let result = appleScript?.executeAndReturnError(&error)

                if let error {
                    let errorMessage = error[NSAppleScript.errorMessage] as? String ?? "Unknown AppleScript error"
                    let nsError = NSError(
                        domain: "com.apple.osascript",
                        code: error[NSAppleScript.errorNumber] as? Int ?? -1,
                        userInfo: [NSLocalizedDescriptionKey: errorMessage]
                    )
                    continuation.resume(throwing: nsError)
                } else {
                    continuation.resume(returning: result?.stringValue)
                }
            }
        }
    }
}
