import ClawdbotKit
import ClawdbotProtocol
import Darwin
import Foundation

struct WizardCliOptions {
    var url: String?
    var token: String?
    var password: String?
    var mode: String = "local"
    var workspace: String?
    var json: Bool = false
    var help: Bool = false

    static func parse(_ args: [String]) -> WizardCliOptions {
        var opts = WizardCliOptions()
        var i = 0
        while i < args.count {
            let arg = args[i]
            switch arg {
            case "-h", "--help":
                opts.help = true
            case "--json":
                opts.json = true
            case "--url":
                opts.url = self.nextValue(args, index: &i)
            case "--token":
                opts.token = self.nextValue(args, index: &i)
            case "--password":
                opts.password = self.nextValue(args, index: &i)
            case "--mode":
                if let value = nextValue(args, index: &i) {
                    opts.mode = value
                }
            case "--workspace":
                opts.workspace = self.nextValue(args, index: &i)
            default:
                break
            }
            i += 1
        }
        return opts
    }

    private static func nextValue(_ args: [String], index: inout Int) -> String? {
        guard index + 1 < args.count else { return nil }
        index += 1
        return args[index].trimmingCharacters(in: .whitespacesAndNewlines)
    }
}

enum WizardCliError: Error, CustomStringConvertible {
    case invalidUrl(String)
    case missingRemoteUrl
    case gatewayError(String)
    case decodeError(String)
    case cancelled

    var description: String {
        switch self {
        case let .invalidUrl(raw): "Invalid URL: \(raw)"
        case .missingRemoteUrl: "gateway.remote.url is missing"
        case let .gatewayError(msg): msg
        case let .decodeError(msg): msg
        case .cancelled: "Wizard cancelled"
        }
    }
}

func runWizardCommand(_ args: [String]) async {
    let opts = WizardCliOptions.parse(args)
    if opts.help {
        print("""
        clawdbot-mac wizard

        Usage:
          clawdbot-mac wizard [--url <ws://host:port>] [--token <token>] [--password <password>]
                              [--mode <local|remote>] [--workspace <path>] [--json]

        Options:
          --url <url>        Gateway WebSocket URL (overrides config)
          --token <token>    Gateway token (if required)
          --password <pw>    Gateway password (if required)
          --mode <mode>      Wizard mode (local|remote). Default: local
          --workspace <path> Wizard workspace override
          --json             Print raw wizard responses
          -h, --help         Show help
        """)
        return
    }

    let config = loadGatewayConfig()
    do {
        guard isatty(STDIN_FILENO) != 0 else {
            throw WizardCliError.gatewayError("Wizard requires an interactive TTY.")
        }
        let endpoint = try resolveWizardGatewayEndpoint(opts: opts, config: config)
        let client = GatewayWizardClient(
            url: endpoint.url,
            token: endpoint.token,
            password: endpoint.password,
            json: opts.json)
        try await client.connect()
        defer { Task { await client.close() } }
        try await runWizard(client: client, opts: opts)
    } catch {
        fputs("wizard: \(error)\n", stderr)
        exit(1)
    }
}

private func resolveWizardGatewayEndpoint(opts: WizardCliOptions, config: GatewayConfig) throws -> GatewayEndpoint {
    if let raw = opts.url, !raw.isEmpty {
        guard let url = URL(string: raw) else { throw WizardCliError.invalidUrl(raw) }
        return GatewayEndpoint(
            url: url,
            token: resolvedToken(opts: opts, config: config),
            password: resolvedPassword(opts: opts, config: config),
            mode: (config.mode ?? "local").lowercased())
    }

    let mode = (config.mode ?? "local").lowercased()
    if mode == "remote" {
        guard let raw = config.remoteUrl?.trimmingCharacters(in: .whitespacesAndNewlines), !raw.isEmpty else {
            throw WizardCliError.missingRemoteUrl
        }
        guard let url = URL(string: raw) else { throw WizardCliError.invalidUrl(raw) }
        return GatewayEndpoint(
            url: url,
            token: resolvedToken(opts: opts, config: config),
            password: resolvedPassword(opts: opts, config: config),
            mode: mode)
    }

    let port = config.port ?? 18789
    let host = "127.0.0.1"
    guard let url = URL(string: "ws://\(host):\(port)") else {
        throw WizardCliError.invalidUrl("ws://\(host):\(port)")
    }
    return GatewayEndpoint(
        url: url,
        token: resolvedToken(opts: opts, config: config),
        password: resolvedPassword(opts: opts, config: config),
        mode: mode)
}

