import Tauri
import UserNotifications

enum NotificationError: LocalizedError {
  case contentNoId
  case contentNoTitle
  case contentNoBody
  case triggerRepeatIntervalTooShort
  case attachmentNoId
  case attachmentNoUrl
  case attachmentFileNotFound(path: String)
  case attachmentUnableToCreate(String)
  case pastScheduledTime
  case invalidDate(String)

  var errorDescription: String? {
    switch self {
    case .contentNoId:
      return "Missing notification identifier"
    case .contentNoTitle:
      return "Missing notification title"
    case .contentNoBody:
      return "Missing notification body"
    case .triggerRepeatIntervalTooShort:
      return "Schedule interval too short, must be a least 1 minute"
    case .attachmentNoId:
      return "Missing attachment identifier"
    case .attachmentNoUrl:
      return "Missing attachment URL"
    case .attachmentFileNotFound(let path):
      return "Unable to find file \(path) for attachment"
    case .attachmentUnableToCreate(let error):
      return "Failed to create attachment: \(error)"
    case .pastScheduledTime:
      return "Scheduled time must be *after* current time"
    case .invalidDate(let date):
      return "Could not parse date \(date)"
    }
  }
}

func makeNotificationContent(_ notification: JSObject) throws -> UNNotificationContent {
  guard let title = notification["title"] as? String else {
    throw NotificationError.contentNoTitle
  }
  guard let body = notification["body"] as? String else {
    throw NotificationError.contentNoBody
  }

  let extra = notification["extra"] as? JSObject ?? [:]
  let schedule = notification["schedule"] as? JSObject ?? [:]
  let content = UNMutableNotificationContent()
  content.title = NSString.localizedUserNotificationString(forKey: title, arguments: nil)
  content.body = NSString.localizedUserNotificationString(
    forKey: body,
    arguments: nil)

  content.userInfo = [
    "__EXTRA__": extra,
    "__SCHEDULE__": schedule,
  ]

  if let actionTypeId = notification["actionTypeId"] as? String {
    content.categoryIdentifier = actionTypeId
  }

  if let threadIdentifier = notification["group"] as? String {
    content.threadIdentifier = threadIdentifier
  }

  if let summaryArgument = notification["summary"] as? String {
    content.summaryArgument = summaryArgument
  }

  if let sound = notification["sound"] as? String {
    content.sound = UNNotificationSound(named: UNNotificationSoundName(sound))
  }

  if let attachments = notification["attachments"] as? [JSObject] {
    content.attachments = try makeAttachments(attachments)
  }

  return content
}

func makeAttachments(_ attachments: [JSObject]) throws -> [UNNotificationAttachment] {
  var createdAttachments = [UNNotificationAttachment]()

  for attachment in attachments {
    guard let id = attachment["id"] as? String else {
      throw NotificationError.attachmentNoId
    }
    guard let url = attachment["url"] as? String else {
      throw NotificationError.attachmentNoUrl
    }
    guard let urlObject = makeAttachmentUrl(url) else {
      throw NotificationError.attachmentFileNotFound(path: url)
    }

    let options = attachment["options"] as? JSObject ?? [:]

    do {
      let newAttachment = try UNNotificationAttachment(
        identifier: id, url: urlObject, options: makeAttachmentOptions(options))
      createdAttachments.append(newAttachment)
    } catch {
      throw NotificationError.attachmentUnableToCreate(error.localizedDescription)
    }
  }

  return createdAttachments
}

func makeAttachmentUrl(_ path: String) -> URL? {
  return URL(string: path)
}

func makeAttachmentOptions(_ options: JSObject) -> JSObject {
  var opts: JSObject = [:]

  if let iosUNNotificationAttachmentOptionsTypeHintKey = options[
    "iosUNNotificationAttachmentOptionsTypeHintKey"] as? String
  {
    opts[UNNotificationAttachmentOptionsTypeHintKey] = iosUNNotificationAttachmentOptionsTypeHintKey
  }
  if let iosUNNotificationAttachmentOptionsThumbnailHiddenKey = options[
    "iosUNNotificationAttachmentOptionsThumbnailHiddenKey"] as? String
  {
    opts[UNNotificationAttachmentOptionsThumbnailHiddenKey] =
      iosUNNotificationAttachmentOptionsThumbnailHiddenKey
  }
  if let iosUNNotificationAttachmentOptionsThumbnailClippingRectKey = options[
    "iosUNNotificationAttachmentOptionsThumbnailClippingRectKey"] as? String
  {
    opts[UNNotificationAttachmentOptionsThumbnailClippingRectKey] =
      iosUNNotificationAttachmentOptionsThumbnailClippingRectKey
  }
  if let iosUNNotificationAttachmentOptionsThumbnailTimeKey = options[
    "iosUNNotificationAttachmentOptionsThumbnailTimeKey"] as? String
  {
    opts[UNNotificationAttachmentOptionsThumbnailTimeKey] =
      iosUNNotificationAttachmentOptionsThumbnailTimeKey
  }
  return opts
}

