import CryptoKit
import Foundation
import OSLog
import Security

enum ExecApprovalsStore {
    private static let logger = Logger(subsystem: "ai.openclaw", category: "exec-approvals")
    private static let defaultAgentId = "main"
    // Keep omitted-file behavior aligned with the TypeScript gateway/CLI contract.
    private static let defaultSecurity: ExecSecurity = .full
    private static let defaultAsk: ExecAsk = .off
    private static let defaultAskFallback: ExecSecurity = .deny
    private static let defaultAutoAllowSkills = false
    private static let secureStateDirPermissions = 0o700

    /// Match the TypeScript writer's `<approvals>.lock` protocol. Both processes
    /// must cover the complete read-modify-write transaction or a stale native
    /// usage update can restore policy that an administrator just revoked.
    private static func withWriteLock<T>(_ body: () throws -> T) throws -> T {
        let fileURL = self.fileURL()
        let trustedRoot = self.trustedRootURL()
        try ExecApprovalsFileIO.assertSafeParentChain(of: fileURL, trustedRoot: trustedRoot)
        try self.ensureSecureStateDirectory()
        return try ExecApprovalsFileIO.withLock(
            fileURL: fileURL,
            trustedRoot: trustedRoot,
            body)
    }

    static func fileURL() -> URL {
        self.stateDirURL().appendingPathComponent("exec-approvals.json")
    }

    static func socketPath() -> String {
        self.stateDirURL().appendingPathComponent("exec-approvals.sock").path
    }

    private static func trustedRootURL() -> URL {
        guard let configured = OpenClawEnv.path("OPENCLAW_HOME") else {
            return FileManager().homeDirectoryForCurrentUser
        }
        return URL(
            fileURLWithPath: (configured as NSString).expandingTildeInPath,
            isDirectory: true)
    }

    private static func stateDirURL() -> URL {
        guard let configured = OpenClawEnv.path("OPENCLAW_STATE_DIR") else {
            return self.trustedRootURL().appendingPathComponent(".openclaw", isDirectory: true)
        }
        let home = self.trustedRootURL().path
        let expanded: String = if configured == "~" {
            home
        } else if configured.hasPrefix("~/") {
            URL(fileURLWithPath: home, isDirectory: true)
                .appendingPathComponent(String(configured.dropFirst(2)), isDirectory: true)
                .path
        } else {
            configured
        }
        return URL(fileURLWithPath: expanded, isDirectory: true).standardizedFileURL
    }

    private static func failClosedFallbackFile() -> ExecApprovalsFile {
        ExecApprovalsFile(
            version: 1,
            socket: nil,
            defaults: ExecApprovalsDefaults(
                security: .deny,
                ask: .off,
                askFallback: .deny,
                autoAllowSkills: false),
            agents: [:])
    }

    static func normalizeIncoming(_ file: ExecApprovalsFile) -> ExecApprovalsFile {
        let socketPath = file.socket?.path?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
        let token = file.socket?.token?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
        var agents = file.agents ?? [:]
        if let legacyDefault = agents["default"] {
            if let main = agents[defaultAgentId] {
                agents[self.defaultAgentId] = self.mergeAgents(current: main, legacy: legacyDefault)
            } else {
                agents[self.defaultAgentId] = legacyDefault
            }
            agents.removeValue(forKey: "default")
        }
        if !agents.isEmpty {
            var normalizedAgents: [String: ExecApprovalsAgent] = [:]
            normalizedAgents.reserveCapacity(agents.count)
            for (key, var agent) in agents {
                if let allowlist = agent.allowlist {
                    let normalized = self.normalizeAllowlistEntries(allowlist, dropInvalid: false).entries
                    agent.allowlist = normalized.isEmpty ? nil : normalized
                }
                normalizedAgents[key] = agent
            }
            agents = normalizedAgents
        }
        return ExecApprovalsFile(
            version: 1,
            socket: ExecApprovalsSocketConfig(
                path: socketPath.isEmpty ? nil : socketPath,
                token: token.isEmpty ? nil : token),
            defaults: file.defaults,
            agents: agents.isEmpty ? nil : agents)
    }

    static func readSnapshot() -> ExecApprovalsSnapshot {
        do {
            return try self.withWriteLock {
                try self.readSnapshotUnlocked()
            }
        } catch {
            self.logger.warning("exec approvals snapshot lock failed: \(error.localizedDescription, privacy: .public)")
            return ExecApprovalsSnapshot(
                path: self.fileURL().path,
                exists: ExecApprovalsFileIO.pathExistsNoFollow(self.fileURL()),
                hash: "",
                file: self.failClosedFallbackFile())
        }
    }

    private static func readSnapshotUnlocked() throws -> ExecApprovalsSnapshot {
        let url = self.fileURL()
        guard let current = try ExecApprovalsFileIO.read(at: url, trustedRoot: self.trustedRootURL()) else {
            return ExecApprovalsSnapshot(
                path: url.path,
                exists: false,
                hash: self.hashRaw(nil),
                file: ExecApprovalsFile(version: 1, socket: nil, defaults: nil, agents: [:]))
        }
        let raw = String(bytes: current.data, encoding: .utf8) ?? ""
        let decoded = (try? self.decodeCurrentFile(current.data))
            .map(self.normalizeIncoming) ?? self.failClosedFallbackFile()
        return ExecApprovalsSnapshot(
            path: url.path,
            exists: true,
            hash: self.hashRaw(raw),
            file: decoded)
    }

