import CryptoKit
import Darwin
import Foundation

private let maximumCapturedBytes = 64 * 1024

private struct ExecutionRecord: Encodable {
    let schemaVersion: String
    let binarySha256: String
    let binaryIdentityMatched: Bool
    let argvFingerprintSha256: String
    let startedAt: String
    let endedAt: String
    let exitCode: Int32?
    let terminationSignal: Int32?
    let stdout: String
    let stderr: String
    let outputTruncated: Bool
    let sessionIdentity: String
    let cellIdentity: String
    let launchStatus: String
    let snapshotCleanupStatus: String

    enum CodingKeys: String, CodingKey {
        case schemaVersion, binarySha256, binaryIdentityMatched, argvFingerprintSha256
        case startedAt, endedAt, exitCode, terminationSignal, stdout, stderr
        case outputTruncated, sessionIdentity, cellIdentity, launchStatus, snapshotCleanupStatus
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(schemaVersion, forKey: .schemaVersion)
        try container.encode(binarySha256, forKey: .binarySha256)
        try container.encode(binaryIdentityMatched, forKey: .binaryIdentityMatched)
        try container.encode(argvFingerprintSha256, forKey: .argvFingerprintSha256)
        try container.encode(startedAt, forKey: .startedAt)
        try container.encode(endedAt, forKey: .endedAt)
        if let exitCode { try container.encode(exitCode, forKey: .exitCode) }
        else { try container.encodeNil(forKey: .exitCode) }
        if let terminationSignal { try container.encode(terminationSignal, forKey: .terminationSignal) }
        else { try container.encodeNil(forKey: .terminationSignal) }
        try container.encode(stdout, forKey: .stdout)
        try container.encode(stderr, forKey: .stderr)
        try container.encode(outputTruncated, forKey: .outputTruncated)
        try container.encode(sessionIdentity, forKey: .sessionIdentity)
        try container.encode(cellIdentity, forKey: .cellIdentity)
        try container.encode(launchStatus, forKey: .launchStatus)
        try container.encode(snapshotCleanupStatus, forKey: .snapshotCleanupStatus)
    }
}

private func sha256(_ data: Data) -> String {
    SHA256.hash(data: data).map { String(format: "%02x", $0) }.joined()
}

private func fileSha256(_ url: URL) throws -> String {
    let handle = try FileHandle(forReadingFrom: url)
    defer { try? handle.close() }
    var digest = SHA256()
    while true {
        let data = try handle.read(upToCount: 64 * 1024) ?? Data()
        if data.isEmpty { break }
        digest.update(data: data)
    }
    return digest.finalize().map { String(format: "%02x", $0) }.joined()
}

private struct ExecutableSnapshot {
    let directoryURL: URL
    let executableURL: URL
    let sha256: String
    let handle: FileHandle
}

private struct SnapshotFailure: Error {
    let stage: String
    let cleanupFailure: String?
}

private func sha256(_ handle: FileHandle) throws -> String {
    try handle.seek(toOffset: 0)
    var digest = SHA256()
    while true {
        let data = try handle.read(upToCount: 64 * 1024) ?? Data()
        if data.isEmpty { break }
        digest.update(data: data)
    }
    return digest.finalize().map { String(format: "%02x", $0) }.joined()
}

private func sameObject(_ left: stat, _ right: stat) -> Bool {
    left.st_dev == right.st_dev && left.st_ino == right.st_ino
}

private func cleanupFailedSnapshot(
    handle: FileHandle?, executableURL: URL?, directoryURL: URL?
) -> String? {
    var failures: [String] = []
    if let handle {
        if fchflags(handle.fileDescriptor, 0) != 0 { failures.append("bound-immutable-flag") }
        try? handle.close()
    }
    if let executableURL {
        _ = chflags(executableURL.path, 0)
        do { try FileManager.default.removeItem(at: executableURL) }
        catch where (error as NSError).code != NSFileNoSuchFileError { failures.append("executable") }
        catch {}
    }
    if let directoryURL {
        do { try FileManager.default.removeItem(at: directoryURL) }
        catch where (error as NSError).code != NSFileNoSuchFileError { failures.append("directory") }
        catch {}
    }
    return failures.isEmpty ? nil : "SNAPSHOT_CLEANUP_FAILED:\(failures.joined(separator: ","))"
}

