import Cocoa
import FlutterMacOS
import CoreGraphics
import Network
import UserNotifications

// ---------------------------------------------------------------------------
// MacOSPermissionsHandler
//
// Native-side MethodChannel handler for macOS-specific permissions
// that the Flutter `permission_handler` plugin does not cover:
//
//   • Input Monitoring  (CGPreflightListenEventAccess /
//                        CGRequestListenEventAccess) for global PTT.
//   • Local Network     (NWBrowser trigger for _ts3._tcp) so macOS 15+
//                        shows the Local Network Privacy prompt.
//   • Notifications     (UNUserNotificationCenter authorization).
//
// Channel name: `app.chanora/macos_permissions`
//
// Trace: SRS-198, SRS-297, SRS-300, SysRS-166, SDD-091.
// ---------------------------------------------------------------------------

/// Polling interval for Input Monitoring state changes.
/// TCC does not emit a callback when the user toggles Input Monitoring
/// in System Settings, so we poll at a reasonable cadence.
private let kInputMonitoringPollInterval: TimeInterval = 2.0

class MacOSPermissionsHandler: NSObject, FlutterPlugin {
  private var channel: FlutterMethodChannel?
  private var inputMonitoringTimer: Timer?
  private var lastInputMonitoringState: String = "Unknown"

  // -- FlutterPlugin -------------------------------------------------------

  static func register(with registrar: FlutterPluginRegistrar) {
    let channel = FlutterMethodChannel(
      name: "app.chanora/macos_permissions",
      binaryMessenger: registrar.messenger
    )
    let instance = MacOSPermissionsHandler()
    instance.channel = channel
    registrar.addMethodCallDelegate(instance, channel: channel)
  }