    static func loadFile() -> ExecApprovalsFile {
        do {
            return try self.withWriteLock {
                self.loadFileUnlocked()
            }
        } catch {
            self.logger.warning("exec approvals read lock failed: \(error.localizedDescription, privacy: .public)")
            return self.failClosedFallbackFile()
        }
    }

    private static func loadFileUnlocked() -> ExecApprovalsFile {
        do {
            return try self.loadFileForMutationUnlocked()
        } catch {
            self.logger.warning("exec approvals load failed: \(error.localizedDescription, privacy: .public)")
            return self.failClosedFallbackFile()
        }
    }

    /// Existing unreadable files are policy state, not equivalent to an absent
    /// file. Mutations must fail instead of replacing them with permissive defaults.
    private static func loadFileForMutationUnlocked() throws -> ExecApprovalsFile {
        let url = self.fileURL()
        guard let current = try ExecApprovalsFileIO.read(at: url, trustedRoot: self.trustedRootURL()) else {
            return ExecApprovalsFile(version: 1, socket: nil, defaults: nil, agents: [:])
        }
        return try self.normalizeIncoming(self.decodeCurrentFile(current.data))
    }

    private static func decodeCurrentFile(_ data: Data) throws -> ExecApprovalsFile {
        guard self.hasValidPersistedStructure(data) else {
            throw NSError(domain: "ExecApprovals", code: 13, userInfo: [
                NSLocalizedDescriptionKey: "invalid exec approvals structure",
            ])
        }
        let decoded = try JSONDecoder().decode(ExecApprovalsFile.self, from: data)
        guard decoded.version == 1 else {
            throw NSError(domain: "ExecApprovals", code: 12, userInfo: [
                NSLocalizedDescriptionKey: "unsupported exec approvals version \(decoded.version)",
            ])
        }
        return decoded
    }

    private static func fileNeedsAllowlistRewrite(_ data: Data) -> Bool {
        guard let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
              let agents = root["agents"] as? [String: Any]
        else { return false }
        for case let agent as [String: Any] in agents.values {
            guard let allowlist = agent["allowlist"] as? [Any] else { continue }
            if allowlist.contains(where: { value in
                guard let entry = value as? [String: Any] else { return true }
                guard let rawID = entry["id"] as? String else { return true }
                return rawID.isEmpty || entry["commandText"] != nil
            }) {
                return true
            }
        }
        return false
    }

    private static func hasValidPersistedStructure(_ data: Data) -> Bool {
        guard let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
              let version = root["version"] as? NSNumber,
              CFGetTypeID(version) != CFBooleanGetTypeID(),
              version.doubleValue == 1
        else { return false }

        if let socket = root["socket"] {
            guard let object = socket as? [String: Any],
                  self.hasOptionalString(object, key: "path"),
                  self.hasOptionalString(object, key: "token")
            else { return false }
        }
        if let defaults = root["defaults"], !self.hasValidPolicyFields(defaults) {
            return false
        }
        if let agents = root["agents"] {
            guard let object = agents as? [String: Any] else { return false }
            for value in object.values {
                guard self.hasValidPolicyFields(value), let agent = value as? [String: Any] else { return false }
                if let allowlist = agent["allowlist"] {
                    guard let entries = allowlist as? [Any],
                          entries.allSatisfy(self.hasValidAllowlistEntry)
                    else { return false }
                }
            }
        }
        return true
    }

    private static func hasValidAllowlistEntry(_ value: Any) -> Bool {
        if let pattern = value as? String {
            return !pattern.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
        }
        guard let object = value as? [String: Any],
              let pattern = object["pattern"] as? String,
              !pattern.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
        else { return false }
        for key in ["id", "source", "commandText", "argPattern", "lastUsedCommand", "lastResolvedPath"] {
            if let value = object[key], !(value is String) {
                return false
            }
        }
        if let lastUsedAt = object["lastUsedAt"] {
            guard let number = lastUsedAt as? NSNumber,
                  CFGetTypeID(number) != CFBooleanGetTypeID(),
                  number.doubleValue.isFinite
            else { return false }
        }
        return true
    }

    private static func hasValidPolicyFields(_ value: Any) -> Bool {
        guard let object = value as? [String: Any] else { return false }
        if let security = object["security"] {
            guard let raw = security as? String, ExecSecurity(rawValue: raw) != nil else { return false }
        }
        if let ask = object["ask"] {
            guard let raw = ask as? String, ExecAsk(rawValue: raw) != nil else { return false }
        }
        if let fallback = object["askFallback"] {
            guard let raw = fallback as? String, ExecSecurity(rawValue: raw) != nil else { return false }
        }
        if let autoAllowSkills = object["autoAllowSkills"], !(autoAllowSkills is Bool) {
            return false
        }
        return true
    }

    private static func hasOptionalString(_ object: [String: Any], key: String) -> Bool {
        guard let value = object[key] else { return true }
        return value is String
    }

    private static func saveFileUnlocked(_ file: ExecApprovalsFile) throws {
        let encoder = JSONEncoder()
        encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
        let data = try encoder.encode(self.normalizeIncoming(file))
        let url = self.fileURL()
        try self.ensureSecureStateDirectory()
        try FileManager().createDirectory(
            at: url.deletingLastPathComponent(),
            withIntermediateDirectories: true)
        try ExecApprovalsFileIO.write(data, to: url, trustedRoot: self.trustedRootURL())
    }

