import type { Activity } from './Activity.js'
import type { Application } from './Application.js'
import { navigateScreen } from './ApplicationRoute.js'

export enum ActivityLifecycle {
  /**
   * The activity is initialized, but not yet created.
   * This is the initial state of the activity.
   */
  Initialized,
  /**
   * The activity is created, but not yet started.
   * This is the state after the `onCreate()` method is called.
   */
  Created,
  /**
   * The activity is started, but not yet resumed.
   * This is the state after the `onStart()` method is called.
   */
  Started,
  /**
   * The activity is resumed and visible to the user.
   * This is the state after the `onResume()` method is called.
   */
  Resumed,
  /**
   * The activity is paused, but still visible to the user.
   * This is the state after the `onPause()` method is called.
   */
  Paused,
  /**
   * The activity is stopped and no longer visible to the user.
   * This is the state after the `onStop()` method is called.
   */
  Stopped,
  /**
   * The activity is destroyed and no longer exists.
   * This is the state after the `onDestroy()` method is called.
   */
  Destroyed,
}

export enum IntentFlags {
  /**
   * Marks that the activity should be rendered as a modal.
   */
  Modal = 1 << 0,
  /**
   * The activity is started for result.
   */
  ForResult = 1 << 1,
  /**
   * When set, the intent data is passed by reference.
   * By default the activity manager makes a copy of the intent data.
   * This is useful when the data is large and you want to avoid copying it
   * or when you want to share the same data between activities.
   */
  ByReference = 1 << 2,
}

export interface Intent {
  action: string
  data?: unknown
  category?: string[]
  flags?: number
  /**
   * The request code used to distinguish between different activities.
   */
  requestCode?: number
}

export enum IntentResult {
  RESULT_CANCELED,
  RESULT_OK,
}

/**
 * Defines the information stored for each activity on the stack:
 * the Activity instance, a unique id, and optionally the Intent that started the activity
 * (useful for restoring state).
 */
interface ActivityStackEntry {
  /**
   * The instance of the activity.
   */
  activity: Activity
  /**
   * Activity's unique ID.
   */
  id: string
  /**
   * The intent that started the activity.
   */
  intent: Intent
  /**
   * The action name the activity is registered for.
   */
  action: string
}

/**
 * Manages application activities.
 *
 * An activity is a screen or a portion of a rendered screen that has its own
 * lifecycle methods and routing.
 *
 * An activity is registered by its class definition. Because of that, activities
 * cannot have constructors that require own parameters. Activities have to be
 * self contained or use the event system to request data.
 */
export class ActivityManager {
  private currentActivity?: Activity

  /**
   * Stores activity *classes* by unique ID (action name).
   */
  private activityClasses = new Map<string, typeof Activity>()

  /**
   * Represents the activity stack. It is used to manage
   * the UI state after calling the back action.
   */
  private activityStack: ActivityStackEntry[] = []

  /**
   * A stack of activities that are rendered over other activities
   * in a dialog.
   */
  private modalActivityStack: ActivityStackEntry[] = []

  private generateActivityId(): string {
    return Math.random().toString(36).substring(2, 15)
  }

  #parent: Application

  constructor(parent: Application) {
    this.#parent = parent
    // this.reflectEvent = this.reflectEvent.bind(this)
  }

  registerActivity(action: string, activityClass: typeof Activity): void {
    if (this.activityClasses.has(action)) {
      // eslint-disable-next-line no-console
      console.warn(`Activity with action "${action}" already registered.`, new Error().stack)
    }
    this.activityClasses.set(action, activityClass)
  }

  // protected reflectEvent<T extends Event>(event: T): T {
  //   const copy = Reflect.construct(event.constructor, [event.type, event])
  //   const dispatched = this.#parent.events.dispatchEvent(copy)
  //   if (!dispatched) {
  //     event.preventDefault()
  //   }
  //   return copy
  // }

