import Foundation
import NucleicCore

/// Tier-2 contract-test stub (OBSERVABILITY B.4): speaks the real Claude Code
/// wire protocols off a recorded fixture — NDJSON on stdout, and for approval
/// directives it performs the actual MCP HTTP `tools/call(approve)` against the
/// adapter's in-process server and asserts on the reply. Runs through the
/// production ProcessHost + adapter with no `claude` installed and no cost.
///
/// Fixture lines are either verbatim native NDJSON (echoed to stdout) or
/// directives:
///   {"__fake__":"expect_stdin"}                      — read + validate one stdin user message
///   {"__fake__":"approval","tool_name":…,"input":…,
///    "tool_use_id":…,"expect":"allow"|"deny"}        — full MCP round-trip, assert behavior
///   {"__fake__":"sleep","ms":50}                     — pacing
///
/// Exit codes: 0 ok · 3 stdin-envelope mismatch · 4 approval-reply mismatch ·
/// 5 setup/handshake failure.
@main
struct FakeClaude {
    static func main() async {
        guard let fixturePath = ProcessInfo.processInfo.environment["FAKE_CLAUDE_FIXTURE"] else {
            die(5, "FAKE_CLAUDE_FIXTURE not set")
        }
        guard let fixture = try? String(contentsOfFile: fixturePath, encoding: .utf8) else {
            die(5, "cannot read fixture \(fixturePath)")
        }

        for rawLine in fixture.split(separator: "\n", omittingEmptySubsequences: true) {
            let line = String(rawLine)
            guard let parsed = try? JSONValue(parsing: line),
                let directive = parsed["__fake__"]?.stringValue
            else {
                emit(line)
                continue
            }

            switch directive {
            case "expect_stdin":
                await expectStdin()
            case "approval":
                await performApproval(parsed)
            case "sleep":
                let ms = parsed["ms"]?.intValue ?? 10
                try? await Task.sleep(for: .milliseconds(ms))
            default:
                die(5, "unknown directive \(directive)")
            }
        }
        exit(0)
    }

    static func emit(_ line: String) {
        FileHandle.standardOutput.write(Data((line + "\n").utf8))
    }

    static func die(_ code: Int32, _ message: String) -> Never {
        FileHandle.standardError.write(Data(("fake-claude: " + message + "\n").utf8))
        exit(code)
    }

    // MARK: - stdin envelope validation (ADAPTERS §1.3)

    static func expectStdin() async {
        guard let line = readLine(strippingNewline: true) else {
            die(3, "expected a user message on stdin, got EOF")
        }
        guard let message = try? JSONValue(parsing: line) else {
            die(3, "stdin line is not JSON: \(line)")
        }
        guard message["type"]?.stringValue == "user" else {
            die(3, "stdin envelope type != user: \(line)")
        }
        guard message["message"]?["role"]?.stringValue == "user" else {
            die(3, "stdin envelope missing message.role=user: \(line)")
        }
        let blocks = message["message"]?["content"]?.arrayValue ?? []
        guard blocks.contains(where: { $0["type"]?.stringValue == "text" }) else {
            die(3, "stdin envelope has no text block: \(line)")
        }
    }

    // MARK: - MCP approval round-trip (ADAPTERS §1.2)

    static func performApproval(_ directive: JSONValue) async {
        // The adapter passed our own argv: find --mcp-config to discover the
        // server URL + bearer token, exactly as the real CLI would.
        let arguments = CommandLine.arguments
        guard let index = arguments.firstIndex(of: "--mcp-config"),
            arguments.indices.contains(index + 1),
            let config = try? JSONValue(parsing: arguments[index + 1]),
            let server = config["mcpServers"]?["nucleic"],
            let urlString = server["url"]?.stringValue,
            let url = URL(string: urlString)
        else {
            die(5, "no usable --mcp-config in argv")
        }
        let authorization = server["headers"]?["Authorization"]?.stringValue ?? ""

        func post(_ body: JSONValue) async -> (status: Int, body: JSONValue?) {
            var request = URLRequest(url: url)
            request.httpMethod = "POST"
            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
            request.setValue("application/json, text/event-stream", forHTTPHeaderField: "Accept")
            request.setValue(authorization, forHTTPHeaderField: "Authorization")
            request.httpBody = try? body.encodedData()
            request.timeoutInterval = 30
            guard let (data, response) = try? await URLSession.shared.data(for: request),
                let http = response as? HTTPURLResponse
            else {
                die(5, "MCP HTTP request failed")
            }
            return (http.statusCode, try? JSONValue(parsing: data))
        }

        // Real-client shape: initialize → initialized notification → tools/list → tools/call.
        let initialize = await post(.object([
            "jsonrpc": .string("2.0"), "id": .number(1), "method": .string("initialize"),
            "params": .object([
                "protocolVersion": .string("2025-06-18"),
                "capabilities": .object([:]),
                "clientInfo": .object(["name": .string("fake-claude"), "version": .string("0.1")]),
            ]),
        ]))
        guard initialize.status == 200, initialize.body?["result"] != nil else {
            die(5, "initialize failed: \(initialize.status)")
        }

        let initialized = await post(.object([
            "jsonrpc": .string("2.0"), "method": .string("notifications/initialized"),
        ]))
        guard initialized.status == 202 else {
            die(5, "initialized notification expected 202, got \(initialized.status)")
        }

        let toolsList = await post(.object([
            "jsonrpc": .string("2.0"), "id": .number(2), "method": .string("tools/list"),
        ]))
        let toolNames = toolsList.body?["result"]?["tools"]?.arrayValue?
            .compactMap { $0["name"]?.stringValue } ?? []
        guard toolNames.contains("approve") else {
            die(5, "tools/list does not advertise approve: \(toolNames)")
        }

        var callArguments: [String: JSONValue] = [
            "tool_name": directive["tool_name"] ?? .string("unknown"),
            "input": directive["input"] ?? .object([:]),
        ]
        if let toolUseID = directive["tool_use_id"] {
            callArguments["tool_use_id"] = toolUseID
        }
        let call = await post(.object([
            "jsonrpc": .string("2.0"), "id": .number(3), "method": .string("tools/call"),
            "params": .object([
                "name": .string("approve"),
                "arguments": .object(callArguments),
            ]),
        ]))
        guard call.status == 200,
            let text = call.body?["result"]?["content"]?[0]?["text"]?.stringValue,
            let reply = try? JSONValue(parsing: text),
            let behavior = reply["behavior"]?.stringValue
        else {
            die(4, "tools/call reply not in the confirmed {behavior,…} shape")
        }

        // The confirmed contract: allow must echo updatedInput; deny must carry message.
        if behavior == "allow", reply["updatedInput"] == nil {
            die(4, "allow reply missing updatedInput")
        }
        if behavior == "deny", reply["message"]?.stringValue == nil {
            die(4, "deny reply missing message")
        }
        if let expected = directive["expect"]?.stringValue, behavior != expected {
            die(4, "expected behavior \(expected), got \(behavior)")
        }
    }
}