    static func ensureFile() -> ExecApprovalsFile {
        do {
            return try self.withWriteLock {
                try self.ensureFileUnlocked()
            }
        } catch {
            self.logger.error("exec approvals ensure failed: \(error.localizedDescription, privacy: .public)")
            return self.failClosedFallbackFile()
        }
    }

    private static func ensureFileUnlocked() throws -> ExecApprovalsFile {
        try self.ensureSecureStateDirectory()
        let url = self.fileURL()
        let current = try ExecApprovalsFileIO.read(at: url, trustedRoot: self.trustedRootURL())
        let existed = current != nil
        let needsAllowlistRewrite = current.map { self.fileNeedsAllowlistRewrite($0.data) } ?? false
        let loaded = try current.map { try self.decodeCurrentFile($0.data) }
            ?? ExecApprovalsFile(version: 1, socket: nil, defaults: nil, agents: [:])
        let loadedHash = self.hashFile(loaded)

        var file = self.normalizeIncoming(loaded)
        if file.socket == nil {
            file.socket = ExecApprovalsSocketConfig(path: nil, token: nil)
        }
        let path = file.socket?.path?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
        if path.isEmpty {
            file.socket?.path = self.socketPath()
        }
        let token = file.socket?.token?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
        if token.isEmpty {
            file.socket?.token = self.generateToken()
        }
        if file.agents == nil {
            file.agents = [:]
        }
        if !existed || needsAllowlistRewrite || current?.linkCount != 1 || loadedHash != self.hashFile(file) {
            try self.saveFileUnlocked(file)
        }
        return file
    }

    static func saveFile(
        _ incoming: ExecApprovalsFile,
        ifBaseHash baseHash: String?) -> ExecApprovalsConditionalSaveResult
    {
        do {
            return try self.withWriteLock {
                // A conditional write must not create or normalize policy state
                // before it proves the caller still owns the observed snapshot.
                let snapshot = try self.readSnapshotUnlocked()
                let expected = baseHash?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
                if snapshot.exists {
                    if snapshot.hash.isEmpty {
                        return .baseHashUnavailable
                    }
                    if expected.isEmpty {
                        return .baseHashRequired
                    }
                    if expected != snapshot.hash {
                        return .conflict
                    }
                } else if !expected.isEmpty, expected != snapshot.hash {
                    return .conflict
                }

                let current = try self.ensureFileUnlocked()
                var normalized = self.normalizeIncoming(incoming)
                let socketPath = normalized.socket?.path?.trimmingCharacters(in: .whitespacesAndNewlines)
                let token = normalized.socket?.token?.trimmingCharacters(in: .whitespacesAndNewlines)
                let resolvedPath = (socketPath?.isEmpty == false)
                    ? socketPath!
                    : current.socket?.path?.trimmingCharacters(in: .whitespacesAndNewlines) ??
                    self.socketPath()
                let resolvedToken = (token?.isEmpty == false)
                    ? token!
                    : current.socket?.token?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
                normalized.socket = ExecApprovalsSocketConfig(path: resolvedPath, token: resolvedToken)

                try self.saveFileUnlocked(normalized)
                return try .saved(self.readSnapshotUnlocked())
            }
        } catch {
            self.logger.error("exec approvals conditional save failed: \(error.localizedDescription, privacy: .public)")
            return .unavailable
        }
    }

    static func resolve(agentId: String?) -> ExecApprovalsResolved {
        switch self.resolveResult(agentId: agentId) {
        case let .success(resolved):
            resolved
        case .failure:
            self.resolveFromFile(self.failClosedFallbackFile(), agentId: agentId)
        }
    }

    static func resolveResult(
        agentId: String?) -> Result<ExecApprovalsResolved, ExecApprovalsReadError>
    {
        do {
            let file = try self.withWriteLock {
                try self.ensureFileUnlocked()
            }
            return .success(self.resolveFromFile(file, agentId: agentId))
        } catch {
            self.logger.warning("exec approvals resolve failed: \(error.localizedDescription, privacy: .public)")
            return .failure(.unavailable)
        }
    }

    static func resolveAsyncResult(
        agentId: String?) async -> Result<ExecApprovalsResolved, ExecApprovalsReadError>
    {
        await Task.detached(priority: .userInitiated) {
            self.resolveResult(agentId: agentId)
        }.value
    }

    /// Read-only resolve: loads file without writing (no ensureFile side effects).
    /// Safe to call from background threads / off MainActor.
    static func resolveReadOnly(agentId: String?) -> ExecApprovalsResolved {
        let file: ExecApprovalsFile
        do {
            file = try self.withWriteLock {
                self.loadFileUnlocked()
            }
        } catch {
            self.logger.warning("exec approvals read-only lock failed: \(error.localizedDescription, privacy: .public)")
            file = self.failClosedFallbackFile()
        }
        return self.resolveFromFile(file, agentId: agentId)
    }

