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

import SwiftUI

/// Single-instance Realtime voice editor — edits the store's active instance in place.
///
/// OpenAI Realtime is the only voice provider, so this view drops the multi-instance
/// concerns (add / list / activate) and presents a flat config form.
struct OpenRockyRealtimeProviderInstanceEditorView: View {
    @ObservedObject var realtimeProviderStore: OpenRockyRealtimeProviderStore
    @Environment(\.dismiss) private var dismiss

    @State private var credential: String = ""
    @State private var openaiVoice: String = OpenRockyOpenAIVoice.alloy.rawValue
    @State private var customHost: String = ""
    @State private var modelID: String = OpenRockyRealtimeProviderKind.openAI.defaultModel
    @State private var advancedSettings: OpenRockyRealtimeAdvancedSettings = .default
    @State private var testState: VoiceTestConnectionState = .idle
    @State private var loadedInstanceID: String?
    @StateObject private var openaiVoicePreview = OpenRockyOpenAIVoicePreview()

    private let provider: OpenRockyRealtimeProviderKind = .openAI

    var body: some View {
        List {
            Section {
                HStack {
                    Image(systemName: "bolt.circle.fill")
                        .font(.system(size: 28))
                        .foregroundStyle(OpenRockyPalette.secondary)
                    VStack(alignment: .leading, spacing: 2) {
                        Text(provider.displayName)
                            .font(.system(size: 16, weight: .semibold))
                        Text(provider.summary)
                            .font(.caption)
                            .foregroundStyle(.secondary)
                            .lineLimit(2)
                    }
                }
                .padding(.vertical, 4)
            } header: {
                Text("Voice Provider")
            }

            Section {
                SecureField(
                    provider.credentialTitle,
                    text: $credential,
                    prompt: Text(provider.credentialPlaceholder)
                )
                .textInputAutocapitalization(.never)
                .autocorrectionDisabled()
                .textContentType(.init(rawValue: ""))

                if let guideURL = provider.apiKeyGuideURL, let url = URL(string: guideURL) {
                    Link(destination: url) {
                        HStack {
                            Image(systemName: "arrow.up.forward.square")
                            Text("Get \(provider.displayName) API Key")
                        }
                        .font(.subheadline)
                    }
                }
            } header: {
                Text(provider.credentialTitle)
            }

            Section {
                ForEach(OpenRockyOpenAIVoice.allCases) { voice in
                    Button {
                        openaiVoice = voice.rawValue
                    } label: {
                        HStack(spacing: 12) {
                            Image(systemName: openaiVoice == voice.rawValue ? "checkmark.circle.fill" : "circle")
                                .foregroundStyle(openaiVoice == voice.rawValue ? Color.accentColor : .secondary)
                                .font(.system(size: 20))
                            VStack(alignment: .leading, spacing: 2) {
                                Text(voice.displayName)
                                    .foregroundStyle(.primary)
                                    .font(.subheadline.weight(.medium))
                                Text(voice.subtitle)
                                    .foregroundStyle(.secondary)
                                    .font(.caption2)
                                    .lineLimit(2)
                            }
                            Spacer()
                            Button {
                                if openaiVoicePreview.playingVoice == voice.rawValue {
                                    openaiVoicePreview.stop()
                                } else {
                                    openaiVoicePreview.play(
                                        voice: voice.rawValue,
                                        credential: credential,
                                        customHost: customHost.isEmpty ? nil : customHost
                                    )
                                }
                            } label: {
                                if openaiVoicePreview.playingVoice == voice.rawValue && openaiVoicePreview.isLoading {
                                    ProgressView()
                                        .controlSize(.small)
                                } else {
                                    Image(systemName: openaiVoicePreview.playingVoice == voice.rawValue ? "stop.circle.fill" : "play.circle.fill")
                                        .font(.system(size: 26))
                                        .foregroundStyle(openaiVoicePreview.playingVoice == voice.rawValue ? .orange : .accentColor)
                                }
                            }
                            .buttonStyle(.plain)
                        }
                    }
                    .buttonStyle(.plain)
                }
                if let err = openaiVoicePreview.error {
                    Text(err)
                        .font(.caption)
                        .foregroundStyle(.red)
                }
            } header: {
                Text("Voice")
            }

            Section {
                TextField("wss://your-proxy.example.com", text: $customHost)
                    .textInputAutocapitalization(.never)
                    .keyboardType(.URL)
                    .autocorrectionDisabled()
            } header: {
                Text("Custom Host")
            } footer: {
                Text("Optional. Override the default WebSocket host (for proxies / self-hosting).")
            }

            OpenRockyRealtimeAdvancedSettingsSection(settings: $advancedSettings)

            Section {
                Button {
                    testVoiceConnection()
                } label: {
                    HStack(spacing: 10) {
                        switch testState {
                        case .idle:
                            Image(systemName: "bolt.circle.fill")
                                .foregroundStyle(OpenRockyPalette.accent)
                            Text("Test Connection")
                                .foregroundStyle(OpenRockyPalette.accent)
                        case .testing:
                            ProgressView()
                                .controlSize(.small)
                            Text("Testing...")
                                .foregroundStyle(.secondary)
                        case .success(let detail):
                            Image(systemName: "checkmark.circle.fill")
                                .foregroundStyle(OpenRockyPalette.success)
                            Text("Connected — \(detail)")
                                .foregroundStyle(OpenRockyPalette.success)
                                .lineLimit(1)
                        case .failure(let message):
                            Image(systemName: "xmark.circle.fill")
                                .foregroundStyle(.red)
                            Text(message)
                                .foregroundStyle(.red)
                                .lineLimit(5)
                        }
                        Spacer()
                    }
                    .font(.system(size: 14, weight: .semibold))
                }
                .disabled(testState == .testing || !draftIsConfigured)
            } header: {
                Text("Connection Test")
            } footer: {
                Text("Opens a WebSocket to verify your credential and model are accepted.")
            }
        }
        .navigationTitle("Realtime Voice")
        .navigationBarTitleDisplayMode(.inline)
        .onAppear { loadActive() }
        .onChange(of: realtimeProviderStore.activeInstanceID) { _, _ in loadActive() }
        .toolbar {
            ToolbarItem(placement: .topBarTrailing) {
                Button("Save") {
                    saveActive()
                    dismiss()
                }
                .fontWeight(.bold)
                .disabled(!draftIsConfigured)
            }
        }
    }