  /**
   * Creates an activity and pushes it into the stack, but it does not initialize any of the lifecycle methods.
   * It is a way to put an activity at some place of the stack,
   * but it will be only initialized when other activities finis.
   *
   * Note, if you add an uninitialized activity and you won't add more activities then this activity may never run.
   * Make sure that there's always another activity.
   *
   * @param intent The intent that created this activity.
   */
  async createActivity(intent: Intent): Promise<void> {
    const activity = this.buildActivity(intent)
    const activityId = this.generateActivityId()
    const stackEntry: ActivityStackEntry = { activity, id: activityId, intent, action: intent.action }
    this.activityStack.push(stackEntry)
  }

  setupRoute(intent: Intent): void {
    if (intent.data) {
      const typed = intent.data as { uri?: string }
      if (typed.uri) {
        navigateScreen(typed.uri)
      }
    }
  }

  /**
   * Starts a new activity and brings it to the foreground.
   *
   * @param intent The intent that created this activity.
   */
  async startActivity(intent: Intent): Promise<void> {
    const { currentActivity, activityStack } = this
    const lastIndex = activityStack.findLastIndex((entry) => entry.action === intent.action)
    // if the activity is in the history list, we bring it up and update intent.
    if (lastIndex > -1) {
      const info = activityStack[lastIndex]
      activityStack.splice(lastIndex, 1)
      activityStack.push({
        id: info.id,
        activity: info.activity,
        action: info.action,
        intent,
      })
      await info.activity.onNewIntent(intent)
      // TODO: Check if the activity is destroyed.
      this.setupRoute(intent)
      await this.updateCurrentActivity(info.activity, false)
      return
    }
    const activity = this.buildActivity(intent)
    if (currentActivity) {
      await currentActivity.onPause()
      currentActivity.lifecycle = ActivityLifecycle.Paused
    }

    const activityId = this.generateActivityId()
    const stackEntry: ActivityStackEntry = { activity, id: activityId, intent, action: intent.action }
    // Modal activities are pretty much the same as regular activities but are rendered on top
    // of other activities. We keep track of them on another stack as all modal activities must
    // finish before returning to the regular stack.
    const isModal = (intent.flags ?? 0) & IntentFlags.Modal
    if (isModal) {
      this.modalActivityStack.push(stackEntry)
    } else {
      this.activityStack.push(stackEntry)
    }
    await activity.onCreate(intent)
    if (this.isDestroyed(activity)) {
      // the activity finished and the manager already took care of this situation.
      return
    }
    activity.lifecycle = ActivityLifecycle.Created

    await this.updateCurrentActivity(activity, !!isModal)
    if (this.isDestroyed(activity)) {
      // the activity finished and the manager already took care of this situation.
      return
    }
    this.setupRoute(intent)
  }