    static func resolveDefaults(from file: ExecApprovalsFile) -> ExecApprovalsResolvedDefaults {
        let defaults = file.defaults ?? ExecApprovalsDefaults()
        return ExecApprovalsResolvedDefaults(
            security: defaults.security ?? self.defaultSecurity,
            ask: defaults.ask ?? self.defaultAsk,
            askFallback: defaults.askFallback ?? self.defaultAskFallback,
            autoAllowSkills: defaults.autoAllowSkills ?? self.defaultAutoAllowSkills)
    }

    private static func resolveFromFile(_ file: ExecApprovalsFile, agentId: String?) -> ExecApprovalsResolved {
        let resolvedDefaults = self.resolveDefaults(from: file)
        let key = self.agentKey(agentId)
        let agentEntry = file.agents?[key] ?? ExecApprovalsAgent()
        let wildcardEntry = file.agents?["*"] ?? ExecApprovalsAgent()
        let resolvedAgent = ExecApprovalsResolvedDefaults(
            security: agentEntry.security ?? wildcardEntry.security ?? resolvedDefaults.security,
            ask: agentEntry.ask ?? wildcardEntry.ask ?? resolvedDefaults.ask,
            askFallback: agentEntry.askFallback ?? wildcardEntry.askFallback
                ?? resolvedDefaults.askFallback,
            autoAllowSkills: agentEntry.autoAllowSkills ?? wildcardEntry.autoAllowSkills
                ?? resolvedDefaults.autoAllowSkills)
        let allowlist = self.normalizeAllowlistEntries(
            (wildcardEntry.allowlist ?? []) + (agentEntry.allowlist ?? []),
            dropInvalid: true).entries
        let socketPath = self.expandPath(file.socket?.path ?? self.socketPath())
        let token = file.socket?.token ?? ""
        return ExecApprovalsResolved(
            url: self.fileURL(),
            socketPath: socketPath,
            token: token,
            defaults: resolvedDefaults,
            agent: resolvedAgent,
            allowlist: allowlist,
            file: file)
    }

    static func resolveDefaultsAsyncResult() async
        -> Result<ExecApprovalsResolvedDefaults, ExecApprovalsReadError>
    {
        await self.resolveAsyncResult(agentId: nil).map(\.defaults)
    }
}

extension ExecApprovalsStore {
    @discardableResult
    static func updateDefaults(
        _ mutate: (inout ExecApprovalsDefaults) -> Void) -> Result<Void, ExecApprovalsMutationError>
    {
        self.updateFile { file in
            var defaults = file.defaults ?? ExecApprovalsDefaults()
            mutate(&defaults)
            file.defaults = defaults
        }
    }

    @discardableResult
    static func addAllowlistEntry(
        agentId: String?,
        pattern: String,
        source: String? = nil,
        commandText: String? = nil,
        argPattern: String? = nil) -> Result<Void, ExecApprovalsMutationError>
    {
        self.addAllowlistEntries(
            agentId: agentId,
            entries: [ExecAllowlistEntry(
                pattern: pattern,
                source: source,
                commandText: commandText,
                argPattern: argPattern)])
    }

    @discardableResult
    static func addAllowlistEntries(
        agentId: String?,
        entries: [ExecAllowlistEntry]) -> Result<Void, ExecApprovalsMutationError>
    {
        var normalizedEntries: [ExecAllowlistEntry] = []
        normalizedEntries.reserveCapacity(entries.count)
        for var item in entries {
            switch ExecApprovalHelpers.validateAllowlistPattern(item.pattern) {
            case let .valid(pattern):
                item.pattern = pattern
            case let .invalid(reason):
                return .failure(.invalidPattern(reason))
            }
            item.commandText = nil
            item.argPattern = self.normalizeArgPattern(item.argPattern)
            normalizedEntries.append(item)
        }

        return self.updateFile { file in
            let key = self.agentKey(agentId)
            var agents = file.agents ?? [:]
            var entry = agents[key] ?? ExecApprovalsAgent()
            var allowlist = entry.allowlist ?? []
            let now = Date().timeIntervalSince1970 * 1000
            for incoming in normalizedEntries {
                if let index = allowlist.firstIndex(where: {
                    self.allowlistEntryMatchKey($0) == self.allowlistEntryMatchKey(incoming)
                }) {
                    if let source = incoming.source {
                        allowlist[index].source = source
                    }
                    allowlist[index].lastUsedAt = now
                    continue
                }
                allowlist.append(ExecAllowlistEntry(
                    pattern: incoming.pattern,
                    source: incoming.source,
                    argPattern: incoming.argPattern,
                    lastUsedAt: now))
            }
            entry.allowlist = allowlist
            agents[key] = entry
            file.agents = agents
        }
    }