private func resolvedToken(opts: WizardCliOptions, config: GatewayConfig) -> String? {
    if let token = opts.token, !token.isEmpty { return token }
    if let token = ProcessInfo.processInfo.environment["CLAWDBOT_GATEWAY_TOKEN"], !token.isEmpty {
        return token
    }
    if (config.mode ?? "local").lowercased() == "remote" {
        return config.remoteToken
    }
    return config.token
}

private func resolvedPassword(opts: WizardCliOptions, config: GatewayConfig) -> String? {
    if let password = opts.password, !password.isEmpty { return password }
    if let password = ProcessInfo.processInfo.environment["CLAWDBOT_GATEWAY_PASSWORD"], !password.isEmpty {
        return password
    }
    if (config.mode ?? "local").lowercased() == "remote" {
        return config.remotePassword
    }
    return config.password
}

actor GatewayWizardClient {
    private enum ConnectChallengeError: Error {
        case timeout
    }

    private let url: URL
    private let token: String?
    private let password: String?
    private let json: Bool
    private let encoder = JSONEncoder()
    private let decoder = JSONDecoder()
    private let session = URLSession(configuration: .default)
    private let connectChallengeTimeoutSeconds: Double = 0.75
    private var task: URLSessionWebSocketTask?

    init(url: URL, token: String?, password: String?, json: Bool) {
        self.url = url
        self.token = token
        self.password = password
        self.json = json
    }

    func connect() async throws {
        let socket = self.session.webSocketTask(with: self.url)
        socket.maximumMessageSize = 16 * 1024 * 1024
        socket.resume()
        self.task = socket
        try await self.sendConnect()
    }

    func close() {
        self.task?.cancel(with: .goingAway, reason: nil)
        self.task = nil
    }

    func request(method: String, params: [String: ProtoAnyCodable]?) async throws -> ResponseFrame {
        guard let task = self.task else {
            throw WizardCliError.gatewayError("gateway not connected")
        }
        let id = UUID().uuidString
        let frame = RequestFrame(
            type: "req",
            id: id,
            method: method,
            params: params.map { ProtoAnyCodable($0) })
        let data = try self.encoder.encode(frame)
        try await task.send(.data(data))

        while true {
            let message = try await task.receive()
            let frame = try decodeFrame(message)
            if case let .res(res) = frame, res.id == id {
                if res.ok == false {
                    let msg = (res.error?["message"]?.value as? String) ?? "gateway error"
                    throw WizardCliError.gatewayError(msg)
                }
                return res
            }
        }
    }

    func decodePayload<T: Decodable>(_ response: ResponseFrame, as _: T.Type) throws -> T {
        guard let payload = response.payload else {
            throw WizardCliError.decodeError("missing payload")
        }
        let data = try self.encoder.encode(payload)
        return try self.decoder.decode(T.self, from: data)
    }

    private func decodeFrame(_ message: URLSessionWebSocketTask.Message) throws -> GatewayFrame {
        let data: Data? = switch message {
        case let .data(data): data
        case let .string(text): text.data(using: .utf8)
        @unknown default: nil
        }
        guard let data else {
            throw WizardCliError.decodeError("empty gateway response")
        }
        return try self.decoder.decode(GatewayFrame.self, from: data)
    }

    private func sendConnect() async throws {
        guard let task = self.task else {
            throw WizardCliError.gatewayError("gateway not connected")
        }
        let osVersion = ProcessInfo.processInfo.operatingSystemVersion
        let platform = "macos \(osVersion.majorVersion).\(osVersion.minorVersion).\(osVersion.patchVersion)"
        let clientId = "clawdbot-macos"
        let clientMode = "ui"
        let role = "operator"
        let scopes: [String] = []
        let client: [String: ProtoAnyCodable] = [
            "id": ProtoAnyCodable(clientId),
            "displayName": ProtoAnyCodable(Host.current().localizedName ?? "Clawdbot macOS Wizard CLI"),
            "version": ProtoAnyCodable("dev"),
            "platform": ProtoAnyCodable(platform),
            "deviceFamily": ProtoAnyCodable("Mac"),
            "mode": ProtoAnyCodable(clientMode),
            "instanceId": ProtoAnyCodable(UUID().uuidString),
        ]

        var params: [String: ProtoAnyCodable] = [
            "minProtocol": ProtoAnyCodable(GATEWAY_PROTOCOL_VERSION),
            "maxProtocol": ProtoAnyCodable(GATEWAY_PROTOCOL_VERSION),
            "client": ProtoAnyCodable(client),
            "caps": ProtoAnyCodable([String]()),
            "locale": ProtoAnyCodable(Locale.preferredLanguages.first ?? Locale.current.identifier),
            "userAgent": ProtoAnyCodable(ProcessInfo.processInfo.operatingSystemVersionString),
            "role": ProtoAnyCodable(role),
            "scopes": ProtoAnyCodable(scopes),
        ]
        if let token = self.token {
            params["auth"] = ProtoAnyCodable(["token": ProtoAnyCodable(token)])
        } else if let password = self.password {
            params["auth"] = ProtoAnyCodable(["password": ProtoAnyCodable(password)])
        }
        let connectNonce = try await self.waitForConnectChallenge()
        let identity = DeviceIdentityStore.loadOrCreate()
        let signedAtMs = Int(Date().timeIntervalSince1970 * 1000)
        let scopesValue = scopes.joined(separator: ",")
        var payloadParts = [
            connectNonce == nil ? "v1" : "v2",
            identity.deviceId,
            clientId,
            clientMode,
            role,
            scopesValue,
            String(signedAtMs),
            self.token ?? "",
        ]
        if let connectNonce {
            payloadParts.append(connectNonce)
        }
        let payload = payloadParts.joined(separator: "|")
        if let signature = DeviceIdentityStore.signPayload(payload, identity: identity),
           let publicKey = DeviceIdentityStore.publicKeyBase64Url(identity)
        {
            var device: [String: ProtoAnyCodable] = [
                "id": ProtoAnyCodable(identity.deviceId),
                "publicKey": ProtoAnyCodable(publicKey),
                "signature": ProtoAnyCodable(signature),
                "signedAt": ProtoAnyCodable(signedAtMs),
            ]
            if let connectNonce {
                device["nonce"] = ProtoAnyCodable(connectNonce)
            }
            params["device"] = ProtoAnyCodable(device)
        }

        let reqId = UUID().uuidString
        let frame = RequestFrame(
            type: "req",
            id: reqId,
            method: "connect",
            params: ProtoAnyCodable(params))
        let data = try self.encoder.encode(frame)
        try await task.send(.data(data))

        while true {
            let message = try await task.receive()
            let frameResponse = try decodeFrame(message)
            if case let .res(res) = frameResponse, res.id == reqId {
                if res.ok == false {
                    let msg = (res.error?["message"]?.value as? String) ?? "gateway connect failed"
                    throw WizardCliError.gatewayError(msg)
                }
                _ = try self.decodePayload(res, as: HelloOk.self)
                return
            }
        }
    }

    private func waitForConnectChallenge() async throws -> String? {
        guard let task = self.task else { return nil }
        do {
            return try await AsyncTimeout.withTimeout(
                seconds: self.connectChallengeTimeoutSeconds,
                onTimeout: { ConnectChallengeError.timeout },
                operation: {
                    while true {
                        let message = try await task.receive()
                        let frame = try await self.decodeFrame(message)
                        if case let .event(evt) = frame, evt.event == "connect.challenge" {
                            if let payload = evt.payload?.value as? [String: ProtoAnyCodable],
                               let nonce = payload["nonce"]?.value as? String
                            {
                                return nonce
                            }
                        }
                    }
                })
        } catch {
            if error is ConnectChallengeError { return nil }
            throw error
        }
    }
}

