import Foundation

/// Thread-safe String-Accumulator. readabilityHandler werden auf einem
/// background-thread aufgerufen, daher dürfen captured-vars nicht direkt
/// mutiert werden (Swift-6-concurrency-rule).
final class LineBuffer: @unchecked Sendable {
    private var _value = ""
    private let lock = NSLock()
    func append(_ s: String) {
        lock.lock(); defer { lock.unlock() }
        _value += s
    }
    var value: String {
        lock.lock(); defer { lock.unlock() }
        return _value
    }
}

enum ProcessRunnerError: Error, LocalizedError {
    case binaryNotFound(String)
    case nonZeroExit(Int32, String)

    var errorDescription: String? {
        switch self {
        case .binaryNotFound(let path): return "Binary nicht gefunden: \(path)"
        case .nonZeroExit(let code, let out): return "Exit \(code): \(out)"
        }
    }
}

/// Spawnt einen child-process und sammelt stdout+stderr.
/// Streamt zeilenweise via AsyncStream wenn `stream: true` — sonst nur Final-Result.
enum ProcessRunner {
    struct Result {
        let exitCode: Int32
        let stdout: String
        let stderr: String
    }

    static func run(
        _ executable: String,
        arguments: [String] = [],
        environment: [String: String]? = nil,
        currentDirectoryPath: String? = nil
    ) async throws -> Result {
        guard FileManager.default.isExecutableFile(atPath: executable) else {
            throw ProcessRunnerError.binaryNotFound(executable)
        }

        let process = Process()
        process.executableURL = URL(fileURLWithPath: executable)
        process.arguments = arguments
        if let environment = environment {
            process.environment = environment
        }
        if let cwd = currentDirectoryPath {
            process.currentDirectoryURL = URL(fileURLWithPath: cwd)
        }

        let outPipe = Pipe()
        let errPipe = Pipe()
        process.standardOutput = outPipe
        process.standardError = errPipe

        try process.run()
        process.waitUntilExit()

        let stdout = String(data: outPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
        let stderr = String(data: errPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""

        return Result(exitCode: process.terminationStatus, stdout: stdout, stderr: stderr)
    }

    /// Streamt stdout zeilenweise. Caller bekommt jeden Line via onLine-Callback.
    /// stderr wird parallel gesammelt + im Final-Result returned.
    @MainActor
    static func stream(
        _ executable: String,
        arguments: [String] = [],
        environment: [String: String]? = nil,
        onLine: @escaping (String) -> Void
    ) async throws -> Result {
        guard FileManager.default.isExecutableFile(atPath: executable) else {
            throw ProcessRunnerError.binaryNotFound(executable)
        }

        let process = Process()
        process.executableURL = URL(fileURLWithPath: executable)
        process.arguments = arguments
        if let environment = environment {
            process.environment = environment
        }

        let outPipe = Pipe()
        let errPipe = Pipe()
        process.standardOutput = outPipe
        process.standardError = errPipe

        let stdoutBuf = LineBuffer()
        let stderrBuf = LineBuffer()

        let outHandle = outPipe.fileHandleForReading
        let errHandle = errPipe.fileHandleForReading

        outHandle.readabilityHandler = { handle in
            let data = handle.availableData
            guard !data.isEmpty, let chunk = String(data: data, encoding: .utf8) else { return }
            stdoutBuf.append(chunk)
            for line in chunk.split(separator: "\n", omittingEmptySubsequences: false) {
                let s = String(line)
                if !s.isEmpty {
                    Task { @MainActor in onLine(s) }
                }
            }
        }

        errHandle.readabilityHandler = { handle in
            let data = handle.availableData
            guard !data.isEmpty, let chunk = String(data: data, encoding: .utf8) else { return }
            stderrBuf.append(chunk)
            for line in chunk.split(separator: "\n", omittingEmptySubsequences: false) {
                let s = String(line)
                if !s.isEmpty {
                    Task { @MainActor in onLine("[stderr] " + s) }
                }
            }
        }

        try process.run()

        await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
            process.terminationHandler = { _ in
                continuation.resume()
            }
        }

        outHandle.readabilityHandler = nil
        errHandle.readabilityHandler = nil

        return Result(exitCode: process.terminationStatus, stdout: stdoutBuf.value, stderr: stderrBuf.value)
    }
}