    @discardableResult
    static func commitExecution(
        _ commit: ExecApprovalExecutionCommit) -> Result<Void, ExecApprovalsMutationError>
    {
        let grants: [ExecAllowlistUse] = switch commit.authorization {
        case let .explicitAlways(_, _, grants):
            grants
        case .currentPolicy, .askFallback, .autoReview, .explicitOnce:
            []
        }
        let normalizedGrants: [ExecAllowlistUse]
        switch self.normalizeExecutionGrants(grants) {
        case let .success(normalized):
            normalizedGrants = normalized
        case let .failure(error):
            return .failure(error)
        }
        let authorizationUsesByKey = Dictionary(
            commit.uses.map { (self.allowlistEntryMatchKey($0.match), $0) },
            uniquingKeysWith: { first, _ in first })
        let allUsesByKey = Dictionary(
            (commit.uses + normalizedGrants).map { (self.allowlistEntryMatchKey($0.match), $0) },
            uniquingKeysWith: { first, _ in first })

        do {
            try self.withWriteLock {
                var file = try self.ensureFileUnlocked()
                try self.assertCurrentExecutionAuthorization(
                    file: file,
                    agentId: commit.agentId,
                    usesByKey: authorizationUsesByKey,
                    authorization: commit.authorization)
                let grantsChanged = self.applyAllowlistGrantsUnlocked(
                    file: &file,
                    agentId: commit.agentId,
                    grants: normalizedGrants)
                let usesChanged = self.applyAllowlistUsesUnlocked(
                    file: &file,
                    agentId: commit.agentId,
                    usesByKey: allUsesByKey,
                    command: commit.command)
                if grantsChanged || usesChanged {
                    try self.saveFileUnlocked(file)
                }
            }
            return .success(())
        } catch {
            self.logger.error("exec approval execution commit failed: \(error.localizedDescription, privacy: .public)")
            return .failure(.unavailable)
        }
    }

    @discardableResult
    static func recordAllowlistUses(
        agentId: String?,
        uses: [ExecAllowlistUse],
        command: String,
        authorization: ExecApprovalAuthorization? = nil) -> Result<Void, ExecApprovalsMutationError>
    {
        if let authorization {
            return self.commitExecution(ExecApprovalExecutionCommit(
                agentId: agentId,
                command: command,
                authorization: authorization,
                uses: uses))
        }
        guard !uses.isEmpty else { return .success(()) }
        let usesByKey = Dictionary(
            uses.map { (self.allowlistEntryMatchKey($0.match), $0) },
            uniquingKeysWith: { first, _ in first })
        do {
            try self.withWriteLock {
                var file = try self.ensureFileUnlocked()
                if self.applyAllowlistUsesUnlocked(
                    file: &file,
                    agentId: agentId,
                    usesByKey: usesByKey,
                    command: command)
                {
                    try self.saveFileUnlocked(file)
                }
            }
            return .success(())
        } catch {
            self.logger.error("exec approvals usage update failed: \(error.localizedDescription, privacy: .public)")
            return .failure(.unavailable)
        }
    }

    private static func normalizeExecutionGrants(
        _ grants: [ExecAllowlistUse]) -> Result<[ExecAllowlistUse], ExecApprovalsMutationError>
    {
        var normalized: [ExecAllowlistUse] = []
        normalized.reserveCapacity(grants.count)
        for grant in grants {
            switch ExecApprovalHelpers.validateAllowlistPattern(grant.match.pattern) {
            case let .valid(pattern):
                normalized.append(ExecAllowlistUse(
                    match: ExecAllowlistEntry(
                        id: grant.match.id,
                        pattern: pattern,
                        source: "allow-always",
                        argPattern: self.normalizeArgPattern(grant.match.argPattern)),
                    resolvedPath: grant.resolvedPath))
            case let .invalid(reason):
                return .failure(.invalidPattern(reason))
            }
        }
        return .success(normalized)
    }

    private static func assertCurrentExecutionAuthorization(
        file: ExecApprovalsFile,
        agentId: String?,
        usesByKey: [ExecAllowlistEntryMatchKey: ExecAllowlistUse],
        authorization: ExecApprovalAuthorization) throws
    {
        let current = self.resolveFromFile(file, agentId: agentId)
        let evaluatedSecurity: ExecSecurity
        let evaluatedAsk: ExecAsk?
        let basis: ExecApprovalAuthorization.Basis?
        let appliesFallback: Bool
        switch authorization {
        case let .autoReview(security, policySnapshot):
            guard ExecSecurity.narrower(security, current.agent.security) != .deny,
                  current.agent.ask != .always,
                  policySnapshot.isCurrent(ExecApprovalPolicySnapshot(resolved: current))
            else {
                throw self.executionAuthorizationChangedError()
            }
            return
        case let .explicitOnce(security, policySnapshot):
            guard ExecSecurity.narrower(security, current.agent.security) != .deny,
                  policySnapshot.isCurrent(ExecApprovalPolicySnapshot(resolved: current))
            else {
                throw self.executionAuthorizationChangedError()
            }
            return
        case let .explicitAlways(security, policySnapshot, _):
            guard ExecSecurity.narrower(security, current.agent.security) != .deny,
                  policySnapshot.isCurrent(ExecApprovalPolicySnapshot(resolved: current))
            else {
                throw self.executionAuthorizationChangedError()
            }
            return
        case let .currentPolicy(security, ask, authorizationBasis):
            evaluatedSecurity = security
            evaluatedAsk = ask
            basis = authorizationBasis
            appliesFallback = false
        case let .askFallback(security, authorizationBasis):
            evaluatedSecurity = security
            evaluatedAsk = nil
            basis = authorizationBasis
            appliesFallback = true
        }

        let currentKeys = Set(current.allowlist.map(self.allowlistEntryMatchKey))
        let currentSecurity = ExecSecurity.narrower(evaluatedSecurity, current.agent.security)
        let authorizationSecurity = appliesFallback
            ? ExecSecurity.narrower(currentSecurity, current.agent.askFallback)
            : currentSecurity
        let currentAskAllowsExecution = evaluatedAsk.map {
            ExecAsk.stricter($0, current.agent.ask) == $0
        } ?? true
        let basisIsCurrent: Bool = switch basis {
        case .allowlistEntries:
            !usesByKey.isEmpty && usesByKey.keys.allSatisfy { currentKeys.contains($0) }
        case .autoAllowedSkill:
            current.agent.autoAllowSkills
        case nil:
            false
        }
        let authorizationIsCurrent: Bool = switch authorizationSecurity {
        case .deny:
            false
        case .full:
            currentAskAllowsExecution && authorizationSecurity == evaluatedSecurity
        case .allowlist:
            currentAskAllowsExecution &&
                authorizationSecurity == evaluatedSecurity &&
                basisIsCurrent
        }
        guard authorizationIsCurrent else {
            throw self.executionAuthorizationChangedError()
        }
    }

