//
//  HybridNitroChucker.swift
//  Pods
//
//  Created by Rishabh Ramola on 6/23/2026.
//

import Foundation
import UIKit
import NitroModules
#if canImport(Wormholy)
import Wormholy
#endif

// Wormholy's presentation observer is normally registered by a C constructor in its ObjC
// module (WormholyConstructor.m). Under static frameworks (useFrameworks: "static") that
// constructor's object file is dead-stripped — it has no ObjC class for `-ObjC` to anchor
// on — so the "wormholy_fire" observer is never installed and show() posts into the void.
// We register it explicitly on first use instead. (The NSURLSessionConfiguration+Wormholy
// category that powers capture IS a category, so `-ObjC` — which this app already sets via
// Firebase — loads it; capture works without our help.)
final class HybridNitroChucker: HybridNitroChuckerSpec {
  override init() {
    super.init()
    Self.ensureWormholyActivated()
  }

  private static var wormholyActivated = false

  /// Mirror of the configured retention cap. Wormholy.limit's getter is hard-coded to
  /// return nil (upstream bug), so the value cannot be read back from Wormholy.
  private static var maxLogCount = 0

  /// Replicates Wormholy's dead-stripped startup constructor: swiftyLoad() registers the
  /// "wormholy_fire" presentation observer; swiftyInitialize() enables capture. Idempotent.
  private static func ensureWormholyActivated() {
    guard !wormholyActivated else { return }
    wormholyActivated = true
    #if canImport(Wormholy)
    Wormholy.swiftyLoad()
    Wormholy.swiftyInitialize()
    #endif
  }

  var isSupported: Bool { true }

  func show() {
    // Wormholy listens for the "wormholy_fire" notification (Config.swift) and presents
    // its SwiftUI RequestsView via presentWormholyFlow(). This is the documented trigger.
    // Dispatch to main: Wormholy presents UIKit synchronously on the posting thread,
    // and Nitro methods are not guaranteed to run on main.
    DispatchQueue.main.async {
      NotificationCenter.default.post(
        name: NSNotification.Name(rawValue: "wormholy_fire"), object: nil
      )
    }
  }

  func dismiss() {
    // Wormholy exposes no public dismiss API; best-effort: dismiss the topmost
    // presented controller on the main thread.
    DispatchQueue.main.async {
      guard let root = Self.keyWindow()?.rootViewController else { return }
      var top = root
      while let presented = top.presentedViewController {
        top = presented
      }
      top.presentingViewController?.dismiss(animated: true)
    }
  }

  func clearLogs() {
    // Wormholy's Storage.clearRequests() is internal (@MainActor) and there is no public
    // clear API or clear notification. Documented best-effort no-op.
    NSLog("NitroChucker: clearLogs() has no public Wormholy API; no-op")
  }

  func setEnabled(enabled: Bool) {
    #if canImport(Wormholy)
    // Real public API (Wormholy 2.4.0): registers/unregisters CustomHTTPProtocol, gating
    // capture on the default URLProtocol path. Does not affect the shake gesture.
    // NitroChuckerCapture.m re-checks isWormholyEnabled() before re-installing the
    // protocol into newly created session configurations, so disabling actually sticks.
    Wormholy.setEnabled(enabled)
    NSLog("NitroChucker: setEnabled(\(enabled)) via Wormholy.setEnabled")
    #else
    NSLog("NitroChucker: setEnabled(\(enabled)) — Wormholy not linked; no-op")
    #endif
  }

  /// Cap retained transactions (FIFO — oldest evicted on each insert). 0 = unlimited.
  ///
  /// Exact on iOS via Wormholy.limit, which Storage enforces on every insert. Two
  /// upstream quirks are handled here: the setter hops to the main actor (so the cap
  /// lands one turn later), and the getter always returns nil (so we mirror the value).
  /// Set this early — Storage removes at most one entry per insert, so lowering the cap
  /// later makes the count plateau rather than shrink. A default of 200 is installed at
  /// image load by NitroChuckerCapture.m so protection does not depend on JS running.
  func setMaxLogCount(maxCount: Double) {
    // Clamp entirely in Double space: Int(_: Double) traps (uncatchable) for anything
    // outside Int64's range, and isFinite alone does not exclude e.g. 1e300, so
    // setMaxLogCount(Number.MAX_VALUE) from JS would abort the process. Wormholy only
    // compares this against requests.count, so an Int32 ceiling loses nothing.
    let bounded = maxCount.isFinite
      ? min(max(maxCount.rounded(), 0), Double(Int32.max))
      : 0
    let normalized = Int(bounded)
    Self.maxLogCount = normalized
    #if canImport(Wormholy)
    Wormholy.limit = normalized == 0 ? nil : NSNumber(value: normalized)
    NSLog(
      "NitroChucker: setMaxLogCount(\(normalized))"
        + (normalized == 0 ? " — unlimited; memory is no longer bounded" : "")
    )
    #else
    NSLog("NitroChucker: setMaxLogCount(\(normalized)) — Wormholy not linked; no-op")
    #endif
  }

  /// Skip capture for these hosts entirely (suffix match), so their bodies are never
  /// retained. This is the only guard that runs before a body is buffered, which makes it
  /// the most effective lever against large media/CDN payloads — Wormholy has no
  /// body-size cap at all, so a single multi-MB response is otherwise held in full.
  func setIgnoredHosts(hosts: [String]) {
    #if canImport(Wormholy)
    // Lowercase to match Android, where Chucker lowercases both the skip list and the
    // request host. Wormholy compares with a case-sensitive hasSuffix against the raw
    // URL host (Foundation does not fold it), so a mixed-case host would still slip
    // through on iOS — hosts are lowercase in practice, and normalizing the patterns is
    // the most we can do without forking Wormholy.
    let cleaned = hosts
      .map { $0.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() }
      .filter { !$0.isEmpty }
    Wormholy.ignoredHosts = cleaned
    NSLog("NitroChucker: setIgnoredHosts(\(cleaned))")
    #else
    NSLog("NitroChucker: setIgnoredHosts(\(hosts)) — Wormholy not linked; no-op")
    #endif
  }

  private static func keyWindow() -> UIWindow? {
    if #available(iOS 13.0, *) {
      return UIApplication.shared.connectedScenes
        .compactMap { $0 as? UIWindowScene }
        .flatMap { $0.windows }
        .first { $0.isKeyWindow }
    } else {
      return UIApplication.shared.windows.first { $0.isKeyWindow }
    }
  }
}
