//
//  CLIServerService.swift
//  Confab
//
//  Local Unix-socket server for one-shot shell/CLI access to a single fixed model
//
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
// Copyright (C) 2026 Rune Olsen
//
// This file is part of Confab.
//
// Confab is licensed under the PolyForm Noncommercial License 1.0.0.
// You may use, study, modify, and share it for any noncommercial
// purpose. Commercial use — including selling Confab or any part of
// it, standalone or bundled into another product or service —
// requires a separate commercial license from the copyright holder.
//
// See the LICENSE file or
// <https://polyformproject.org/licenses/noncommercial/1.0.0> for
// the full license text. For commercial licensing, contact Rune
// Olsen via <https://confab.no>.


import Foundation
import Network
import os

/// Local Unix-domain-socket server exposing one-shot, non-streaming text completions from a
/// single fixed model (Settings → MCP → CLI Access) — e.g. an `ai "prompt"` shell function.
/// Deliberately minimal for v1: no tool-calling, no streaming, no per-request model override.
/// Speaks a minimal HTTP/1.1 subset (just enough for `curl --unix-socket`) rather than a custom
/// protocol, so it stays curl-friendly and trivially extensible — new fields can be added to the
/// JSON request/response bodies later without changing the transport.
final class CLIServerService {
    static let shared = CLIServerService()
    private init() {}

    private let log = Logger(subsystem: Log.subsystem, category: "cli")

    /// All listener/connection callbacks run on this single serial queue, so `activeConnections`
    /// never needs a lock — Network.framework callbacks don't run on the main thread.
    private let queue = DispatchQueue(label: "com.oai.Confab.cliserver")
    private var listener: NWListener?
    private var activeConnections: [ObjectIdentifier: NWConnection] = [:]

    static let socketPath: String = {
        (("~/Library/Application Support/oAI" as NSString).expandingTildeInPath as NSString)
            .appendingPathComponent("cli.sock")
    }()

    // MARK: - Lifecycle

    func start() {
        guard SettingsService.shared.cliServerEnabled else {
            log.info("CLI server disabled")
            return
        }
        startListening()
    }

    func stop() {
        listener?.cancel()
        listener = nil
        for (_, connection) in activeConnections { connection.cancel() }
        activeConnections.removeAll()
        try? FileManager.default.removeItem(atPath: Self.socketPath)
        log.info("CLI server stopped")
    }

    /// Call after toggling cliServerEnabled or changing the provider/model, so the change takes
    /// effect immediately instead of requiring an app relaunch.
    func restart() {
        stop()
        start()
    }

    private func startListening() {
        // Remove a stale socket file left behind by an unclean shutdown (bind() fails on an
        // existing path even if nothing is listening on it anymore).
        try? FileManager.default.removeItem(atPath: Self.socketPath)

        let params = NWParameters()
        params.defaultProtocolStack.transportProtocol = NWProtocolTCP.Options()
        params.requiredLocalEndpoint = NWEndpoint.unix(path: Self.socketPath)
        params.allowLocalEndpointReuse = true

        do {
            let newListener = try NWListener(using: params)
            newListener.newConnectionHandler = { [weak self] connection in
                self?.handle(connection: connection)
            }
            newListener.stateUpdateHandler = { [weak self] state in
                switch state {
                case .ready:
                    self?.log.info("CLI server listening at \(Self.socketPath)")
                case .failed(let error):
                    self?.log.error("CLI server listener failed: \(error.localizedDescription)")
                default:
                    break
                }
            }
            newListener.start(queue: queue)
            listener = newListener
        } catch {
            log.error("Failed to start CLI server: \(error.localizedDescription)")
        }
    }

    // MARK: - Connection handling

    private func handle(connection: NWConnection) {
        let id = ObjectIdentifier(connection)
        activeConnections[id] = connection
        connection.stateUpdateHandler = { [weak self] state in
            switch state {
            case .failed, .cancelled:
                self?.activeConnections.removeValue(forKey: id)
            default:
                break
            }
        }
        connection.start(queue: queue)
        readRequest(on: connection, buffer: Data())
    }

    private func readRequest(on connection: NWConnection, buffer: Data) {
        connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { [weak self] data, _, isComplete, error in
            guard let self else { return }
            var buffer = buffer
            if let data { buffer.append(data) }

            if let body = Self.parseRequestBody(from: buffer) {
                Task {
                    let responseData = await self.processRequest(body)
                    self.sendAndClose(responseData, on: connection)
                }
                return
            }

            if isComplete || error != nil {
                connection.cancel()
                return
            }

            self.readRequest(on: connection, buffer: buffer)
        }
    }

