//
// OpenRocky — Voice-first AI Agent
// https://github.com/openrocky
//
// Developed by everettjf with the assistance of Claude Code and Codex.
// Date: 2026-05-04
// Copyright (c) 2026 everettjf. All rights reserved.
//

import Combine
import Foundation

/// Persistent registry of user-configured Model Context Protocol servers.
/// Stored as JSON in UserDefaults — small payload, tiny number of servers, no
/// reason to spin up a real database. Mirrors `OpenRockyCustomSkillStore`'s
/// shape so the settings UI can be kept consistent.
@MainActor
final class OpenRockyMCPStore: ObservableObject {
    static let shared = OpenRockyMCPStore()

    @Published private(set) var servers: [OpenRockyMCPServer] = []

    private static let storageKey = "OpenRockyMCPServers.v1"

    init() {
        load()
    }

    // MARK: - CRUD

    func add(_ server: OpenRockyMCPServer) {
        rlog.info("MCP server added: \(server.label)", category: "MCP")
        servers.append(server)
        persist()
    }

    func update(_ server: OpenRockyMCPServer) {
        guard let idx = servers.firstIndex(where: { $0.id == server.id }) else { return }
        servers[idx] = server
        persist()
    }

    func delete(id: String) {
        let label = servers.first { $0.id == id }?.label ?? id
        rlog.info("MCP server deleted: \(label)", category: "MCP")
        servers.removeAll { $0.id == id }
        persist()
    }

    func toggle(id: String) {
        guard let idx = servers.firstIndex(where: { $0.id == id }) else { return }
        servers[idx].isEnabled.toggle()
        persist()
    }

    func server(id: String) -> OpenRockyMCPServer? {
        servers.first { $0.id == id }
    }

    func server(forSanitizedLabel label: String) -> OpenRockyMCPServer? {
        servers.first { OpenRockyMCPServer.sanitizedLabel($0.label) == label }
    }

    /// Update the cached tool catalog after a successful `tools/list`. Called
    /// from the refresh action in the settings UI and from the lazy refresh
    /// path the toolbox kicks off when it sees an empty cache.
    func updateCachedTools(serverID: String, tools: [OpenRockyMCPServer.CachedTool]) {
        guard let idx = servers.firstIndex(where: { $0.id == serverID }) else { return }
        servers[idx].cachedTools = tools
        servers[idx].lastRefreshedAt = Date()
        persist()
    }

    // MARK: - Tool lookup

    /// Resolves a namespaced tool name back to its server. Returns nil for
    /// non-MCP tools or unknown servers (the toolbox's default branch will
    /// then bubble up `unsupportedTool` so the caller sees a real error
    /// rather than the call hanging).
    func resolveTool(namespacedName: String) -> (server: OpenRockyMCPServer, toolName: String)? {
        guard let parsed = OpenRockyMCPServer.parseNamespacedToolName(namespacedName) else { return nil }
        guard let server = server(forSanitizedLabel: parsed.sanitizedLabel) else { return nil }
        // The cached tool name might have been stored verbatim or with `_`
        // substitutions from sanitization; accept either form.
        let normalized = parsed.toolName
        let known = server.cachedTools.contains { tool in
            tool.name == normalized
                || OpenRockyMCPServer.sanitizedToolName(serverLabel: server.label, toolName: tool.name).hasSuffix("-\(normalized)")
        }
        guard known else { return nil }
        // Try to map back to the real (un-scrubbed) tool name so the JSON-RPC
        // call carries the canonical identifier the server expects.
        let realName = server.cachedTools.first(where: { tool in
            tool.name == normalized
                || OpenRockyMCPServer.sanitizedToolName(serverLabel: server.label, toolName: tool.name).hasSuffix("-\(normalized)")
        })?.name ?? normalized
        return (server, realName)
    }

    // MARK: - Persistence

    private func load() {
        guard let data = UserDefaults.standard.data(forKey: Self.storageKey) else { return }
        do {
            servers = try JSONDecoder().decode([OpenRockyMCPServer].self, from: data)
        } catch {
            rlog.error("MCP store decode failed: \(error.localizedDescription)", category: "MCP")
            servers = []
        }
    }

    private func persist() {
        do {
            let data = try JSONEncoder().encode(servers)
            UserDefaults.standard.set(data, forKey: Self.storageKey)
        } catch {
            rlog.error("MCP store encode failed: \(error.localizedDescription)", category: "MCP")
        }
    }
}
