import Foundation

@MainActor
enum CLIInstaller {
    enum LocalGatewayActivation: Equatable {
        case ready
        case deferred
        case failed
    }

    enum Status: Equatable {
        case ready(location: String, version: String)
        case missing(location: String)
        case unusable(location: String)
        case incompatible(location: String, found: String, required: String)

        var isReady: Bool {
            if case .ready = self { return true }
            return false
        }

        var location: String {
            switch self {
            case let .ready(location, _),
                 let .missing(location),
                 let .unusable(location),
                 let .incompatible(location, _, _):
                location
            }
        }

        var message: String {
            switch self {
            case let .ready(_, version):
                "OpenClaw Gateway \(version) is ready."
            case .missing:
                "OpenClaw Gateway is not installed yet."
            case .unusable:
                "The OpenClaw Gateway could not be verified. Setup will repair it."
            case let .incompatible(_, found, required):
                "Gateway \(found) does not match app \(required). Setup will update it."
            }
        }
    }

    static func installedLocation() -> String? {
        self.installedLocations(
            searchPaths: CommandResolver.preferredPaths(),
            fileManager: .default).first
    }

    static func installedLocation(
        searchPaths: [String],
        fileManager: FileManager) -> String?
    {
        self.installedLocations(searchPaths: searchPaths, fileManager: fileManager).first
    }

    static func installedLocations(
        searchPaths: [String],
        fileManager: FileManager) -> [String]
    {
        var locations: [String] = []
        for basePath in searchPaths {
            let candidate = URL(fileURLWithPath: basePath).appendingPathComponent("openclaw").path
            var isDirectory: ObjCBool = false

            guard fileManager.fileExists(atPath: candidate, isDirectory: &isDirectory),
                  !isDirectory.boolValue
            else {
                continue
            }

            guard fileManager.isExecutableFile(atPath: candidate) else { continue }

            locations.append(candidate)
        }
        return locations
    }

    static func isInstalled() -> Bool {
        self.installedLocation() != nil
    }

    static func managedExecutableLocation() -> String {
        URL(fileURLWithPath: self.installPrefix())
            .appendingPathComponent("bin/openclaw")
            .path
    }

    static func status() async -> Status {
        let locations = self.installedLocations(
            searchPaths: CommandResolver.preferredPaths(),
            fileManager: .default)
        guard !locations.isEmpty else {
            return .missing(location: self.managedExecutableLocation())
        }

        var fallbackStatus: Status?
        for location in locations {
            let status = await self.status(location: location)
            if status.isReady {
                self.rememberValidated(status)
                return status
            }
            fallbackStatus = fallbackStatus ?? status
        }
        return fallbackStatus ?? .missing(location: self.managedExecutableLocation())
    }

    static func managedStatus() async -> Status {
        let location = self.managedExecutableLocation()
        guard FileManager.default.isExecutableFile(atPath: location) else {
            return .missing(location: location)
        }

        let status = await self.status(location: location)
        if status.isReady {
            self.rememberValidated(status)
        }
        return status
    }

    static func status(location: String) async -> Status {
        let environment = self.probeEnvironment(location: location)
        let response = await ShellExecutor.runDetailed(
            command: [location, "--version"],
            cwd: nil,
            env: environment,
            timeout: 5)
        guard response.success else {
            return .unusable(location: location)
        }
        let versionStatus = self.classifyVersion(
            location: location,
            output: response.stdout,
            expectedVersion: GatewayEnvironment.expectedGatewayVersionString())
        guard versionStatus.isReady else { return versionStatus }
        guard await self.runtimeIsCompatible(environment: environment) else {
            return .unusable(location: location)
        }
        return versionStatus
    }

    private static func runtimeIsCompatible(environment: [String: String]) async -> Bool {
        let paths = environment["PATH"]?.split(separator: ":").map(String.init) ?? []
        return await Task.detached(priority: .utility) {
            if case .success = RuntimeLocator.resolve(searchPaths: paths) {
                return true
            }
            return false
        }.value
    }

    static func classifyVersion(
        location: String,
        output: String?,
        expectedVersion: String?) -> Status
    {
        let normalized = GatewayEnvironment.normalizeGatewayVersionOutput(output)
        guard let normalized, let installed = Semver.parse(normalized) else {
            return .unusable(location: location)
        }
        guard let required = Semver.parse(expectedVersion) else {
            return .ready(location: location, version: normalized)
        }
        guard installed.compatible(with: required) else {
            return .incompatible(
                location: location,
                found: normalized,
                required: expectedVersion ?? required.description)
        }
        return .ready(location: location, version: normalized)
    }