    private static func executionAuthorizationChangedError() -> NSError {
        NSError(domain: "ExecApprovals", code: 21, userInfo: [
            NSLocalizedDescriptionKey: "exec approval changed before execution",
        ])
    }

    private static func applyAllowlistGrantsUnlocked(
        file: inout ExecApprovalsFile,
        agentId: String?,
        grants: [ExecAllowlistUse]) -> Bool
    {
        guard !grants.isEmpty else { return false }
        let key = self.agentKey(agentId)
        var agents = file.agents ?? [:]
        var entry = agents[key] ?? ExecApprovalsAgent()
        var allowlist = entry.allowlist ?? []
        let now = Date().timeIntervalSince1970 * 1000
        for grant in grants {
            let incoming = grant.match
            if let index = allowlist.firstIndex(where: {
                self.allowlistEntryMatchKey($0) == self.allowlistEntryMatchKey(incoming)
            }) {
                allowlist[index].source = "allow-always"
                allowlist[index].lastUsedAt = now
                continue
            }
            allowlist.append(ExecAllowlistEntry(
                pattern: incoming.pattern,
                source: "allow-always",
                argPattern: incoming.argPattern,
                lastUsedAt: now))
        }
        entry.allowlist = allowlist
        agents[key] = entry
        file.agents = agents
        return true
    }

    private static func applyAllowlistUsesUnlocked(
        file: inout ExecApprovalsFile,
        agentId: String?,
        usesByKey: [ExecAllowlistEntryMatchKey: ExecAllowlistUse],
        command: String) -> Bool
    {
        guard !usesByKey.isEmpty else { return false }
        let key = self.agentKey(agentId)
        let targetKeys = key == "*" ? [key] : ["*", key]
        let now = Date().timeIntervalSince1970 * 1000
        var changed = false
        var agents = file.agents ?? [:]
        for targetKey in targetKeys {
            guard var entry = agents[targetKey], let currentAllowlist = entry.allowlist else { continue }
            var entryChanged = false
            let allowlist = currentAllowlist.map { item -> ExecAllowlistEntry in
                guard let use = usesByKey[self.allowlistEntryMatchKey(item)] else { return item }
                entryChanged = true
                return ExecAllowlistEntry(
                    id: item.id,
                    pattern: item.pattern,
                    source: item.source,
                    argPattern: item.argPattern,
                    lastUsedAt: now,
                    lastUsedCommand: self.shouldRecordLastUsedCommand(for: item) ? command : nil,
                    lastResolvedPath: use.resolvedPath)
            }
            if entryChanged {
                changed = true
                entry.allowlist = allowlist
                agents[targetKey] = entry
            }
        }
        if changed {
            file.agents = agents
        }
        return changed
    }

    private static func shouldRecordLastUsedCommand(for entry: ExecAllowlistEntry) -> Bool {
        !(entry.argPattern?.hasPrefix("sha256:argv:") ?? false)
    }

    @discardableResult
    static func updateAllowlistEntry(
        agentId: String?,
        id: String,
        pattern: String) -> Result<Void, ExecApprovalsMutationError>
    {
        let normalizedPattern: String
        switch ExecApprovalHelpers.validateAllowlistPattern(pattern) {
        case let .valid(validPattern):
            normalizedPattern = validPattern
        case let .invalid(reason):
            return .failure(.invalidPattern(reason))
        }

        return self.updateFile { file in
            let key = self.agentKey(agentId)
            var agents = file.agents ?? [:]
            var agent = agents[key] ?? ExecApprovalsAgent()
            var allowlist = agent.allowlist ?? []
            guard let index = allowlist.firstIndex(where: { $0.id == id }) else {
                if key != "*", agents["*"]?.allowlist?.contains(where: { $0.id == id }) == true {
                    throw ExecApprovalsMutationError.entryNotOwned
                }
                return
            }
            allowlist[index].pattern = normalizedPattern
            agent.allowlist = allowlist
            agents[key] = agent
            file.agents = agents
        }
    }

    @discardableResult
    static func removeAllowlistEntry(
        agentId: String?,
        id: String) -> Result<Void, ExecApprovalsMutationError>
    {
        self.updateFile { file in
            let key = self.agentKey(agentId)
            var agents = file.agents ?? [:]
            var agent = agents[key] ?? ExecApprovalsAgent()
            var allowlist = agent.allowlist ?? []
            guard let index = allowlist.firstIndex(where: { $0.id == id }) else {
                if key != "*", agents["*"]?.allowlist?.contains(where: { $0.id == id }) == true {
                    throw ExecApprovalsMutationError.entryNotOwned
                }
                return
            }
            allowlist.remove(at: index)
            agent.allowlist = allowlist
            agents[key] = agent
            file.agents = agents
        }
    }

