//
// Copyright (c) Double Symmetry GmbH
// Commercial use requires a license. See https://rntp.dev/pricing
//

import Foundation
import Network

/// A minimal HTTP server for integration tests. Serves one or more resources,
/// dispatched by request path, with configurable response headers — e.g. with
/// or without `Content-Length`. Used both as a single-file audio origin and as
/// a multi-path HLS origin (manifest + segments).
final class LocalAudioServer {

  struct Options {
    /// When `false`, the `Content-Length` header is omitted from the response.
    var includeContentLength: Bool = true
    /// MIME type for the Content-Type header.
    var contentType: String = "audio/wav"
    /// If true, sends the response body in small chunks with delays to simulate slow network.
    var simulateSlowNetwork: Bool = false
  }

  /// A single served resource, addressed by path.
  struct Route {
    let data: Data
    var contentType: String = "audio/wav"
    var includeContentLength: Bool = true

    init(data: Data, contentType: String = "audio/wav", includeContentLength: Bool = true) {
      self.data = data
      self.contentType = contentType
      self.includeContentLength = includeContentLength
    }
  }

  private let listener: NWListener
  private let routes: [String: Route]
  private let simulateSlowNetwork: Bool
  private var connections: [NWConnection] = []

  /// The `http://localhost:<port>/audio.wav` URL for the single-file server.
  var url: URL { url(forPath: "/audio.wav") }

  /// The `http://localhost:<port><path>` URL for a served route.
  func url(forPath path: String) -> URL {
    URL(string: "http://localhost:\(listener.port!.rawValue)\(path)")!
  }

  /// Single-file server (serves `audioData` at `/audio.wav`).
  convenience init(audioData: Data, options: Options = Options()) throws {
    try self.init(
      routes: [
        "/audio.wav": Route(
          data: audioData,
          contentType: options.contentType,
          includeContentLength: options.includeContentLength
        )
      ],
      simulateSlowNetwork: options.simulateSlowNetwork
    )
  }

  /// Multi-path server: dispatches responses by request path.
  init(routes: [String: Route], simulateSlowNetwork: Bool = false) throws {
    self.routes = routes
    self.simulateSlowNetwork = simulateSlowNetwork

    let params = NWParameters.tcp
    listener = try NWListener(using: params, on: .any)
  }

  func start() {
    let ready = DispatchSemaphore(value: 0)
    listener.stateUpdateHandler = { state in
      if case .ready = state { ready.signal() }
    }
    listener.newConnectionHandler = { [weak self] conn in
      self?.handleConnection(conn)
    }
    listener.start(queue: .global(qos: .utility))
    ready.wait()
  }

  func stop() {
    for conn in connections { conn.cancel() }
    connections.removeAll()
    listener.cancel()
  }

  // MARK: - Private

  private func handleConnection(_ connection: NWConnection) {
    connections.append(connection)
    connection.start(queue: .global(qos: .utility))

    // Read the full HTTP request (up to 8 KB is plenty for test requests).
    connection.receive(minimumIncompleteLength: 1, maximumLength: 8192) { [weak self] data, _, _, _ in
      guard let self = self, let data = data else { return }
      let (path, rangeStart) = self.parseRequest(data)
      self.sendResponse(on: connection, path: path, rangeStart: rangeStart)
    }
  }

  /// Parses the request path (query stripped) and an optional `Range: bytes=N-`
  /// start offset from the raw HTTP request data.
  private func parseRequest(_ requestData: Data) -> (path: String, rangeStart: Int?) {
    guard let requestString = String(data: requestData, encoding: .utf8) else { return ("/", nil) }
    let lines = requestString.components(separatedBy: "\r\n")

    var path = "/"
    if let requestLine = lines.first {
      let parts = requestLine.split(separator: " ")
      if parts.count >= 2 {
        let rawPath = String(parts[1])
        path = rawPath.split(separator: "?", maxSplits: 1).first.map(String.init) ?? rawPath
      }
    }

    var rangeStart: Int?
    for line in lines {
      let lower = line.lowercased()
      guard lower.hasPrefix("range:") else { continue }
      let value = line.dropFirst("range:".count).trimmingCharacters(in: .whitespaces)
      guard value.hasPrefix("bytes=") else { break }
      let byteRange = value.dropFirst("bytes=".count)
      let startString = byteRange.components(separatedBy: "-").first ?? ""
      rangeStart = Int(startString)
      break
    }

    return (path, rangeStart)
  }

  private func sendResponse(on connection: NWConnection, path: String, rangeStart: Int?) {
    guard let route = routes[path] else {
      let header = "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
      connection.send(content: Data(header.utf8), completion: .contentProcessed { _ in
        connection.cancel()
      })
      return
    }

    let audioData = route.data

    if let start = rangeStart {
      // Partial content response (HTTP 206).
      let slice = audioData.suffix(from: min(start, audioData.count))
      let end = audioData.count - 1
      var header = "HTTP/1.1 206 Partial Content\r\n"
      header += "Content-Type: \(route.contentType)\r\n"
      header += "Content-Range: bytes \(start)-\(end)/\(audioData.count)\r\n"
      header += "Content-Length: \(slice.count)\r\n"
      header += "Connection: close\r\n"
      header += "\r\n"

      var response = Data(header.utf8)
      response.append(slice)

      connection.send(content: response, completion: .contentProcessed { _ in
        connection.cancel()
      })
    } else if route.includeContentLength {
      var header = "HTTP/1.1 200 OK\r\n"
      header += "Content-Type: \(route.contentType)\r\n"
      header += "Content-Length: \(audioData.count)\r\n"
      header += "Connection: close\r\n"
      header += "\r\n"

      if simulateSlowNetwork {
        // Send headers first, then body in chunks with delays.
        connection.send(content: Data(header.utf8), completion: .contentProcessed { _ in })
        sendSlowly(audioData, on: connection)
      } else {
        var response = Data(header.utf8)
        response.append(audioData)
        connection.send(content: response, completion: .contentProcessed { _ in
          connection.cancel()
        })
      }
    } else {
      // Use chunked transfer encoding so AVPlayer can determine
      // body boundaries without a Content-Length header.
      var header = "HTTP/1.1 200 OK\r\n"
      header += "Content-Type: \(route.contentType)\r\n"
      header += "Transfer-Encoding: chunked\r\n"
      header += "\r\n"

      var response = Data(header.utf8)
      // Single chunk: hex size + CRLF + data + CRLF
      response.append(Data(String(audioData.count, radix: 16).utf8))
      response.append(Data("\r\n".utf8))
      response.append(audioData)
      response.append(Data("\r\n".utf8))
      // Final zero-length chunk
      response.append(Data("0\r\n\r\n".utf8))

      connection.send(content: response, completion: .contentProcessed { _ in
        connection.cancel()
      })
    }
  }

  /// Sends data in 64KB chunks with 10ms delays between each, then closes.
  private func sendSlowly(_ data: Data, on connection: NWConnection) {
    let chunkSize = 64 * 1024
    var offset = 0

    func sendNext() {
      guard offset < data.count else {
        connection.cancel()
        return
      }
      let end = min(offset + chunkSize, data.count)
      let chunk = data[offset..<end]
      offset = end
      connection.send(content: chunk, completion: .contentProcessed { _ in
        DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(10)) {
          sendNext()
        }
      })
    }

    sendNext()
  }
}