    private func sendAndClose(_ data: Data, on connection: NWConnection) {
        let id = ObjectIdentifier(connection)
        connection.send(content: data, completion: .contentProcessed { [weak self] _ in
            connection.cancel()
            self?.activeConnections.removeValue(forKey: id)
        })
    }

    // MARK: - Request processing

    private struct AskRequest: Decodable {
        let prompt: String
    }

    private func processRequest(_ body: Data) async -> Data {
        guard let req = try? JSONDecoder().decode(AskRequest.self, from: body),
              !req.prompt.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
            return Self.errorResponse("Request body must be JSON: {\"prompt\": \"...\"}")
        }

        let settings = SettingsService.shared
        guard settings.cliServerEnabled else {
            return Self.errorResponse("CLI server is disabled in Settings")
        }
        guard let providerType = Settings.Provider(rawValue: settings.cliServerProvider),
              let provider = ProviderRegistry.shared.getProvider(for: providerType) else {
            return Self.errorResponse("No provider configured — set one in Settings > MCP > CLI Access")
        }
        guard !settings.cliServerModel.isEmpty else {
            return Self.errorResponse("No model configured — set one in Settings > MCP > CLI Access")
        }

        do {
            let request = ChatRequest(
                messages: [Message(role: .user, content: req.prompt)],
                model: settings.cliServerModel,
                stream: false
            )
            let response = try await provider.chat(request: request)
            return Self.successResponse(response.content)
        } catch {
            log.error("CLI request failed: \(error.localizedDescription)")
            return Self.errorResponse(error.localizedDescription, statusCode: 500)
        }
    }

    // MARK: - Pure helpers (unit tested — see CLIServerServiceTests.swift)

    struct AskResponseBody: Codable {
        let response: String?
        let error: String?
    }

    /// Parses a minimal HTTP/1.1 request out of `buffer`, returning the body once the full
    /// header block and (per Content-Length) body have arrived, or nil if more data is needed.
    /// Not a general-purpose HTTP parser — this server only ever talks to `curl --unix-socket`,
    /// so the method/path/most headers are read past rather than validated.
    static func parseRequestBody(from buffer: Data) -> Data? {
        let headerTerminator = Data("\r\n\r\n".utf8)
        guard let headerEndRange = buffer.range(of: headerTerminator) else { return nil }

        let headerData = buffer.subdata(in: buffer.startIndex..<headerEndRange.lowerBound)
        guard let headerString = String(data: headerData, encoding: .utf8) else { return nil }

        var contentLength = 0
        for line in headerString.split(separator: "\r\n") {
            let parts = line.split(separator: ":", maxSplits: 1)
            guard parts.count == 2,
                  parts[0].trimmingCharacters(in: .whitespaces).caseInsensitiveCompare("Content-Length") == .orderedSame,
                  let value = Int(parts[1].trimmingCharacters(in: .whitespaces)) else { continue }
            contentLength = value
        }

        let bodyStart = headerEndRange.upperBound
        let availableBodyLength = buffer.distance(from: bodyStart, to: buffer.endIndex)
        guard availableBodyLength >= contentLength else { return nil }

        let bodyEnd = buffer.index(bodyStart, offsetBy: contentLength)
        return buffer.subdata(in: bodyStart..<bodyEnd)
    }

    static func makeHTTPResponse(statusCode: Int, statusText: String, jsonBody: Data) -> Data {
        let header = "HTTP/1.1 \(statusCode) \(statusText)\r\nContent-Type: application/json\r\nContent-Length: \(jsonBody.count)\r\nConnection: close\r\n\r\n"
        var data = Data(header.utf8)
        data.append(jsonBody)
        return data
    }

    static func successResponse(_ text: String) -> Data {
        let body = (try? JSONEncoder().encode(AskResponseBody(response: text, error: nil))) ?? Data()
        return makeHTTPResponse(statusCode: 200, statusText: "OK", jsonBody: body)
    }

    static func errorResponse(_ message: String, statusCode: Int = 400) -> Data {
        let body = (try? JSONEncoder().encode(AskResponseBody(response: nil, error: message))) ?? Data()
        let statusText = statusCode == 400 ? "Bad Request" : "Internal Server Error"
        return makeHTTPResponse(statusCode: statusCode, statusText: statusText, jsonBody: body)
    }
}
