import Flutter
import UIKit
import UserNotifications
import workmanager_apple

@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {

  // Kept in sync with lib/push/push_actions.dart (kTalkReplyActionId /
  // kTalkMarkReadActionId) and lib/push/push_renderer.dart (iosTalkCategory).
  private let talkCategoryId = "TALK_MESSAGE"
  private let replyActionId = "TALK_REPLY"
  private let markReadActionId = "TALK_MARK_READ"

  // Shared (App Group) keychain — same group as the NSE and the Dart side.
  private let keychainAccessGroup = "MY55VF3KPG.eu.mhsl.marianum.mobile.client.push"
  private let usernameAccount = "nextcloud_username"
  private let appPasswordAccount = "nextcloud_app_password"
  private let baseUrlAccount = "nextcloud_base_url"

  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    let result = super.application(application, didFinishLaunchingWithOptions: launchOptions)
    registerTalkCategory()
    // BGAppRefresh for the home-screen widget. Must all happen before
    // didFinishLaunching returns: with the UIScene lifecycle Flutter registers
    // plugins only during scene connection, which is past BGTaskScheduler's
    // registration deadline — registerLaunchHandlers() bridges that gap.
    // The task identifier mirrors WidgetBackgroundTask.periodicTaskName; the
    // plugin re-submits the refresh request itself after every run.
    WorkmanagerPlugin.setPluginRegistrantCallback { registry in
      GeneratedPluginRegistrant.register(with: registry)
    }
    WorkmanagerPlugin.registerPeriodicTask(
      withIdentifier: "eu.mhsl.marianum.widget.refresh",
      earliestBeginInSeconds: 1800)
    WorkmanagerPlugin.registerLaunchHandlers()
    // FlutterAppDelegate conforms to UNUserNotificationCenterDelegate and
    // forwards these callbacks to the plugins (firebase_messaging,
    // flutter_local_notifications). We route Talk actions natively here — the
    // Flutter engine is not guaranteed to run for a background action — and
    // forward everything else to `super` so plugin behaviour is preserved.
    UNUserNotificationCenter.current().delegate = self
    return result
  }

  func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
    GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
  }

  private func registerTalkCategory() {
    let reply = UNTextInputNotificationAction(
      identifier: replyActionId,
      title: "Antworten",
      options: [],
      textInputButtonTitle: "Senden",
      textInputPlaceholder: "Nachricht"
    )
    let markRead = UNNotificationAction(
      identifier: markReadActionId,
      title: "Als gelesen markieren",
      options: []
    )
    let category = UNNotificationCategory(
      identifier: talkCategoryId,
      actions: [reply, markRead],
      intentIdentifiers: [],
      options: []
    )
    UNUserNotificationCenter.current().setNotificationCategories([category])
  }

  // MARK: - UNUserNotificationCenterDelegate

  override func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    didReceive response: UNNotificationResponse,
    withCompletionHandler completionHandler: @escaping () -> Void
  ) {
    let actionId = response.actionIdentifier
    if actionId == replyActionId || actionId == markReadActionId {
      // Handled natively; deliberately NOT forwarded to super so the plugins
      // don't also process it (which would double-send the reply).
      handleTalkAction(response, completion: completionHandler)
      return
    }
    super.userNotificationCenter(
      center, didReceive: response, withCompletionHandler: completionHandler)
  }

  // MARK: - Native Talk action handling

  private func handleTalkAction(
    _ response: UNNotificationResponse,
    completion: @escaping () -> Void
  ) {
    guard
      let token = chatToken(from: response),
      let credentials = loadCredentials()
    else {
      completion()
      return
    }

    let group = DispatchGroup()

    if response.actionIdentifier == replyActionId,
       let text = (response as? UNTextInputNotificationResponse)?.userText
         .trimmingCharacters(in: .whitespacesAndNewlines),
       !text.isEmpty {
      group.enter()
      ocsPost(
        credentials: credentials,
        path: "apps/spreed/api/v1/chat/\(token)",
        form: ["message": text]
      ) { group.leave() }
    }

    // Both reply and mark-read clear the unread marker on the chat.
    group.enter()
    ocsPost(
      credentials: credentials,
      path: "apps/spreed/api/v1/chat/\(token)/read",
      form: nil
    ) { group.leave() }

    group.notify(queue: .main) { completion() }
  }

  private func chatToken(from response: UNNotificationResponse) -> String? {
    let userInfo = response.notification.request.content.userInfo
    if let token = userInfo["chatToken"] as? String, !token.isEmpty {
      return token
    }
    let thread = response.notification.request.content.threadIdentifier
    if thread.hasPrefix("talk_") {
      let token = String(thread.dropFirst("talk_".count))
      return token.isEmpty ? nil : token
    }
    return nil
  }

  private struct Credentials {
    let baseUrl: String
    let authorization: String
  }

  private func loadCredentials() -> Credentials? {
    guard
      let username = keychainString(usernameAccount),
      let appPassword = keychainString(appPasswordAccount),
      let baseUrl = keychainString(baseUrlAccount),
      let token = "\(username):\(appPassword)".data(using: .utf8)
    else { return nil }
    let authorization = "Basic \(token.base64EncodedString())"
    let trimmed = baseUrl.hasSuffix("/") ? String(baseUrl.dropLast()) : baseUrl
    return Credentials(baseUrl: trimmed, authorization: authorization)
  }

  private func ocsPost(
    credentials: Credentials,
    path: String,
    form: [String: String]?,
    completion: @escaping () -> Void
  ) {
    guard let url = URL(string: "\(credentials.baseUrl)/ocs/v2.php/\(path)") else {
      completion()
      return
    }
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue(credentials.authorization, forHTTPHeaderField: "Authorization")
    request.setValue("true", forHTTPHeaderField: "OCS-APIRequest")
    request.setValue("application/json", forHTTPHeaderField: "Accept")
    if let form = form {
      request.setValue(
        "application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
      request.httpBody = form
        .map { "\($0.key)=\(formEncode($0.value))" }
        .joined(separator: "&")
        .data(using: .utf8)
    }
    URLSession.shared.dataTask(with: request) { _, response, error in
      if let error = error {
        NSLog("[Talk action] \(path) failed: \(error.localizedDescription)")
      } else if let http = response as? HTTPURLResponse,
                !(200..<300).contains(http.statusCode) {
        NSLog("[Talk action] \(path) -> HTTP \(http.statusCode)")
      }
      completion()
    }.resume()
  }

  private func formEncode(_ value: String) -> String {
    var allowed = CharacterSet.alphanumerics
    allowed.insert(charactersIn: "-._~")
    return value.addingPercentEncoding(withAllowedCharacters: allowed) ?? value
  }

  // MARK: - Keychain (shared App Group)

  private func keychainString(_ account: String) -> String? {
    let query: [CFString: Any] = [
      kSecClass: kSecClassGenericPassword,
      kSecAttrAccount: account,
      kSecAttrAccessGroup: keychainAccessGroup,
      kSecReturnData: true,
      kSecMatchLimit: kSecMatchLimitOne,
    ]
    var result: AnyObject?
    let status = SecItemCopyMatching(query as CFDictionary, &result)
    guard status == errSecSuccess, let data = result as? Data else { return nil }
    return String(data: data, encoding: .utf8)
  }
}