private func runWizard(client: GatewayWizardClient, opts: WizardCliOptions) async throws {
    var params: [String: ProtoAnyCodable] = [:]
    let mode = opts.mode.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
    if mode == "local" || mode == "remote" {
        params["mode"] = ProtoAnyCodable(mode)
    }
    if let workspace = opts.workspace?.trimmingCharacters(in: .whitespacesAndNewlines), !workspace.isEmpty {
        params["workspace"] = ProtoAnyCodable(workspace)
    }

    let startResponse = try await client.request(method: "wizard.start", params: params)
    let startResult = try await client.decodePayload(startResponse, as: WizardStartResult.self)
    if opts.json {
        dumpResult(startResponse)
    }

    let sessionId = startResult.sessionid
    var nextResult = WizardNextResult(
        done: startResult.done,
        step: startResult.step,
        status: startResult.status,
        error: startResult.error)

    do {
        while true {
            let status = wizardStatusString(nextResult.status) ?? (nextResult.done ? "done" : "running")
            if status == "cancelled" {
                print("Wizard cancelled.")
                return
            }
            if status == "error" || (nextResult.done && nextResult.error != nil) {
                throw WizardCliError.gatewayError(nextResult.error ?? "wizard error")
            }
            if status == "done" || nextResult.done {
                print("Wizard complete.")
                return
            }

            if let step = decodeWizardStep(nextResult.step) {
                let answer = try promptAnswer(for: step)
                var answerPayload: [String: ProtoAnyCodable] = [
                    "stepId": ProtoAnyCodable(step.id),
                ]
                if !(answer is NSNull) {
                    answerPayload["value"] = ProtoAnyCodable(answer)
                }
                let response = try await client.request(
                    method: "wizard.next",
                    params: [
                        "sessionId": ProtoAnyCodable(sessionId),
                        "answer": ProtoAnyCodable(answerPayload),
                    ])
                nextResult = try await client.decodePayload(response, as: WizardNextResult.self)
                if opts.json {
                    dumpResult(response)
                }
            } else {
                let response = try await client.request(
                    method: "wizard.next",
                    params: ["sessionId": ProtoAnyCodable(sessionId)])
                nextResult = try await client.decodePayload(response, as: WizardNextResult.self)
                if opts.json {
                    dumpResult(response)
                }
            }
        }
    } catch WizardCliError.cancelled {
        _ = try? await client.request(
            method: "wizard.cancel",
            params: ["sessionId": ProtoAnyCodable(sessionId)])
        throw WizardCliError.cancelled
    }
}

