import {inject, Injectable} from '@angular/core'
import {ITask} from '../../models/ITask'
import {Filter} from '../../models/filter'
import {ILabel} from '../../models/ILabel'
import {StorageService} from './storage.service'
import {addMinutes, parseISO} from 'date-fns'
import {LocalNotifications, LocalNotificationSchema} from '@capacitor/local-notifications'
import {SettingService} from './setting.service'

@Injectable({
  providedIn: 'root'
})
export class TaskService {

  readonly #storageKey = 'taskStorage'
  #storageService = inject(StorageService)
  #settingService = inject(SettingService)

  // The list of task that will be displayed in the application.
  #taskList: ITask[] = []

  constructor() {
    this.#loadTasks().then(() => console.log('Tasks successfully loaded'))
  }

  get tasks(): ITask[] {
    // The structuredClone methode created a deep copy of every element in the array.
    // This way, the only possible changes that affect the tasks must be made through the TaskService.
    // If we don't use this, the pages would be able to perform all CRUD operations on the array and the effects would
    // be visible on other pages.
    // Because we return a copy that shares no references with the #taskList instance variable, all changes must go
    // through this service.
    // This is a rather academical safety feature, especially in the small-scale apps that we build in this course,
    // but it is included as an extra for those who which to gain a deeper understanding of programming.
    // For more info, see https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy.
    return window.structuredClone(this.#taskList)
  }

  /**
   * Retrieve a task based on its id and return it.
   *
   * @param id The task with the matching id or undefined if no task can be found.
   */
  getTask(id: string): ITask | undefined {
    return window.structuredClone(this.#taskList.find(t => t.id === id))
  }

  /**
   * Create a new uncompleted task with a given name.
   *
   * @param name        The name of the new task.
   * @param description A longer description for the task.
   * @param labels      The labels attached to the task.
   * @param deadline    The deadline by which the task must be finished. Optional, defaults to undefined.
   */
  async createTask(name: string, description: string, labels: ILabel[] = [], deadline?: string): Promise<void> {
    this.#taskList.push({
      name,
      id: window.crypto.randomUUID(),
      done: false,
      description,
      deadline,
      labels,
    })
    await this.#persistTasks()
  }

  /**
   * Change the status of a task from done to not done or the reverse.
   *
   * @param id The id of the task for which the status must be changed.
   */
  async toggleTaskStatus(id: string): Promise<void> {
    const task = this.#taskList.find(t => t.id === id)
    if (task) {
      task.done = !task.done
    }
    await this.#persistTasks()
  }

  /**
   * Update a task with the specified values.
   *
   * @param updatedTask The new values for the task.
   */
  async updateTask(updatedTask: Partial<ITask> & { id: string }): Promise<void> {
    // The Partial interface can be used to create a new interface where every property is optional.
    // This creates a very flexible update method, anywhere between zero and all properties of a task can be updated
    // using this method.
    // It is, of course, impossible to update a task if we don't know the id of the task which must be updated.
    // Therefore, we've also used an intersection type; the function now only accepts a parameter that satisfies both
    // the Partial<ITask> and {id: string} interfaces.

    const task = this.#taskList.find(t => t.id === updatedTask.id)

    if (task === undefined) {
      console.error('Trying to update a nonexistent task.')
      return
    }

    // Copy all the properties and associated values from updatedTask to task.
    // All properties of task that have a name that also exists within updatedTask are overwritten.
    Object.assign(task, updatedTask)
    await this.#persistTasks()
  }

  /**
   * Delete a task from the task list.
   *
   * @param id The id of the task that is to be deleted.
   */
  async deleteTask(id: string): Promise<void> {
    this.#taskList = this.#taskList.filter(t => t.id !== id)
    await this.#persistTasks()
  }

  /**
   * Go through all the tasks and remove the label with the given id.
   *
   * @param labelId The id of the label that is to be removed from all tasks.
   */
  async deleteLabelFromTasks(labelId: string): Promise<void> {
    this.#taskList.forEach(t => t.labels = t.labels.filter(l => l.id !== labelId))
    await this.#persistTasks()
  }

  /**
   * Check if a given filter matches a given task.
   *
   * @param task   The task to compare with the filter.
   * @param filter The filter that is to be applied to the task.
   * @return       True if the filter matches the task, false otherwise.
   */
  private static taskMatchesFilter(task: ITask, filter: Filter): boolean {
    if (Filter.all === filter) {
      return true
    }

    return filter === Filter.completed && task.done || filter === Filter.toDo && !task.done
  }

  /**
   * Filters the list of tasks based on the currently selected filter.
   *
   * @param filter The filter used to filter the tasks.
   * @return       The tasks that match the currently selected filter.
   */
  getFilteredTasks(filter: Filter): ITask[] {
    return this.#taskList
      .filter(t => TaskService.taskMatchesFilter(t, filter))
  }

  /**
   * Return a filtered list of tasks that only contains those tasks that are
   * marked with the given label.
   *
   * @param labelId The id of the label that the tasks must be marked with.
   */
  getTasksByLabel(labelId: string) {
    return this.#taskList.filter(t => t.labels.some(l => l.id === labelId))
  }

  /**
   * Persist all the tasks in this service.
   */
  async #persistTasks(): Promise<void> {
    await this.#storageService.persistObjectData(this.#storageKey, this.#taskList)
    await this.#scheduleNotifications()
  }

  /**
   * Load the persisted tasks or set the task list to an empty array.
   *
   * @private
   */
  async #loadTasks(): Promise<void> {
    this.#taskList = await this.#storageService.loadJSONData<ITask[]>(this.#storageKey) ?? []
  }

  /**
   * Schedule new notification for each task that has a deadline in the future.
   * If notifications aren't enabled, nothing is done.
   */
  async #scheduleNotifications(): Promise<void> {
    if (!this.#settingService.notificationsAreEnabled()) {
      return
    }

    await this.#clearScheduledNotifications()

    const timeToDeadline = this.#settingService.getTimeBeforeDeadline()

    const notifications: LocalNotificationSchema[] = []
    let id = 0

    for (const task of this.#taskList) {
      if (!task.deadline || !this.#taskHasFutureDeadline(task)) {
        continue
      }

      const schedule = {
        at: addMinutes(parseISO(task.deadline), -1 * timeToDeadline),
        allowWhileIdle: true,
      }

      notifications.push({
        title: `The deadline for ${task.name} is nearing.`,
        body: `${task.name} must be completed in ${timeToDeadline} minutes`,
        id,
        schedule,
      })
      id++
    }

    if (notifications.length > 0) {
      await LocalNotifications.schedule({notifications})
    }
  }

  /**
   * Clear all notifications that are currently scheduled.
   *
   * @private
   */
  async #clearScheduledNotifications(): Promise<void> {
    const {notifications} = await LocalNotifications.getPending()
    if (notifications.length > 0) {
      const cancelOptions = {notifications: notifications.map(n => ({id: n.id}))}
      await LocalNotifications.cancel(cancelOptions)
    }
  }

  /**
   * Check if this task has a deadline that hasn't been passed yet.
   */
  #taskHasFutureDeadline(task: ITask): boolean {
    if (task.done || !task.deadline) {
      return false
    }

    const deadlineDate = parseISO(task.deadline)
    const now = new Date()
    return deadlineDate !== undefined && now < deadlineDate
  }
}
