//===--- PotentialCrasher.swift -------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2026 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import Foundation

/// A potential crasher input with a given set of options to use.
struct PotentialCrasher: Hashable {
  var path: AbsolutePath
  var options: Reproducer.Options
  var buffers: [Buffer]
  var annotatedBuffers: [Buffer]

  init(_ file: ReproducerFile) {
    self.path = file.path
    self.options = file.reproducer.options
    self.buffers = file.reproducer.buffers
    self.annotatedBuffers = buffers
  }

  private init(
    path: AbsolutePath, options: Reproducer.Options, buffers: [Buffer],
    annotatedBuffers: [Buffer]? = nil
  ) {
    self.path = path
    self.options = options
    self.buffers = buffers
    self.annotatedBuffers = annotatedBuffers ?? buffers
  }

  init(
    _ input: FuzzerInput, options: Reproducer.Options,
    annotatedBuffers: [Buffer]? = nil
  ) {
    var extraArgs = input.header.extraArgs
    // -typecheck doesn't support emitting modules + completion doesn't support
    // any auxiliary outputs.
    if options.kind == .typecheck || options.kind == .complete {
      for i in extraArgs.indices where i + 1 < extraArgs.count {
        guard let opt = extraArgs[i].value else { continue }
        if options.kind == .typecheck,
           opt.starts(with: "-emit-module") ||
            opt.starts(with: "-emit-private-module") {
          extraArgs.removeSubrange(i ... (i + 1))
        } else if options.kind == .complete, opt.starts(with: "-emit") {
          extraArgs.removeSubrange(i ... (i + 1))
        }
      }
    }
    self.init(
      path: input.path, options: options.withExtraArgs(extraArgs),
      buffers: input.buffers, annotatedBuffers: annotatedBuffers
    )
  }

  static func typecheck(_ input: FuzzerInput) -> Self {
    .init(input, options: .typecheck)
  }

  static func emitSILGen(_ input: FuzzerInput) -> Self {
    .init(input, options: .emitSILGen)
  }

  static func emitSIL(_ input: FuzzerInput) -> Self {
    .init(input, options: .emitSIL)
  }

  static func emitIR(_ input: FuzzerInput) -> Self {
    .init(input, options: .emitIR)
  }

  static func completion(_ input: FuzzerInput) throws -> Self? {
    let reqs = input.header.sourceKitRequests.sorted(by: \.offset)
      .filter { $0.kind == .complete }
    var buffers = input.buffers
    if reqs.isEmpty {
      if buffers.contains(where: { $0.code.text.contains(#"#^"#)} ) {
        return Self(input, options: .complete)
      }
      return nil
    }
    if reqs.contains(where: { $0.fileIdx >= buffers.count }) {
      throw ReproducerError("""
        completion in \(input) exceeds number of buffers
        """)
    }
    let annotatedBuffers = try buffers.enumerated().map { bufferIdx, buffer in
      var buffer = buffer
      var bytes = Array(buffer.code.text.replacing("#^", with: "# ").utf8)
      var preOffset = 0
      for (i, req) in reqs.enumerated() where req.fileIdx == bufferIdx {
        let tok = "#^COMPLETE\(i + 1)^#".utf8
        let offset = req.offset + preOffset
        guard offset <= bytes.count else {
          throw ReproducerError("""
            completion at \(offset) exceeded \(bytes.count) for \(input)
            """)
        }
        bytes.insert(contentsOf: tok, at: offset)
        preOffset += tok.count
      }
      buffer.code = Code(String(decoding: bytes, as: UTF8.self))
      return buffer
    }
    if buffers.count == 1 {
      buffers = annotatedBuffers
    }
    var input = input
    input.buffers = buffers
    return Self(input, options: .complete, annotatedBuffers: annotatedBuffers)
  }

  static func custom(
    _ input: FuzzerInput, frontendArgs: [Command.Argument]
  ) -> Self {
    .init(input, options: .custom(frontendArgs: frontendArgs))
  }

  var withGuardMalloc: Self {
    var result = self
    result.options.useGuardMalloc = true
    return result
  }

  func withSourceOrderCompletion(_ value: Bool = true) -> Self {
    var result = self
    result.options.useSourceOrderCompletion = value
    return result
  }

  var withNoSDK: Self {
    var result = self
    result.options.noSDK = true
    return result
  }

  var withNoObjCInterop: Self {
    var result = self
    result.options.noObjCInterop = true
    return result
  }

  func withSolverLimits(_ value: Bool = true) -> Self {
    var result = self
    result.options.withSolverLimits = value
    return result
  }

  func withExtraArgs(_ extraArgs: [Command.Argument]) -> Self {
    var result = self
    result.options = result.options.withExtraArgs(extraArgs)
    return result
  }

  var hasSolverLimits: Bool {
    options.withSolverLimits
  }

  func withDeterministic(_ value: Bool = true) -> Self {
    var result = self
    result.options.isDeterministic = value
    return result
  }

  func withPrimaryIdx(_ idx: Int) -> Self {
    var result = self
    result.options.primaryIdx = idx
    result.buffers[idx] = result.annotatedBuffers[idx]
    return result
  }

  func withJoinedBuffers() -> Self {
    var result = self
    result.options.isJoined = true
    result.options.primaryIdx = nil
    let code = Code(annotatedBuffers.map { $0.code.text }.joined(separator: "\n"))
    result.buffers = [Buffer(name: annotatedBuffers[0].name, code: code)]
    result.annotatedBuffers = result.buffers
    return result
  }

  private static let idRegex = #/crash-([a-fA-F0-9]{8})/#

  var id: String? {
    guard let match = path.fileName.firstMatch(of: Self.idRegex) else {
      return nil
    }
    return String(match.output.1)
  }
}

extension PotentialCrasher: CustomStringConvertible {
  var description: String {
    "(\(options)) \(path.fileName)"
  }
}

extension PotentialCrasher {
  func withInputFiles<T>(
    _ body: ([AbsolutePath]) async throws -> T
  ) async throws -> T {
    try await withTemporaryDirectory(prefix: path.fileName) { dir in
      var files: [AbsolutePath] = []
      for buffer in buffers {
        let file = dir.appending(buffer.name)
        files.append(file)
        try file.write(buffer.code.text)
      }
      return try await body(files)
    }
  }
}

struct Crasher {
  var input: PotentialCrasher
  var crashInfo: CrashInfo

  var primaryCrash: CrashLog {
    crashInfo.primary
  }
  var primarySig: Signature {
    crashInfo.primarySig
  }

  var signatures: KnownSignatures { crashInfo.signatures }
  var path: AbsolutePath { input.path }
  var kind: Reproducer.Options { input.options }
  var buffers: [Buffer] { input.buffers }
}

extension Crasher {
  func withDeterministic(_ value: Bool) -> Self {
    Self(input: input.withDeterministic(value), crashInfo: crashInfo)
  }
  func withSourceOrderCompletion(_ value: Bool) -> Self {
    Self(input: input.withSourceOrderCompletion(value), crashInfo: crashInfo)
  }
}

extension Crasher: CustomStringConvertible {
  var description: String {
    "\(signatures) | \(input)"
  }
}