    static func probeEnvironment(
        location: String,
        processEnvironment: [String: String] = ProcessInfo.processInfo.environment,
        preferredPaths: [String] = CommandResolver.preferredPaths(),
        managedExecutable: String? = nil,
        managedRuntimeDirectory: String? = nil) -> [String: String]
    {
        var environment = processEnvironment
        let executableDirectory = URL(fileURLWithPath: location).deletingLastPathComponent().path
        let effectiveManagedExecutable = managedExecutable ?? self.managedExecutableLocation()
        let effectiveManagedRuntimeDirectory = managedRuntimeDirectory ?? URL(fileURLWithPath: self.installPrefix())
            .appendingPathComponent("tools/node/bin")
            .path
        let initialPaths = location == effectiveManagedExecutable
            ? [executableDirectory, effectiveManagedRuntimeDirectory]
            : [executableDirectory]
        var seen = Set<String>()
        let paths = (initialPaths + preferredPaths).filter { seen.insert($0).inserted }
        environment["PATH"] = paths.joined(separator: ":")
        return environment
    }

    private static func rememberValidated(_ status: Status) {
        guard case let .ready(location, version) = status else { return }
        UserDefaults.standard.set(location, forKey: cliValidatedExecutableKey)
        UserDefaults.standard.set(version, forKey: cliValidatedVersionKey)
    }

    @discardableResult
    static func install(statusHandler: @escaping @MainActor @Sendable (String) async -> Void) async -> Bool {
        let expected = GatewayEnvironment.expectedGatewayVersionString() ?? "latest"
        let prefix = Self.installPrefix()
        await statusHandler("Installing openclaw CLI…")
        guard let installerURL = Bundle.main.url(forResource: "install-cli", withExtension: "sh") else {
            await statusHandler("Install failed: installer resource is missing. Reinstall OpenClaw.")
            return false
        }
        let cmd = self.installScriptCommand(
            version: expected,
            prefix: prefix,
            scriptPath: installerURL.path)
        let response = await ShellExecutor.runDetailed(command: cmd, cwd: nil, env: nil, timeout: 900)

        if response.success {
            let managedStatus = await self.managedStatus()
            guard managedStatus.isReady else {
                await statusHandler("Install failed: \(managedStatus.message)")
                return false
            }
            let parsed = self.parseInstallEvents(response.stdout)
            let installedVersion = parsed.last { $0.event == "done" }?.version
            let summary = installedVersion.map { "Installed openclaw \($0)." } ?? "Installed openclaw."
            await statusHandler(summary)
            return true
        }

        let parsed = self.parseInstallEvents(response.stdout)
        if let error = parsed.last(where: { $0.event == "error" })?.message {
            await statusHandler("Install failed: \(error)")
            return false
        }

        let detail = response.stderr.trimmingCharacters(in: .whitespacesAndNewlines)
        let fallback = response.errorMessage ?? "install failed"
        await statusHandler("Install failed: \(detail.isEmpty ? fallback : detail)")
        return false
    }

    private static func installPrefix() -> String {
        FileManager().homeDirectoryForCurrentUser
            .appendingPathComponent(".openclaw")
            .path
    }

    static func installScriptCommand(version: String, prefix: String, scriptPath: String) -> [String] {
        [
            "/bin/bash",
            scriptPath,
            "--json",
            "--no-onboard",
            "--prefix",
            prefix,
            "--version",
            version,
        ]
    }

    static func activateLocalGateway(
        mode: AppState.ConnectionMode = AppStateStore.shared.connectionMode,
        paused: Bool = AppStateStore.shared.isPaused,
        start: @MainActor () -> Void = { GatewayProcessManager.shared.setActive(true) },
        waitUntilReady: @MainActor () async -> Bool = {
            await GatewayProcessManager.shared.waitForGatewayReady(timeout: 12)
        }) async -> LocalGatewayActivation
    {
        guard mode == .local, !paused else { return .deferred }
        start()
        return await waitUntilReady() ? .ready : .failed
    }

    private static func parseInstallEvents(_ output: String) -> [InstallEvent] {
        let decoder = JSONDecoder()
        let lines = output
            .split(whereSeparator: \.isNewline)
            .map { String($0) }
        var events: [InstallEvent] = []
        for line in lines {
            guard let data = line.data(using: .utf8) else { continue }
            if let event = try? decoder.decode(InstallEvent.self, from: data) {
                events.append(event)
            }
        }
        return events
    }
}

private struct InstallEvent: Decodable {
    let event: String
    let version: String?
    let message: String?
}