    @discardableResult
    static func updateAgentSettings(
        agentId: String?,
        mutate: (inout ExecApprovalsAgent) -> Void) -> Result<Void, ExecApprovalsMutationError>
    {
        self.updateFile { file in
            let key = self.agentKey(agentId)
            var agents = file.agents ?? [:]
            var entry = agents[key] ?? ExecApprovalsAgent()
            mutate(&entry)
            if entry.isEmpty {
                agents.removeValue(forKey: key)
            } else {
                agents[key] = entry
            }
            file.agents = agents.isEmpty ? nil : agents
        }
    }

    private static func updateFile(
        _ mutate: (inout ExecApprovalsFile) throws -> Void) -> Result<Void, ExecApprovalsMutationError>
    {
        do {
            try self.withWriteLock {
                var file = try self.ensureFileUnlocked()
                try mutate(&file)
                try self.saveFileUnlocked(file)
            }
            return .success(())
        } catch let error as ExecApprovalsMutationError {
            return .failure(error)
        } catch {
            self.logger.error("exec approvals update failed: \(error.localizedDescription, privacy: .public)")
            return .failure(.unavailable)
        }
    }

    private static func normalizeArgPattern(_ value: String?) -> String? {
        guard let value, !value.isEmpty else { return nil }
        return value
    }

    static func allowlistEntryMatchKey(_ entry: ExecAllowlistEntry) -> ExecAllowlistEntryMatchKey {
        ExecAllowlistEntryMatchKey(
            pattern: entry.pattern,
            argPattern: entry.argPattern)
    }

    private static func ensureSecureStateDirectory() throws {
        let url = self.stateDirURL()
        // Create with the final 0700 mode directly: a default-mode (0755)
        // create followed by the chmod below leaves a transient window where
        // the directory is world-listable and concurrent observers see the
        // wrong permissions.
        try FileManager().createDirectory(
            at: url,
            withIntermediateDirectories: true,
            attributes: [.posixPermissions: self.secureStateDirPermissions])
        try ExecApprovalsFileIO.assertSafeDirectory(at: url)
        try FileManager().setAttributes(
            [.posixPermissions: self.secureStateDirPermissions],
            ofItemAtPath: url.path)
        try ExecApprovalsFileIO.assertSafeDirectory(at: url)
    }

    private static func generateToken() -> String {
        var bytes = [UInt8](repeating: 0, count: 24)
        let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
        if status == errSecSuccess {
            return Data(bytes)
                .base64EncodedString()
                .replacingOccurrences(of: "+", with: "-")
                .replacingOccurrences(of: "/", with: "_")
                .replacingOccurrences(of: "=", with: "")
        }
        return UUID().uuidString
    }

    private static func hashRaw(_ raw: String?) -> String {
        let data = Data((raw ?? "").utf8)
        let digest = SHA256.hash(data: data)
        let hash = digest.map { String(format: "%02x", $0) }.joined()
        return raw == nil ? "missing:\(hash)" : hash
    }

    private static func hashFile(_ file: ExecApprovalsFile) -> String {
        let encoder = JSONEncoder()
        encoder.outputFormatting = [.sortedKeys]
        let data = (try? encoder.encode(file)) ?? Data()
        let digest = SHA256.hash(data: data)
        return digest.map { String(format: "%02x", $0) }.joined()
    }

    static func expandPath(_ raw: String) -> String {
        let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
        let configuredHome = OpenClawEnv.path("OPENCLAW_HOME")
            .map { ($0 as NSString).expandingTildeInPath }
        let home = configuredHome.map { URL(fileURLWithPath: $0, isDirectory: true) }
            ?? FileManager().homeDirectoryForCurrentUser
        if trimmed == "~" {
            return home.path
        }
        if trimmed.hasPrefix("~/") {
            let suffix = trimmed.dropFirst(2)
            return home.appendingPathComponent(String(suffix)).path
        }
        return trimmed
    }

    private static func agentKey(_ agentId: String?) -> String {
        let trimmed = agentId?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
        return trimmed.isEmpty ? self.defaultAgentId : trimmed
    }

    private static func normalizedPattern(_ pattern: String?) -> String? {
        switch ExecApprovalHelpers.validateAllowlistPattern(pattern) {
        case let .valid(normalized):
            return normalized.lowercased()
        case .invalid(.empty):
            return nil
        case .invalid:
            let trimmed = pattern?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
            return trimmed.isEmpty ? nil : trimmed.lowercased()
        }
    }