private func dumpResult(_ response: ResponseFrame) {
    guard let payload = response.payload else {
        print("{\"error\":\"missing payload\"}")
        return
    }
    let encoder = JSONEncoder()
    encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
    if let data = try? encoder.encode(payload), let text = String(data: data, encoding: .utf8) {
        print(text)
    }
}

private func promptAnswer(for step: WizardStep) throws -> Any {
    let type = wizardStepType(step)
    if let title = step.title, !title.isEmpty {
        print("\n\(title)")
    }
    if let message = step.message, !message.isEmpty {
        print(message)
    }

    switch type {
    case "note":
        _ = try readLineWithPrompt("Continue? (enter)")
        return NSNull()
    case "progress":
        _ = try readLineWithPrompt("Continue? (enter)")
        return NSNull()
    case "action":
        _ = try readLineWithPrompt("Run? (enter)")
        return true
    case "text":
        let initial = anyCodableString(step.initialvalue)
        let prompt = step.placeholder ?? "Value"
        let value = try readLineWithPrompt("\(prompt)\(initial.isEmpty ? "" : " [\(initial)]")")
        let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
        return trimmed.isEmpty ? initial : trimmed
    case "confirm":
        let initial = anyCodableBool(step.initialvalue)
        let value = try readLineWithPrompt("Confirm? (y/n) [\(initial ? "y" : "n")]")
        let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
        if trimmed.isEmpty { return initial }
        return trimmed == "y" || trimmed == "yes" || trimmed == "true"
    case "select":
        return try promptSelect(step)
    case "multiselect":
        return try promptMultiSelect(step)
    default:
        _ = try readLineWithPrompt("Continue? (enter)")
        return NSNull()
    }
}