  func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
    switch call.method {
    // -- Input Monitoring ---------------------------------------------------
    case "checkInputMonitoring":
      result(inputMonitoringStateString())
      startInputMonitoringPolling()

    case "requestInputMonitoring":
      requestInputMonitoring(result: result)

    case "openInputMonitoringSettings":
      openInputMonitoringSettings(result: result)

    // -- Local Network ------------------------------------------------------
    case "checkLocalNetwork":
      checkLocalNetwork(result: result)

    case "triggerLocalNetworkPrompt":
      triggerLocalNetworkPrompt(result: result)

    case "checkLocalNetworkAccess":
      guard let args = call.arguments as? [String: Any],
            let host = args["host"] as? String,
            let port = args["port"] as? Int else {
        result(FlutterError(
          code: "INVALID_ARGS",
          message: "checkLocalNetworkAccess requires host (String) and port (Int)",
          details: nil))
        return
      }
      checkLocalNetworkAccess(host: host, port: port, result: result)

    // -- Notifications ------------------------------------------------------
    case "checkNotifications":
      checkNotifications(result: result)

    case "requestNotifications":
      requestNotifications(result: result)

    default:
      result(FlutterMethodNotImplemented)
    }
  }

  // =========================================================================
  // Input Monitoring
  // =========================================================================

  /// Returns the current Input Monitoring state as a string
  /// consumable by the Dart side: "Granted", "Denied", "NotDetermined".
  private func inputMonitoringStateString() -> String {
    // CGPreflightListenEventAccess returns true when access is already
    // granted. On macOS 10.15+ it returns false when denied or not yet
    // determined — we cannot distinguish those two without attempting
    // CGRequestListenEventAccess, so we conservatively report
    // "NotDetermined" when preflight returns false. The Dart side
    // treats both "Denied" and "NotDetermined" as L0Focused.
    if CGPreflightListenEventAccess() {
      return "Granted"
    }
    return "NotDetermined"
  }

  /// Request Input Monitoring permission via
  /// `CGRequestListenEventAccess()`. On macOS 13+ this opens
  /// System Settings → Privacy & Security → Input Monitoring.
  private func requestInputMonitoring(result: @escaping FlutterResult) {
    // CGRequestListenEventAccess shows the system prompt.
    // It returns true if access was already granted or becomes
    // granted synchronously (rare). Most of the time it returns
    // false and the user must toggle the switch manually.
    let granted = CGRequestListenEventAccess()
    let state = granted ? "Granted" : "NotDetermined"
    lastInputMonitoringState = state
    result(state)

    // Start polling so we detect when the user grants in Settings.
    startInputMonitoringPolling()
  }

  /// Open System Settings → Privacy & Security → Input Monitoring
  /// so the user can manually enable the app.
  private func openInputMonitoringSettings(result: @escaping FlutterResult) {
    if let url = URL(
      string: "x-apple.systempreferences:com.apple.preference.security?Privacy_ListenEvent"
    ) {
      NSWorkspace.shared.open(url)
    }
    result(nil)
  }

  /// Start a periodic timer that checks Input Monitoring state and
  /// notifies the Dart side when it changes.
  private func startInputMonitoringPolling() {
    // Don't start a second timer if one is already running.
    guard inputMonitoringTimer == nil else { return }
    lastInputMonitoringState = inputMonitoringStateString()

    inputMonitoringTimer = Timer.scheduledTimer(
      withTimeInterval: kInputMonitoringPollInterval,
      repeats: true
    ) { [weak self] _ in
      self?.pollInputMonitoring()
    }
  }

  private func pollInputMonitoring() {
    let current = inputMonitoringStateString()
    guard current != lastInputMonitoringState else { return }
    lastInputMonitoringState = current

    // Notify the Dart side via the inbound method.
    channel?.invokeMethod("inputMonitoringStateChanged", arguments: [
      "state": current,
    ])
  }

  // =========================================================================
  // Local Network
  // =========================================================================

  /// Check whether Local Network access is available.
  /// On macOS 14 and earlier, Local Network Privacy does not exist,
  /// so we report "Unsupported". On macOS 15+, we attempt a brief
  /// NWBrowser scan and report based on the result.
  private func checkLocalNetwork(result: @escaping FlutterResult) {
    if #available(macOS 15.0, *) {
      // We cannot synchronously determine the Local Network state
      // without actually using the network. Report "NotDetermined"
      // and let triggerLocalNetworkPrompt resolve the actual state.
      result("NotDetermined")
    } else {
      result("Unsupported")
    }
  }

  /// Trigger the Local Network Privacy prompt by starting a brief
  /// NWBrowser for `_ts3._tcp`. On macOS 15+, this causes the system
  /// to show the Local Network permission dialog if not already
  /// determined.
  ///
  /// The browser is started and stopped after a short scan window.
  /// State changes are reported back to Dart via
  /// `localNetworkStateChanged`.
  @available(macOS 15.0, *)
  private func triggerLocalNetworkPromptImpl(result: @escaping FlutterResult) {
    let bonjourType = "_ts3._tcp"
    let browserDescriptor = NWBrowser.Descriptor.bonjourWithTXTRecord(
      type: bonjourType, domain: nil)
    let browser = NWBrowser(for: browserDescriptor, using: NWParameters.tcp)

    var resolved = false

    browser.stateUpdateHandler = { [weak self] (browserState: NWBrowser.State) in
      switch browserState {
      case .ready:
        // Browser started successfully — local network is accessible.
        if !resolved {
          resolved = true
          result("Granted")
          self?.channel?.invokeMethod("localNetworkStateChanged", arguments: [
            "state": "Granted",
          ])
        }
        browser.cancel()
      case .failed(let error):
        if !resolved {
          resolved = true
          // Check for DNS policy-denied error (kDNSServiceErr_PolicyDenied = -65570).
          // This is the canonical signal that the user denied the Local Network prompt.
          if case .dns(let dnsError) = error,
             dnsError == DNSServiceErrorType(kDNSServiceErr_PolicyDenied) {
            result("Denied")
            self?.channel?.invokeMethod("localNetworkStateChanged", arguments: [
              "state": "Denied",
            ])
          } else {
            // Network unreachable or other transient error —
            // don't assume denied.
            result("NotDetermined")
          }
        }
        browser.cancel()
      case .waiting(let error):
        // The browser is waiting for network. If the specific DNS error
        // is kDNSServiceErr_PolicyDenied, the user explicitly denied
        // the Local Network prompt — report immediately.
        if case .dns(let dnsError) = error,
           dnsError == DNSServiceErrorType(kDNSServiceErr_PolicyDenied) {
          if !resolved {
            resolved = true
            result("Denied")
            self?.channel?.invokeMethod("localNetworkStateChanged", arguments: [
              "state": "Denied",
            ])
          }
          browser.cancel()
        }
        // Otherwise the system dialog may be showing — wait for
        // .ready, .failed, or the timeout.
      case .setup, .cancelled:
        break
      @unknown default:
        break
      }
    }

    browser.start(queue: DispatchQueue.main)

    // Timeout: if the browser doesn't resolve within 10 seconds,
    // report NotDetermined so the Dart side doesn't hang forever.
    DispatchQueue.main.asyncAfter(deadline: .now() + 10.0) {
      if !resolved {
        resolved = true
        result("NotDetermined")
        browser.cancel()
      }
    }
  }

  private func triggerLocalNetworkPrompt(result: @escaping FlutterResult) {
    if #available(macOS 15.0, *) {
      triggerLocalNetworkPromptImpl(result: result)
    } else {
      result("Unsupported")
    }
  }

  /// Probe whether Local Network access is currently denied for a specific
  /// host:port by creating a short-lived NWConnection and checking
  /// `unsatisfiedReason == .localNetworkDenied`.  This does NOT trigger a
  /// new system prompt — it is a read-only check.
  private func checkLocalNetworkAccess(
    host: String, port: Int, result: @escaping FlutterResult
  ) {
    if #available(macOS 15.0, *) {
      checkLocalNetworkAccessImpl(host: host, port: port, result: result)
    } else {
      result("Unsupported")
    }
  }

  @available(macOS 15.0, *)
  private func checkLocalNetworkAccessImpl(
    host: String, port: Int, result: @escaping FlutterResult
  ) {
    guard let endpointPort = NWEndpoint.Port(rawValue: UInt16(port)) else {
      result("NotDetermined")
      return
    }
    let endpointHost = NWEndpoint.Host(host)
    let connection = NWConnection(
      host: endpointHost, port: endpointPort, using: .tcp)
    let queue = DispatchQueue(
      label: "app.chanora.macos_permissions.local_network_check")

    var didComplete = false

    func finish(_ state: String) {
      guard !didComplete else { return }
      didComplete = true
      connection.cancel()
      result(state)
    }

    connection.stateUpdateHandler = { state in
      switch state {
      case .waiting, .failed:
        if connection.currentPath?.unsatisfiedReason
            == .localNetworkDenied {
          finish("Denied")
        } else {
          finish("NotDetermined")
        }
      case .ready:
        finish("Granted")
      case .cancelled:
        finish("NotDetermined")
      case .setup, .preparing:
        break
      @unknown default:
        break
      }
    }

    connection.start(queue: queue)

    DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
      if !didComplete {
        if connection.currentPath?.unsatisfiedReason
            == .localNetworkDenied {
          finish("Denied")
        } else {
          finish("NotDetermined")
        }
      }
    }
  }

  // =========================================================================
  // Notifications
  // =========================================================================

  private func checkNotifications(result: @escaping FlutterResult) {
    UNUserNotificationCenter.current().getNotificationSettings { settings in
      switch settings.authorizationStatus {
      case .authorized, .provisional:
        result("Granted")
      case .denied:
        result("Denied")
      case .notDetermined:
        result("NotDetermined")
      @unknown default:
        result("NotDetermined")
      }
    }
  }

  private func requestNotifications(result: @escaping FlutterResult) {
    UNUserNotificationCenter.current().requestAuthorization(options: [
      .alert, .sound, .badge,
    ]) { granted, _ in
      let state = granted ? "Granted" : "Denied"
      result(state)
    }
  }

  // =========================================================================
  // Cleanup
  // =========================================================================

  deinit {
    inputMonitoringTimer?.invalidate()
  }
}

// ---------------------------------------------------------------------------
// MainFlutterWindow
// ---------------------------------------------------------------------------

class MainFlutterWindow: NSWindow {
  override func awakeFromNib() {
    let flutterViewController = FlutterViewController()
    let windowFrame = self.frame
    self.contentViewController = flutterViewController
    self.setFrame(windowFrame, display: true)

    self.titlebarAppearsTransparent = true
    self.titleVisibility = .hidden
    self.styleMask.insert(.fullSizeContentView)
    self.isMovableByWindowBackground = true

    RegisterGeneratedPlugins(registry: flutterViewController)

    // Register the macOS permissions MethodChannel handler.
    MacOSPermissionsHandler.register(with: flutterViewController.registrar(forPlugin: "MacOSPermissionsHandler"))
    // Register the macOS audio-lifecycle MethodChannel handler (closes the iOS/macOS asymmetry in SysRS-051).
    MacOSAudioLifecycle.register(with: flutterViewController.registrar(forPlugin: "MacOSAudioLifecycle"))

    super.awakeFromNib()
  }
}