    private static func migrateLegacyPattern(_ entry: ExecAllowlistEntry) -> ExecAllowlistEntry {
        let trimmedPattern = entry.pattern.trimmingCharacters(in: .whitespacesAndNewlines)
        let trimmedResolved = entry.lastResolvedPath?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
        let normalizedResolved = trimmedResolved.isEmpty ? nil : trimmedResolved

        if !ExecApprovalHelpers.patternHasPathSelector(trimmedPattern),
           !trimmedResolved.isEmpty,
           case let .valid(migratedPattern) = ExecApprovalHelpers.validateAllowlistPattern(trimmedResolved)
        {
            return ExecAllowlistEntry(
                id: entry.id,
                pattern: migratedPattern,
                source: entry.source,
                commandText: entry.commandText,
                argPattern: entry.argPattern,
                lastUsedAt: entry.lastUsedAt,
                lastUsedCommand: entry.lastUsedCommand,
                lastResolvedPath: normalizedResolved)
        }

        switch ExecApprovalHelpers.validateAllowlistPattern(trimmedPattern) {
        case let .valid(pattern):
            return ExecAllowlistEntry(
                id: entry.id,
                pattern: pattern,
                source: entry.source,
                commandText: entry.commandText,
                argPattern: entry.argPattern,
                lastUsedAt: entry.lastUsedAt,
                lastUsedCommand: entry.lastUsedCommand,
                lastResolvedPath: normalizedResolved)
        case .invalid:
            switch ExecApprovalHelpers.validateAllowlistPattern(trimmedResolved) {
            case let .valid(migratedPattern):
                return ExecAllowlistEntry(
                    id: entry.id,
                    pattern: migratedPattern,
                    source: entry.source,
                    commandText: entry.commandText,
                    argPattern: entry.argPattern,
                    lastUsedAt: entry.lastUsedAt,
                    lastUsedCommand: entry.lastUsedCommand,
                    lastResolvedPath: normalizedResolved)
            case .invalid:
                return ExecAllowlistEntry(
                    id: entry.id,
                    pattern: trimmedPattern,
                    source: entry.source,
                    commandText: entry.commandText,
                    argPattern: entry.argPattern,
                    lastUsedAt: entry.lastUsedAt,
                    lastUsedCommand: entry.lastUsedCommand,
                    lastResolvedPath: normalizedResolved)
            }
        }
    }

    private static func normalizeAllowlistEntries(
        _ entries: [ExecAllowlistEntry],
        dropInvalid: Bool) -> (entries: [ExecAllowlistEntry], rejected: [ExecAllowlistRejectedEntry])
    {
        var normalized: [ExecAllowlistEntry] = []
        normalized.reserveCapacity(entries.count)
        var rejected: [ExecAllowlistRejectedEntry] = []

        for entry in entries {
            var migrated = self.migrateLegacyPattern(entry)
            // Command text can contain secrets; it is accepted only for legacy decode.
            migrated.commandText = nil
            // Regex whitespace and Unicode normalization are semantic policy bytes.
            let normalizedArgPattern = self.normalizeArgPattern(migrated.argPattern)
            let trimmedPattern = migrated.pattern.trimmingCharacters(in: .whitespacesAndNewlines)
            let trimmedResolvedPath = migrated.lastResolvedPath?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
            let normalizedResolvedPath = trimmedResolvedPath.isEmpty ? nil : trimmedResolvedPath

            switch ExecApprovalHelpers.validateAllowlistPattern(trimmedPattern) {
            case let .valid(pattern):
                normalized.append(
                    ExecAllowlistEntry(
                        id: migrated.id,
                        pattern: pattern,
                        source: migrated.source,
                        commandText: migrated.commandText,
                        argPattern: normalizedArgPattern,
                        lastUsedAt: migrated.lastUsedAt,
                        lastUsedCommand: migrated.lastUsedCommand,
                        lastResolvedPath: normalizedResolvedPath))
            case let .invalid(reason):
                if dropInvalid {
                    rejected.append(
                        ExecAllowlistRejectedEntry(
                            id: migrated.id,
                            pattern: trimmedPattern,
                            reason: reason))
                } else if reason != .empty {
                    normalized.append(
                        ExecAllowlistEntry(
                            id: migrated.id,
                            pattern: trimmedPattern,
                            source: migrated.source,
                            commandText: migrated.commandText,
                            argPattern: normalizedArgPattern,
                            lastUsedAt: migrated.lastUsedAt,
                            lastUsedCommand: migrated.lastUsedCommand,
                            lastResolvedPath: normalizedResolvedPath))
                }
            }
        }

        return (normalized, rejected)
    }

    private static func mergeAgents(
        current: ExecApprovalsAgent,
        legacy: ExecApprovalsAgent) -> ExecApprovalsAgent
    {
        let currentAllowlist = self.normalizeAllowlistEntries(current.allowlist ?? [], dropInvalid: false).entries
        let legacyAllowlist = self.normalizeAllowlistEntries(legacy.allowlist ?? [], dropInvalid: false).entries
        var seen = Set<ExecAllowlistEntryMatchKey>()
        var allowlist: [ExecAllowlistEntry] = []
        func append(_ entry: ExecAllowlistEntry) {
            guard let patternKey = normalizedPattern(entry.pattern) else {
                return
            }
            let key = ExecAllowlistEntryMatchKey(
                pattern: patternKey,
                argPattern: entry.argPattern)
            guard !seen.contains(key) else {
                return
            }
            seen.insert(key)
            allowlist.append(entry)
        }
        for entry in currentAllowlist {
            append(entry)
        }
        for entry in legacyAllowlist {
            append(entry)
        }

        return ExecApprovalsAgent(
            security: current.security ?? legacy.security,
            ask: current.ask ?? legacy.ask,
            askFallback: current.askFallback ?? legacy.askFallback,
            autoAllowSkills: current.autoAllowSkills ?? legacy.autoAllowSkills,
            allowlist: allowlist.isEmpty ? nil : allowlist)
    }
}