private func createExecutableSnapshot(from sourceURL: URL) throws -> ExecutableSnapshot {
    #if EVIDENCE_RUNNER_TESTING
    if ProcessInfo.processInfo.environment["MINDRAW_EVIDENCE_TEST_FAIL_SNAPSHOT_CREATE"] == "1" {
        throw SnapshotFailure(stage: "SNAPSHOT_CREATE_FAILED", cleanupFailure: nil)
    }
    #endif
    let manager = FileManager.default
    let directoryURL = manager.temporaryDirectory
        .appendingPathComponent("mindraw-evidence-exec-\(UUID().uuidString)", isDirectory: true)
    do {
        try manager.createDirectory(
            at: directoryURL,
            withIntermediateDirectories: false,
            attributes: [.posixPermissions: 0o700]
        )
    } catch {
        throw SnapshotFailure(stage: "SNAPSHOT_CREATE_FAILED", cleanupFailure: nil)
    }
    var directoryStatus = stat()
    guard lstat(directoryURL.path, &directoryStatus) == 0,
          (directoryStatus.st_mode & S_IFMT) == S_IFDIR,
          directoryStatus.st_mode & 0o777 == 0o700 else {
        try? manager.removeItem(at: directoryURL)
        throw SnapshotFailure(stage: "SNAPSHOT_CREATE_FAILED", cleanupFailure: nil)
    }

    let executableURL = directoryURL.appendingPathComponent("verified-tool", isDirectory: false)
    let descriptor = open(executableURL.path, O_RDWR | O_CREAT | O_EXCL | O_NOFOLLOW, 0o600)
    guard descriptor >= 0 else {
        try? manager.removeItem(at: directoryURL)
        throw SnapshotFailure(stage: "SNAPSHOT_CREATE_FAILED", cleanupFailure: nil)
    }
    let output = FileHandle(fileDescriptor: descriptor, closeOnDealloc: true)
    do {
        let input = try FileHandle(forReadingFrom: sourceURL)
        defer { try? input.close() }
        var digest = SHA256()
        while true {
            let data = try input.read(upToCount: 64 * 1024) ?? Data()
            if data.isEmpty { break }
            digest.update(data: data)
            try output.write(contentsOf: data)
        }
        try output.synchronize()
        let copiedSha = digest.finalize().map { String(format: "%02x", $0) }.joined()
        var createdStatus = stat()
        guard fstat(descriptor, &createdStatus) == 0,
              (createdStatus.st_mode & S_IFMT) == S_IFREG,
              fchmod(descriptor, 0o500) == 0 else {
            throw SnapshotFailure(stage: "SNAPSHOT_VERIFY_FAILED", cleanupFailure: nil)
        }
        #if EVIDENCE_RUNNER_TESTING
        if ProcessInfo.processInfo.environment["MINDRAW_EVIDENCE_TEST_FAIL_SNAPSHOT_VERIFY"] == "1" {
            throw SnapshotFailure(stage: "SNAPSHOT_VERIFY_FAILED", cleanupFailure: nil)
        }
        guard try sha256(output) == copiedSha else {
            throw SnapshotFailure(stage: "SNAPSHOT_VERIFY_FAILED", cleanupFailure: nil)
        }
        if let replacement = ProcessInfo.processInfo.environment["MINDRAW_EVIDENCE_TEST_REPLACE_SNAPSHOT_WITH"] {
            try manager.removeItem(at: executableURL)
            try manager.copyItem(at: URL(fileURLWithPath: replacement), to: executableURL)
            try manager.setAttributes([.posixPermissions: 0o500], ofItemAtPath: executableURL.path)
        }
        #endif
        guard fchflags(descriptor, UInt32(UF_IMMUTABLE)) == 0 else {
            throw SnapshotFailure(stage: "SNAPSHOT_HARDENING_FAILED", cleanupFailure: nil)
        }
        var boundStatus = stat()
        var pathStatus = stat()
        guard fstat(descriptor, &boundStatus) == 0,
              lstat(executableURL.path, &pathStatus) == 0,
              sameObject(createdStatus, boundStatus), sameObject(boundStatus, pathStatus) else {
            throw SnapshotFailure(stage: "SNAPSHOT_OBJECT_REPLACED", cleanupFailure: nil)
        }
        guard (boundStatus.st_mode & S_IFMT) == S_IFREG,
              boundStatus.st_mode & 0o777 == 0o500,
              boundStatus.st_flags & UInt32(UF_IMMUTABLE) != 0,
              try sha256(output) == copiedSha else {
            throw SnapshotFailure(stage: "SNAPSHOT_VERIFY_FAILED", cleanupFailure: nil)
        }
        return ExecutableSnapshot(
            directoryURL: directoryURL,
            executableURL: executableURL,
            sha256: copiedSha,
            handle: output
        )
    } catch {
        let cleanupFailure = cleanupFailedSnapshot(
            handle: output, executableURL: executableURL, directoryURL: directoryURL
        )
        if let failure = error as? SnapshotFailure {
            throw SnapshotFailure(stage: failure.stage, cleanupFailure: cleanupFailure)
        }
        throw SnapshotFailure(stage: "SNAPSHOT_VERIFY_FAILED", cleanupFailure: cleanupFailure)
    }
}