private func promptSelect(_ step: WizardStep) throws -> Any {
    let options = parseWizardOptions(step.options)
    guard !options.isEmpty else { return NSNull() }
    for (idx, option) in options.enumerated() {
        let hint = option.hint?.isEmpty == false ? " — \(option.hint!)" : ""
        print("  [\(idx + 1)] \(option.label)\(hint)")
    }
    let initialIndex = options.firstIndex(where: { anyCodableEqual($0.value, step.initialvalue) })
    let defaultLabel = initialIndex.map { " [\($0 + 1)]" } ?? ""
    while true {
        let input = try readLineWithPrompt("Select one\(defaultLabel)")
        let trimmed = input.trimmingCharacters(in: .whitespacesAndNewlines)
        if trimmed.isEmpty, let initialIndex {
            return options[initialIndex].value?.value ?? options[initialIndex].label
        }
        if trimmed.lowercased() == "q" { throw WizardCliError.cancelled }
        if let number = Int(trimmed), (1...options.count).contains(number) {
            let option = options[number - 1]
            return option.value?.value ?? option.label
        }
        print("Invalid selection.")
    }
}

private func promptMultiSelect(_ step: WizardStep) throws -> [Any] {
    let options = parseWizardOptions(step.options)
    guard !options.isEmpty else { return [] }
    for (idx, option) in options.enumerated() {
        let hint = option.hint?.isEmpty == false ? " — \(option.hint!)" : ""
        print("  [\(idx + 1)] \(option.label)\(hint)")
    }
    let initialValues = anyCodableArray(step.initialvalue)
    let initialIndices = options.enumerated().compactMap { index, option in
        initialValues.contains { anyCodableEqual($0, option.value) } ? index + 1 : nil
    }
    let defaultLabel = initialIndices.isEmpty ? "" : " [\(initialIndices.map(String.init).joined(separator: ","))]"
    while true {
        let input = try readLineWithPrompt("Select (comma-separated)\(defaultLabel)")
        let trimmed = input.trimmingCharacters(in: .whitespacesAndNewlines)
        if trimmed.isEmpty {
            return initialIndices.map { options[$0 - 1].value?.value ?? options[$0 - 1].label }
        }
        if trimmed.lowercased() == "q" { throw WizardCliError.cancelled }
        let parts = trimmed.split(separator: ",").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
        let indices = parts.compactMap { Int($0) }.filter { (1...options.count).contains($0) }
        if indices.isEmpty {
            print("Invalid selection.")
            continue
        }
        return indices.map { options[$0 - 1].value?.value ?? options[$0 - 1].label }
    }
}

private func readLineWithPrompt(_ prompt: String) throws -> String {
    print("\(prompt): ", terminator: "")
    guard let line = readLine() else {
        throw WizardCliError.cancelled
    }
    return line
}