    private func loadActive() {
        guard let instance = realtimeProviderStore.activeInstance else { return }
        if loadedInstanceID == instance.id { return }
        loadedInstanceID = instance.id
        credential = realtimeProviderStore.credential(for: instance) ?? ""
        openaiVoice = instance.openaiVoice ?? OpenRockyOpenAIVoice.alloy.rawValue
        customHost = instance.customHost ?? ""
        modelID = instance.modelID
        advancedSettings = instance.effectiveAdvancedSettings
    }

    private var draftIsConfigured: Bool {
        let config = OpenRockyRealtimeProviderConfiguration(
            provider: provider,
            modelID: modelID,
            credential: credential,
            customHost: customHost
        ).normalized()
        return config.isConfigured
    }

    private func saveActive() {
        guard var instance = realtimeProviderStore.activeInstance else { return }
        let cred = credential.trimmingCharacters(in: .whitespacesAndNewlines)
        instance.kind = provider
        // Use advanced.realtimeModel as the canonical model — modelID was a top-level
        // legacy field, the advanced settings are the source of truth now.
        instance.modelID = advancedSettings.realtimeModel
        instance.openaiVoice = openaiVoice
        instance.customHost = customHost.isEmpty ? nil : customHost
        // Persist advanced settings only if they differ from defaults — keeps the
        // serialized JSON clean for users who never touch the disclosure.
        instance.advancedSettings = advancedSettings == .default ? nil : advancedSettings
        realtimeProviderStore.update(instance, credential: cred.isEmpty ? nil : cred)
    }