  private buildActivity(intent: Intent): Activity {
    const action = intent.action
    if (!this.activityClasses.has(action)) {
      throw new Error(`No activity class registered for action: ${action}`)
    }

    const ActivityClass = this.activityClasses.get(action)
    if (!ActivityClass) {
      throw new Error(`No activity class registered for action: ${action}`)
    }

    // We use the container to resolve dependencies, if any.
    // const activity = (await container.make(ActivityClass, [this.#parent])) as Activity
    return new ActivityClass(this.#parent)
  }

  /**
   * Starts an activity that should return a result to the calling activity.
   * The result should be a unique number across the application.
   * @param intent The intent to start the activity.
   * @param requestCode The request code used to match the activity result.
   */
  async startActivityForResult(intent: Intent, requestCode: number): Promise<void> {
    intent.requestCode = requestCode
    const byReference = (intent.flags ?? 0) & IntentFlags.ByReference
    const shallowCopy = { ...intent }
    const data = shallowCopy.data
    if (byReference) {
      delete shallowCopy.data
    }
    const deepCopy = structuredClone(shallowCopy)
    if (data && byReference) {
      deepCopy.data = data
    }
    if (deepCopy.flags) {
      deepCopy.flags = deepCopy.flags | IntentFlags.ForResult
    } else {
      deepCopy.flags = IntentFlags.ForResult
    }
    await this.startActivity(deepCopy)
  }

  isDestroyed(activity: Activity): boolean {
    return activity.lifecycle === ActivityLifecycle.Destroyed
  }

  /**
   * @param activity The activity to finish.
   */
  async finishActivity(activity: Activity): Promise<void> {
    this.setupStyles(undefined, activity)
    const isModal = this.modalActivityStack.some((entry) => entry.activity === activity)
    const stack = isModal ? this.modalActivityStack : this.activityStack
    const index = stack.findIndex((entry) => entry.activity === activity)
    if (index !== -1) {
      const stackEntry = stack.splice(index, 1)[0]

      await stackEntry.activity.onPause()
      stackEntry.activity.lifecycle = ActivityLifecycle.Paused
      await stackEntry.activity.onStop()
      stackEntry.activity.lifecycle = ActivityLifecycle.Stopped
      await stackEntry.activity.onDestroy()
      stackEntry.activity.lifecycle = ActivityLifecycle.Destroyed

      await this.manageActivityResult(stackEntry.activity, stackEntry.intent)

      // Resume the previous activity
      await this.bringLastActivityToFront()
    } else {
      if (activity.lifecycle === ActivityLifecycle.Created) {
        await activity.onStop()
        activity.lifecycle = ActivityLifecycle.Stopped
      }
      await activity.onDestroy()
      activity.lifecycle = ActivityLifecycle.Destroyed
      // This can happen when an activity finishes in one of the callback methods,
      // before it is added to the stack. In that case, we bring the last activity back to the front.
      await this.bringLastActivityToFront()
    }
    this.#parent.requestUpdate()
  }

  private async manageActivityResult(activity: Activity, intent: Intent): Promise<void> {
    const { flags = 0, requestCode = -1 } = intent
    if (flags & IntentFlags.ForResult) {
      const all = [...this.activityStack, ...this.modalActivityStack]
      const target = all.find((entry) => entry.activity.hasRequestCode(requestCode))
      if (target) {
        if (target.activity.lifecycle === ActivityLifecycle.Destroyed) {
          return
        }
        if (target.activity.lifecycle === ActivityLifecycle.Resumed) {
          await target.activity.onPause()
          target.activity.lifecycle = ActivityLifecycle.Paused
        }
        const intentCopy = structuredClone(intent)
        intentCopy.data = activity.getResult()
        await target.activity.onActivityResult(requestCode, activity.resultCode, intentCopy)
      }
    }
  }

  private async bringLastActivityToFront(): Promise<void> {
    if (this.modalActivityStack.length > 0) {
      const previousEntry = this.modalActivityStack[this.activityStack.length - 1]
      await this.updateCurrentActivity(previousEntry.activity, false)
    } else if (this.activityStack.length > 0) {
      const previousEntry = this.activityStack[this.activityStack.length - 1]
      await this.updateCurrentActivity(previousEntry.activity, false)
    } else {
      this.currentActivity = undefined // No more activities
    }
  }

  getTopActivity(): Activity | undefined {
    const topModal = this.modalActivityStack.findLast((a) => a.activity.lifecycle === ActivityLifecycle.Resumed)
    if (topModal) {
      return topModal.activity
    }
    const top = this.activityStack.findLast((a) => a.activity.lifecycle === ActivityLifecycle.Resumed)
    return top ? top.activity : undefined
  }

  getCurrentActivity(): Activity | undefined {
    return this.currentActivity
  }

  /**
   * Sets the styles for the current activity.
   * This method adds or removes CSS classes based on the activity's class name
   * and action, allowing for dynamic styling based on the active activity.
   * @param activity The activity to set styles for.
   * If not provided, it will not set any styles.
   * If the activity is the same as the current one, it does nothing.
   */
  setupStyles(activity?: Activity, oldActivity?: Activity): void {
    if (activity === oldActivity) {
      return
    }
    const root = document?.documentElement
    if (!root) {
      return
    }
    if (oldActivity) {
      const ctor = oldActivity.constructor as typeof Activity
      root.classList.remove(ctor.name.toLowerCase())
      if (ctor.action) {
        root.classList.remove(ctor.action.replace(/\//g, '-'))
      }
    }
    if (activity) {
      const ctor = activity.constructor as typeof Activity
      root.classList.add(ctor.name.toLowerCase())
      if (ctor.action) {
        root.classList.add(ctor.action.replace(/\//g, '-'))
      }
    }
  }

  /**
   * A helper method to handle the transition between activities, ensuring
   * lifecycle methods are called in the correct order (pause the previous activity,
   * then start/resume the new one). This centralizes lifecycle management.
   *
   * @param activity The activity to bring to the foreground.
   */
  private async updateCurrentActivity(activity: Activity, isModal: boolean): Promise<void> {
    this.setupStyles(activity, this.currentActivity)
    if (
      this.currentActivity &&
      this.currentActivity !== activity &&
      this.currentActivity.lifecycle !== ActivityLifecycle.Destroyed &&
      // Make sure we don't pause the current activity if it's already paused.
      this.currentActivity.lifecycle !== ActivityLifecycle.Paused
    ) {
      await this.currentActivity.onPause()
      this.currentActivity.lifecycle = ActivityLifecycle.Paused
    }
    if (activity.lifecycle === ActivityLifecycle.Paused) {
      await activity.onResume()
      if (this.isDestroyed(activity)) {
        return
      }
      activity.lifecycle = ActivityLifecycle.Resumed
      activity.requestUpdate()
    } else if (activity.lifecycle === ActivityLifecycle.Stopped) {
      await activity.onRestart()
      activity.lifecycle = ActivityLifecycle.Resumed
    } else if (activity.lifecycle === ActivityLifecycle.Destroyed) {
      throw new Error(`Invalid state. The activity is already destroyed.`)
    } else if (activity.lifecycle === ActivityLifecycle.Created) {
      await activity.onStart()
      if (this.isDestroyed(activity)) {
        return
      }
      activity.lifecycle = ActivityLifecycle.Started
      await activity.onResume()
      if (this.isDestroyed(activity)) {
        return
      }
      activity.lifecycle = ActivityLifecycle.Resumed
      activity.requestUpdate()
    } else if (activity.lifecycle === ActivityLifecycle.Started) {
      await activity.onResume()
      if (this.isDestroyed(activity)) {
        return
      }
      activity.lifecycle = ActivityLifecycle.Resumed
      activity.requestUpdate()
    } else if (activity.lifecycle === ActivityLifecycle.Initialized) {
      // TODO: Figure out intent passing here.
      await activity.onCreate()
      activity.lifecycle = ActivityLifecycle.Created
      if (this.isDestroyed(activity)) {
        // the activity finished and the manager already took care of this situation.
        return
      }
      await activity.onStart()
      if (this.isDestroyed(activity)) {
        return
      }
      activity.lifecycle = ActivityLifecycle.Started
      await activity.onResume()
      if (this.isDestroyed(activity)) {
        return
      }
      activity.lifecycle = ActivityLifecycle.Resumed
      activity.requestUpdate()
    }
    if (!isModal) {
      // With a modal activity, it renders directly to the `<body>` and renders outside
      // of the normal rendering flow. We keep the previous activity rendered.
      this.currentActivity = activity
    }
  }

  /**
   * Allows navigating back through the activity stack.
   * Call `ActivityManager.navigateBack()` when the user performs a back action
   * (e.g., clicks a back button).
   */
  async navigateBack(): Promise<void> {
    if (this.activityStack.length > 1 && this.currentActivity) {
      // Finish current, which will resume previous
      await this.finishActivity(this.currentActivity)
    }
    // Else do nothing (or display a message, or exit the app)
  }

  createRequestCode(): number {
    const min = 1000
    const max = 9999
    return Math.floor(Math.random() * (max - min) + min)
  }

  findActiveActivity(action: string): Activity | undefined {
    return this.activityStack.find((entry) => entry.action === action)?.activity
  }
}