private func cleanupExecutableSnapshot(_ snapshot: ExecutableSnapshot) -> String? {
    let manager = FileManager.default
    var failures: [String] = []
    if fchflags(snapshot.handle.fileDescriptor, 0) != 0 {
        failures.append("immutable-flag")
    }
    try? snapshot.handle.close()
    if failures.isEmpty {
        do { try manager.removeItem(at: snapshot.executableURL) }
        catch { failures.append("executable") }
    }
    do { try manager.removeItem(at: snapshot.directoryURL) }
    catch { failures.append("directory") }
    #if EVIDENCE_RUNNER_TESTING
    if ProcessInfo.processInfo.environment["MINDRAW_EVIDENCE_TEST_FAIL_SNAPSHOT_CLEANUP"] == "1" {
        failures.append("injected")
    }
    #endif
    return failures.isEmpty ? nil : "SNAPSHOT_CLEANUP_FAILED:\(failures.joined(separator: ","))"
}

private func validOpaqueIdentity(_ value: String) -> Bool {
    value.range(of: #"^[A-Za-z0-9._-]{1,128}$"#, options: .regularExpression) != nil
}

private func replaceMatches(_ pattern: String, in value: String, with replacement: String) -> String {
    guard let expression = try? NSRegularExpression(pattern: pattern) else { return value }
    let range = NSRange(value.startIndex..<value.endIndex, in: value)
    return expression.stringByReplacingMatches(in: value, range: range, withTemplate: replacement)
}

private func sanitizeOutput(_ data: Data, statePath: String, sourceIdentifier: String) -> (String, Bool) {
    let truncated = data.count > maximumCapturedBytes
    let bounded = data.prefix(maximumCapturedBytes)
    var value = String(decoding: bounded, as: UTF8.self)
    value = value.replacingOccurrences(of: statePath, with: "<STATE_PATH>")
    value = value.replacingOccurrences(of: sourceIdentifier, with: "<SOURCE_IDENTIFIER>")
    value = replaceMatches(#"/(?:Users|private|var|tmp)/[^\s]+"#, in: value, with: "<PRIVATE_PATH>")
    value = replaceMatches(#"(?i)\bpid=\d+"#, in: value, with: "pid=<PID>")
    value = replaceMatches(#"(?i)\bwindow(?:_?id)?=\d+"#, in: value, with: "window=<WINDOW_ID>")
    if truncated { value += "<OUTPUT_TRUNCATED>\n" }
    return (value, truncated)
}

private func argvFingerprint(binarySha: String, mode: String) -> String {
    sha256(Data("\(binarySha)|\(mode)|<STATE_PATH>|<SOURCE_IDENTIFIER>".utf8))
}

private func writeRecord(_ record: ExecutionRecord, to url: URL) throws {
    let encoder = JSONEncoder()
    encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
    let data = try encoder.encode(record)
    try data.write(to: url, options: .atomic)
    let recordSha = Data("\(sha256(data))\n".utf8)
    try recordSha.write(to: URL(fileURLWithPath: url.path + ".sha256"), options: .atomic)
}

private final class BoundedCapture {
    private let lock = NSLock()
    private var data = Data()
    private(set) var truncated = false

    func append(_ incoming: Data) {
        lock.lock()
        defer { lock.unlock() }
        let remaining = max(0, maximumCapturedBytes + 1 - data.count)
        if remaining > 0 { data.append(incoming.prefix(remaining)) }
        if incoming.count > remaining || data.count > maximumCapturedBytes { truncated = true }
    }

    func snapshot() -> Data {
        lock.lock()
        defer { lock.unlock() }
        return data
    }
}

private func run() -> Int32 {
    let arguments = CommandLine.arguments
    guard arguments.count == 10, arguments[1] == "run" else {
        fputs("usage: EvidenceToolRunner run record.json expectedBinarySha sessionIdentity cellIdentity binary isolate|restore state.json sourceIdentifier\n", stderr)
        return 64
    }
    let recordURL = URL(fileURLWithPath: arguments[2])
    let expectedSha = arguments[3]
    let sessionIdentity = arguments[4]
    let cellIdentity = arguments[5]
    let binaryURL = URL(fileURLWithPath: arguments[6])
    let mode = arguments[7]
    let statePath = arguments[8]
    let sourceIdentifier = arguments[9]
    guard expectedSha.range(of: #"^[a-f0-9]{64}$"#, options: .regularExpression) != nil,
          validOpaqueIdentity(sessionIdentity), validOpaqueIdentity(cellIdentity),
          ["isolate", "restore"].contains(mode), !statePath.isEmpty, !sourceIdentifier.isEmpty else {
        fputs("invalid evidence runner arguments\n", stderr)
        return 64
    }

    let formatter = ISO8601DateFormatter()
    formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
    let startedAt = formatter.string(from: Date())
    let snapshot: ExecutableSnapshot
    do {
        snapshot = try createExecutableSnapshot(from: binaryURL)
    } catch {
        let failure = error as? SnapshotFailure
        let record = ExecutionRecord(
            schemaVersion: "mindraw.qa-evidence-exec.v1",
            binarySha256: expectedSha,
            binaryIdentityMatched: false,
            argvFingerprintSha256: argvFingerprint(binarySha: expectedSha, mode: mode),
            startedAt: startedAt,
            endedAt: formatter.string(from: Date()),
            exitCode: nil,
            terminationSignal: nil,
            stdout: "",
            stderr: failure?.cleanupFailure.map { $0 + "\n" } ?? "",
            outputTruncated: false,
            sessionIdentity: sessionIdentity,
            cellIdentity: cellIdentity,
            launchStatus: failure?.stage ?? "SNAPSHOT_VERIFY_FAILED",
            snapshotCleanupStatus: failure?.cleanupFailure == nil ? "CLEANUP_COMPLETE" : "CLEANUP_FAILED"
        )
        do { try writeRecord(record, to: recordURL) } catch { return 77 }
        fputs("executable snapshot unavailable\n", stderr)
        return 77
    }
    let actualSha = snapshot.sha256
    let fingerprint = argvFingerprint(binarySha: actualSha, mode: mode)
    if actualSha != expectedSha {
        let cleanupFailure = cleanupExecutableSnapshot(snapshot)
        let endedAt = formatter.string(from: Date())
        let record = ExecutionRecord(
            schemaVersion: "mindraw.qa-evidence-exec.v1",
            binarySha256: actualSha,
            binaryIdentityMatched: false,
            argvFingerprintSha256: fingerprint,
            startedAt: startedAt,
            endedAt: endedAt,
            exitCode: nil,
            terminationSignal: nil,
            stdout: "",
            stderr: cleanupFailure ?? "",
            outputTruncated: false,
            sessionIdentity: sessionIdentity,
            cellIdentity: cellIdentity,
            launchStatus: cleanupFailure == nil ? "BINARY_IDENTITY_MISMATCH" : "SNAPSHOT_CLEANUP_FAILED",
            snapshotCleanupStatus: cleanupFailure == nil ? "CLEANUP_COMPLETE" : "CLEANUP_FAILED"
        )
        do { try writeRecord(record, to: recordURL) } catch { return 77 }
        return cleanupFailure == nil ? 78 : 77
    }

    #if EVIDENCE_RUNNER_TESTING
    if let replacement = ProcessInfo.processInfo.environment["MINDRAW_EVIDENCE_TEST_REPLACE_SOURCE_WITH"] {
        do {
            try FileManager.default.removeItem(at: binaryURL)
            try FileManager.default.copyItem(at: URL(fileURLWithPath: replacement), to: binaryURL)
            try FileManager.default.setAttributes([.posixPermissions: 0o700], ofItemAtPath: binaryURL.path)
        } catch {
            _ = cleanupExecutableSnapshot(snapshot)
            return 77
        }
    }
    #endif

    let process = Process()
    let stdoutPipe = Pipe()
    let stderrPipe = Pipe()
    let stdoutCapture = BoundedCapture()
    let stderrCapture = BoundedCapture()
    process.executableURL = snapshot.executableURL
    process.arguments = [mode, statePath, sourceIdentifier]
    process.standardOutput = stdoutPipe
    process.standardError = stderrPipe
    stdoutPipe.fileHandleForReading.readabilityHandler = { handle in
        let data = handle.availableData
        if !data.isEmpty { stdoutCapture.append(data) }
    }
    stderrPipe.fileHandleForReading.readabilityHandler = { handle in
        let data = handle.availableData
        if !data.isEmpty { stderrCapture.append(data) }
    }
    do {
        try process.run()
    } catch {
        let cleanupFailure = cleanupExecutableSnapshot(snapshot)
        stdoutPipe.fileHandleForReading.readabilityHandler = nil
        stderrPipe.fileHandleForReading.readabilityHandler = nil
        let record = ExecutionRecord(
            schemaVersion: "mindraw.qa-evidence-exec.v1",
            binarySha256: actualSha,
            binaryIdentityMatched: cleanupFailure == nil,
            argvFingerprintSha256: fingerprint,
            startedAt: startedAt,
            endedAt: formatter.string(from: Date()),
            exitCode: nil,
            terminationSignal: nil,
            stdout: "",
            stderr: cleanupFailure.map { $0 + "\n" } ?? "",
            outputTruncated: false,
            sessionIdentity: sessionIdentity,
            cellIdentity: cellIdentity,
            launchStatus: cleanupFailure == nil ? "LAUNCH_FAILED" : "SNAPSHOT_CLEANUP_FAILED",
            snapshotCleanupStatus: cleanupFailure == nil ? "CLEANUP_COMPLETE" : "CLEANUP_FAILED"
        )
        do { try writeRecord(record, to: recordURL) } catch { return 77 }
        return 77
    }
    process.waitUntilExit()
    let cleanupFailure = cleanupExecutableSnapshot(snapshot)
    stdoutPipe.fileHandleForReading.readabilityHandler = nil
    stderrPipe.fileHandleForReading.readabilityHandler = nil
    stdoutCapture.append(stdoutPipe.fileHandleForReading.readDataToEndOfFile())
    stderrCapture.append(stderrPipe.fileHandleForReading.readDataToEndOfFile())

    let (safeStdout, stdoutTruncated) = sanitizeOutput(
        stdoutCapture.snapshot(), statePath: statePath, sourceIdentifier: sourceIdentifier
    )
    let (safeStderr, stderrTruncated) = sanitizeOutput(
        stderrCapture.snapshot(), statePath: statePath, sourceIdentifier: sourceIdentifier
    )
    let signal = process.terminationReason == .uncaughtSignal ? process.terminationStatus : nil
    let exitCode = process.terminationReason == .exit ? process.terminationStatus : nil
    let record = ExecutionRecord(
        schemaVersion: "mindraw.qa-evidence-exec.v1",
        binarySha256: actualSha,
        binaryIdentityMatched: cleanupFailure == nil,
        argvFingerprintSha256: fingerprint,
        startedAt: startedAt,
        endedAt: formatter.string(from: Date()),
        exitCode: exitCode,
        terminationSignal: signal,
        stdout: safeStdout,
        stderr: cleanupFailure.map { safeStderr + $0 + "\n" } ?? safeStderr,
        outputTruncated: stdoutTruncated || stderrTruncated || stdoutCapture.truncated || stderrCapture.truncated,
        sessionIdentity: sessionIdentity,
        cellIdentity: cellIdentity,
        launchStatus: cleanupFailure == nil ? "EXECUTED" : "SNAPSHOT_CLEANUP_FAILED",
        snapshotCleanupStatus: cleanupFailure == nil ? "CLEANUP_COMPLETE" : "CLEANUP_FAILED"
    )
    do { try writeRecord(record, to: recordURL) } catch { return 77 }
    return cleanupFailure == nil ? (exitCode ?? 76) : 77
}

private func selfTest() -> Int32 {
    let state = "/private/tmp/private-state.json"
    let source = "private-source"
    let sanitized = sanitizeOutput(
        Data("/Users/private/name \(state) \(source) pid=123 window_id=456\n".utf8),
        statePath: state,
        sourceIdentifier: source
    ).0
    guard sanitized == "<PRIVATE_PATH> <STATE_PATH> <SOURCE_IDENTIFIER> pid=<PID> window=<WINDOW_ID>\n",
          validOpaqueIdentity("session-a"), !validOpaqueIdentity("session/path"),
          argvFingerprint(binarySha: String(repeating: "a", count: 64), mode: "isolate").count == 64 else {
        return 1
    }
    print("self_test=PASS bounded_output=true private_values_redacted=true identity_mismatch_fail_closed=true verified_snapshot_launch=true")
    return 0
}

if CommandLine.arguments == [CommandLine.arguments[0], "--self-test"] {
    exit(selfTest())
}
exit(run())