    private func testVoiceConnection() {
        testState = .testing
        let config = OpenRockyRealtimeProviderConfiguration(
            provider: provider,
            modelID: modelID,
            credential: credential,
            customHost: customHost
        ).normalized()

        Task {
            do {
                let testModelID = "gpt-realtime-2"
                let openAIHost = config.customHost ?? "wss://api.openai.com"
                guard let url = URL(string: "\(openAIHost)/v1/realtime?model=\(testModelID)") else {
                    await MainActor.run { testState = .failure(message: "Invalid custom host URL") }
                    return
                }

                var request = URLRequest(url: url)
                request.setValue("Bearer \(config.credential ?? "")", forHTTPHeaderField: "Authorization")

                let socket = URLSession.shared.webSocketTask(with: request)
                socket.resume()

                let success = try await withThrowingTaskGroup(of: String.self) { group in
                    group.addTask {
                        let message = try await socket.receive()
                        switch message {
                        case .data(let data):
                            if data.count >= 4 {
                                let msgType = data[1] >> 4
                                if msgType == 0x0F {
                                    let headerSize = Int(data[0] & 0x0F) * 4
                                    if data.count > headerSize + 8 {
                                        let payloadStart = headerSize + 8
                                        var errorData = data.subdata(in: payloadStart..<data.count)
                                        let compression = data[2] & 0x0F
                                        if compression == 0x01 {
                                            errorData = (try? (errorData as NSData).decompressed(using: .zlib) as Data) ?? errorData
                                        }
                                        let errorText = String(data: errorData, encoding: .utf8) ?? "unknown"
                                        return "ERROR:\(errorText)"
                                    }
                                    return "ERROR:unknown server error"
                                }
                            }
                            return "OK"
                        case .string(let text):
                            rlog.debug("Realtime test response: \(text.prefix(300))", category: "Test")
                            if let d = text.data(using: .utf8),
                               let json = try? JSONSerialization.jsonObject(with: d) as? [String: Any] {
                                if let type = json["type"] as? String, type == "error" {
                                    let msg = (json["error"] as? [String: Any])?["message"] as? String ?? text
                                    return "ERROR:\(msg)"
                                }
                                if let errObj = json["error"] as? [String: Any] {
                                    let msg = errObj["message"] as? String ?? text
                                    return "ERROR:\(msg)"
                                }
                            }
                            return "OK"
                        @unknown default:
                            return "ERROR:unknown"
                        }
                    }
                    group.addTask {
                        try await Task.sleep(nanoseconds: 10_000_000_000)
                        throw URLError(.timedOut)
                    }
                    let first = try await group.next()!
                    group.cancelAll()
                    return first
                }

                socket.cancel(with: .goingAway, reason: nil)

                if success.hasPrefix("ERROR:") {
                    let msg = String(success.dropFirst(6))
                    rlog.error("Realtime test failed: \(msg)", category: "Test")
                    testState = .failure(message: msg)
                } else {
                    rlog.info("Realtime test passed: \(config.provider) \(testModelID)", category: "Test")
                    testState = .success(detail: testModelID)
                }
            } catch let error as URLError where error.code == .timedOut {
                rlog.warning("Realtime test timeout: \(config.provider)", category: "Test")
                testState = .failure(message: "Connection timed out")
            } catch {
                rlog.error("Realtime test error: \(error.localizedDescription)", category: "Test")
                let probeMsg = await probeEndpointError(config: config)
                rlog.debug("Realtime test probe: \(probeMsg)", category: "Test")
                testState = .failure(message: probeMsg)
            }
        }
    }

    private func probeEndpointError(config: OpenRockyRealtimeProviderConfiguration) async -> String {
        let host = config.customHost?.replacingOccurrences(of: "wss://", with: "https://") ?? "https://api.openai.com"
        guard let httpURL = URL(string: "\(host)/v1/realtime?model=gpt-realtime-2") else {
            return "Invalid custom host URL"
        }

        var request = URLRequest(url: httpURL)
        request.httpMethod = "GET"
        request.setValue("Bearer \(config.credential ?? "")", forHTTPHeaderField: "Authorization")

        do {
            let (data, response) = try await URLSession.shared.data(for: request)
            let status = (response as? HTTPURLResponse)?.statusCode ?? 0
            let body = String(data: data, encoding: .utf8) ?? ""

            if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
                if let errorDict = json["error"] as? [String: Any],
                   let msg = errorDict["message"] as? String {
                    return "HTTP \(status): \(msg)"
                }
                if let msg = json["message"] as? String {
                    return "HTTP \(status): \(msg)"
                }
            }

            let preview = String(body.prefix(200))
            return "HTTP \(status): \(preview.isEmpty ? "No response body" : preview)"
        } catch {
            return "Connection failed: \(error.localizedDescription)"
        }
    }
}

private enum VoiceTestConnectionState: Equatable {
    case idle
    case testing
    case success(detail: String)
    case failure(message: String)
}