func handleScheduledNotification(_ schedule: JSObject) throws
  -> UNNotificationTrigger?
{
  let kind = schedule["kind"] as? String ?? ""
  let payload = schedule["data"] as? JSObject ?? [:]
  switch kind {
  case "At":
    let date = payload["date"] as? String ?? ""
    let dateFormatter = DateFormatter()
    dateFormatter.locale = Locale(identifier: "en_US_POSIX")
    dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"

    if let at = dateFormatter.date(from: date) {
      let repeats = payload["repeats"] as? Bool ?? false

      let dateInfo = Calendar.current.dateComponents(in: TimeZone.current, from: at)

      if dateInfo.date! < Date() {
        throw NotificationError.pastScheduledTime
      }

      let dateInterval = DateInterval(start: Date(), end: dateInfo.date!)

      // Notifications that repeat have to be at least a minute between each other
      if repeats && dateInterval.duration < 60 {
        throw NotificationError.triggerRepeatIntervalTooShort
      }

      return UNTimeIntervalNotificationTrigger(
        timeInterval: dateInterval.duration, repeats: repeats)

    } else {
      throw NotificationError.invalidDate(date)
    }
  case "Interval":
    let dateComponents = getDateComponents(payload)
    return UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true)
  case "Every":
    let interval = payload["interval"] as? String ?? ""
    let count = schedule["count"] as? Int ?? 1

    if let repeatDateInterval = getRepeatDateInterval(interval, count) {
      // Notifications that repeat have to be at least a minute between each other
      if repeatDateInterval.duration < 60 {
        throw NotificationError.triggerRepeatIntervalTooShort
      }

      return UNTimeIntervalNotificationTrigger(
        timeInterval: repeatDateInterval.duration, repeats: true)
    }

  default:
    return nil
  }

  return nil
}

/// Given our schedule format, return a DateComponents object
/// that only contains the components passed in.

func getDateComponents(_ at: JSObject) -> DateComponents {
  // var dateInfo = Calendar.current.dateComponents(in: TimeZone.current, from: Date())
  // dateInfo.calendar = Calendar.current
  var dateInfo = DateComponents()

  if let year = at["year"] as? Int {
    dateInfo.year = year
  }
  if let month = at["month"] as? Int {
    dateInfo.month = month
  }
  if let day = at["day"] as? Int {
    dateInfo.day = day
  }
  if let hour = at["hour"] as? Int {
    dateInfo.hour = hour
  }
  if let minute = at["minute"] as? Int {
    dateInfo.minute = minute
  }
  if let second = at["second"] as? Int {
    dateInfo.second = second
  }
  if let weekday = at["weekday"] as? Int {
    dateInfo.weekday = weekday
  }
  return dateInfo
}

/// Compute the difference between the string representation of a date
/// interval and today. For example, if every is "month", then we
/// return the interval between today and a month from today.

func getRepeatDateInterval(_ every: String, _ count: Int) -> DateInterval? {
  let cal = Calendar.current
  let now = Date()
  switch every {
  case "Year":
    let newDate = cal.date(byAdding: .year, value: count, to: now)!
    return DateInterval(start: now, end: newDate)
  case "Month":
    let newDate = cal.date(byAdding: .month, value: count, to: now)!
    return DateInterval(start: now, end: newDate)
  case "TwoWeeks":
    let newDate = cal.date(byAdding: .weekOfYear, value: 2 * count, to: now)!
    return DateInterval(start: now, end: newDate)
  case "Week":
    let newDate = cal.date(byAdding: .weekOfYear, value: count, to: now)!
    return DateInterval(start: now, end: newDate)
  case "Day":
    let newDate = cal.date(byAdding: .day, value: count, to: now)!
    return DateInterval(start: now, end: newDate)
  case "Hour":
    let newDate = cal.date(byAdding: .hour, value: count, to: now)!
    return DateInterval(start: now, end: newDate)
  case "Minute":
    let newDate = cal.date(byAdding: .minute, value: count, to: now)!
    return DateInterval(start: now, end: newDate)
  case "Second":
    let newDate = cal.date(byAdding: .second, value: count, to: now)!
    return DateInterval(start: now, end: newDate)
  default:
    return nil
  }
}
